@mingozhou/dsh-replay 0.3.3 → 0.4.2
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/README.md +225 -164
- package/README.zh.md +225 -164
- package/assets/icon.png +0 -0
- package/lib/client.js +955 -2419
- package/lib/core.js +557 -86
- package/lib/index.js +1043 -51
- package/lib/types/src/client/api.d.ts +44 -0
- package/lib/types/src/client/components/AuditView.d.ts +2 -1
- package/lib/types/src/client/components/CompareView.d.ts +2 -1
- package/lib/types/src/client/components/DashboardView.d.ts +8 -0
- package/lib/types/src/client/components/ExportDialog.d.ts +9 -0
- package/lib/types/src/client/components/ReplayApp.d.ts +4 -2
- package/lib/types/src/client/components/ReplayModal.d.ts +1 -1
- package/lib/types/src/client/components/SessionPicker.d.ts +4 -3
- package/lib/types/src/client/components/TimelineView.d.ts +2 -1
- package/lib/types/src/client/export.d.ts +6 -1
- package/lib/types/src/client/i18n.d.ts +8 -1
- package/lib/types/src/client/pricing.d.ts +6 -0
- package/lib/types/src/core/audit.d.ts +18 -0
- package/lib/types/src/core/compare.d.ts +8 -3
- package/lib/types/src/core/cost.d.ts +21 -3
- package/lib/types/src/core/index.d.ts +1 -0
- package/lib/types/src/core/redact.d.ts +40 -0
- package/lib/types/src/core/timeline.d.ts +9 -0
- package/lib/types/src/index.d.ts +52 -1
- package/lib/viewer.js +282 -29
- package/package.json +118 -107
- package/scripts/build.mjs +8 -2
- package/src/client/api.ts +114 -10
- package/src/client/components/AuditView.tsx +89 -10
- package/src/client/components/CompareView.tsx +89 -13
- package/src/client/components/DashboardView.tsx +232 -0
- package/src/client/components/ExportDialog.tsx +136 -0
- package/src/client/components/ItemDetail.tsx +36 -4
- package/src/client/components/OverviewView.tsx +150 -34
- package/src/client/components/ReplayApp.tsx +207 -60
- package/src/client/components/ReplayModal.tsx +41 -6
- package/src/client/components/SessionPicker.tsx +55 -27
- package/src/client/components/TimelineView.tsx +106 -41
- package/src/client/export.ts +20 -4
- package/src/client/i18n.ts +266 -45
- package/src/client/index.ts +7 -5
- package/src/client/mascot-image.ts +1 -1
- package/src/client/pricing.ts +90 -0
- package/src/client/styles.css +250 -1
- package/src/client/viewer-entry.tsx +25 -4
- package/src/core/audit.ts +274 -67
- package/src/core/compare.ts +0 -0
- package/src/core/cost.ts +94 -16
- package/src/core/index.ts +1 -0
- package/src/core/redact.ts +182 -0
- package/src/core/timeline.ts +107 -12
- package/src/index.ts +290 -54
package/lib/core.js
CHANGED
|
@@ -90,11 +90,34 @@ function fromInspection(header, records) {
|
|
|
90
90
|
// src/core/timeline.ts
|
|
91
91
|
function contentText(message) {
|
|
92
92
|
if (!message?.content) return "";
|
|
93
|
-
return message.content.filter((b) => typeof b.text === "string").map((b) => b.text).join("");
|
|
93
|
+
return message.content.filter((b) => typeof b.text === "string" && b.type !== "reasoning").map((b) => b.text).join("");
|
|
94
|
+
}
|
|
95
|
+
function contentReasoning(message) {
|
|
96
|
+
if (!message?.content) return "";
|
|
97
|
+
return message.content.filter((b) => b.type === "reasoning" && typeof b.text === "string").map((b) => b.text).join("");
|
|
98
|
+
}
|
|
99
|
+
function oneLine(text, max) {
|
|
100
|
+
return truncate(text.replaceAll(/\s+/gu, " ").trim(), max);
|
|
101
|
+
}
|
|
102
|
+
function summarizeToolArguments(name, raw) {
|
|
103
|
+
let args;
|
|
104
|
+
try {
|
|
105
|
+
const parsed = JSON.parse(raw);
|
|
106
|
+
if (typeof parsed === "object" && parsed !== null) args = parsed;
|
|
107
|
+
} catch {
|
|
108
|
+
args = void 0;
|
|
109
|
+
}
|
|
110
|
+
if (args === void 0) return oneLine(raw, 64);
|
|
111
|
+
const keys = ["command", "cmd", "script", "path", "file_path", "filePath", "url", "query", "pattern", "description"];
|
|
112
|
+
for (const key of keys) {
|
|
113
|
+
const value = args[key];
|
|
114
|
+
if (typeof value === "string" && value.trim() !== "") return oneLine(value, 64);
|
|
115
|
+
}
|
|
116
|
+
return name;
|
|
94
117
|
}
|
|
95
118
|
function truncate(text, max = 96) {
|
|
96
|
-
const
|
|
97
|
-
return
|
|
119
|
+
const oneLine2 = text.replaceAll(/\s+/gu, " ").trim();
|
|
120
|
+
return oneLine2.length > max ? `${oneLine2.slice(0, max - 1)}\u2026` : oneLine2;
|
|
98
121
|
}
|
|
99
122
|
function tryPretty(raw) {
|
|
100
123
|
try {
|
|
@@ -170,10 +193,13 @@ function buildTimeline(session) {
|
|
|
170
193
|
const toolCallsBySeq = /* @__PURE__ */ new Map();
|
|
171
194
|
const toolCalls = [];
|
|
172
195
|
const usageByStep = /* @__PURE__ */ new Map();
|
|
196
|
+
const usageModelByStep = /* @__PURE__ */ new Map();
|
|
197
|
+
const modelTotals = {};
|
|
173
198
|
const models = /* @__PURE__ */ new Set();
|
|
174
199
|
const shadowed = shadowedSeqs(events);
|
|
175
200
|
let seedBoundarySeq;
|
|
176
201
|
let activeTurn;
|
|
202
|
+
let activeModel;
|
|
177
203
|
let sessionTitle;
|
|
178
204
|
const ensureTurn = (turn, event) => {
|
|
179
205
|
let model = turns.get(turn);
|
|
@@ -245,10 +271,16 @@ function buildTimeline(session) {
|
|
|
245
271
|
const message = get(d, "message");
|
|
246
272
|
const usage = get(d, "usage");
|
|
247
273
|
const text = contentText(message);
|
|
274
|
+
const reasoningText = contentReasoning(message);
|
|
248
275
|
const source = message?.source;
|
|
249
|
-
if (typeof source?.["model"] === "string")
|
|
276
|
+
if (typeof source?.["model"] === "string") {
|
|
277
|
+
activeModel = source["model"];
|
|
278
|
+
models.add(activeModel);
|
|
279
|
+
}
|
|
250
280
|
if (usage) {
|
|
251
|
-
|
|
281
|
+
const key = `${String(turn)}/${String(step)}`;
|
|
282
|
+
usageByStep.set(key, usage);
|
|
283
|
+
if (activeModel !== void 0) usageModelByStep.set(key, activeModel);
|
|
252
284
|
}
|
|
253
285
|
push({
|
|
254
286
|
kind: "assistant",
|
|
@@ -259,6 +291,7 @@ function buildTimeline(session) {
|
|
|
259
291
|
type: event.type,
|
|
260
292
|
label: truncate(text) || "(assistant message)",
|
|
261
293
|
text,
|
|
294
|
+
reasoningText,
|
|
262
295
|
usage,
|
|
263
296
|
event
|
|
264
297
|
});
|
|
@@ -268,16 +301,23 @@ function buildTimeline(session) {
|
|
|
268
301
|
const chunk = get(d, "chunk");
|
|
269
302
|
if (get(chunk, "type") === "usage") {
|
|
270
303
|
const usage = get(chunk, "usage");
|
|
271
|
-
|
|
304
|
+
const key = `${String(turn)}/${String(step)}`;
|
|
305
|
+
usageByStep.set(key, usage);
|
|
306
|
+
if (activeModel !== void 0 && !usageModelByStep.has(key)) {
|
|
307
|
+
usageModelByStep.set(key, activeModel);
|
|
308
|
+
}
|
|
272
309
|
}
|
|
273
310
|
break;
|
|
274
311
|
}
|
|
275
312
|
case "tool/call": {
|
|
313
|
+
const raw = String(get(d, "arguments") ?? "");
|
|
314
|
+
const name = String(get(d, "name") ?? "");
|
|
276
315
|
const info = {
|
|
277
316
|
callId: String(get(d, "callId") ?? ""),
|
|
278
|
-
name
|
|
279
|
-
argumentsRaw:
|
|
280
|
-
argumentsPretty: tryPretty(
|
|
317
|
+
name,
|
|
318
|
+
argumentsRaw: raw,
|
|
319
|
+
argumentsPretty: tryPretty(raw),
|
|
320
|
+
summary: summarizeToolArguments(name, raw),
|
|
281
321
|
callSeq: event.seq,
|
|
282
322
|
callTime: event.time
|
|
283
323
|
};
|
|
@@ -290,7 +330,7 @@ function buildTimeline(session) {
|
|
|
290
330
|
turn,
|
|
291
331
|
step,
|
|
292
332
|
type: event.type,
|
|
293
|
-
label:
|
|
333
|
+
label: info.summary === name ? name : `${name} \xB7 ${info.summary}`,
|
|
294
334
|
toolCall: info,
|
|
295
335
|
event
|
|
296
336
|
});
|
|
@@ -302,7 +342,7 @@ function buildTimeline(session) {
|
|
|
302
342
|
const callId = String(source?.["callId"] ?? "");
|
|
303
343
|
const info = toolCallsBySeq.get(callId);
|
|
304
344
|
const block = message?.content?.[0];
|
|
305
|
-
const resultText = typeof get(block, "text") === "string" ? get(block, "text") : contentText({ content: get(block, "content") });
|
|
345
|
+
const resultText = contentText(message) || (typeof get(block, "text") === "string" ? get(block, "text") : contentText({ content: get(block, "content") }));
|
|
306
346
|
const error = get(d, "error");
|
|
307
347
|
if (info) {
|
|
308
348
|
info.resultSeq = event.seq;
|
|
@@ -324,9 +364,44 @@ function buildTimeline(session) {
|
|
|
324
364
|
event
|
|
325
365
|
});
|
|
326
366
|
break;
|
|
367
|
+
case "system/message": {
|
|
368
|
+
const message = get(d, "message");
|
|
369
|
+
const text = contentText(message);
|
|
370
|
+
push({
|
|
371
|
+
kind: "meta",
|
|
372
|
+
seq: event.seq,
|
|
373
|
+
time: event.time,
|
|
374
|
+
turn: typeof turn === "number" ? turn : void 0,
|
|
375
|
+
step: typeof step === "number" ? step : void 0,
|
|
376
|
+
type: event.type,
|
|
377
|
+
label: "system prompt",
|
|
378
|
+
text,
|
|
379
|
+
event
|
|
380
|
+
});
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
case "model/selection": {
|
|
384
|
+
const model = get(d, "model");
|
|
385
|
+
if (typeof model === "string" && model !== "") {
|
|
386
|
+
activeModel = model;
|
|
387
|
+
models.add(model);
|
|
388
|
+
push({
|
|
389
|
+
kind: "meta",
|
|
390
|
+
seq: event.seq,
|
|
391
|
+
time: event.time,
|
|
392
|
+
type: event.type,
|
|
393
|
+
label: `model selected: ${model}`,
|
|
394
|
+
event
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
327
399
|
case "request/context": {
|
|
328
400
|
const model = get(d, "model");
|
|
329
|
-
if (typeof model === "string")
|
|
401
|
+
if (typeof model === "string") {
|
|
402
|
+
activeModel = model;
|
|
403
|
+
models.add(model);
|
|
404
|
+
}
|
|
330
405
|
break;
|
|
331
406
|
}
|
|
332
407
|
case "session/title": {
|
|
@@ -353,13 +428,31 @@ function buildTimeline(session) {
|
|
|
353
428
|
}
|
|
354
429
|
}
|
|
355
430
|
const totals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, billedInput: 0 };
|
|
431
|
+
const emptyTotals = () => ({
|
|
432
|
+
input: 0,
|
|
433
|
+
output: 0,
|
|
434
|
+
cacheRead: 0,
|
|
435
|
+
cacheWrite: 0,
|
|
436
|
+
reasoning: 0,
|
|
437
|
+
billedInput: 0
|
|
438
|
+
});
|
|
356
439
|
for (const [key, usage] of usageByStep) {
|
|
357
440
|
const [turnStr, stepStr] = key.split("/");
|
|
441
|
+
const billed = usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0);
|
|
358
442
|
totals.input += usage.inputTokens;
|
|
359
443
|
totals.output += usage.outputTokens;
|
|
360
444
|
totals.cacheRead += usage.cacheReadTokens ?? 0;
|
|
361
445
|
totals.cacheWrite += usage.cacheWriteTokens ?? 0;
|
|
362
446
|
totals.reasoning += usage.reasoningTokens ?? 0;
|
|
447
|
+
const modelName = usageModelByStep.get(key) ?? activeModel ?? "unknown";
|
|
448
|
+
const modelTotal = modelTotals[modelName] ?? emptyTotals();
|
|
449
|
+
modelTotal.input += usage.inputTokens;
|
|
450
|
+
modelTotal.output += usage.outputTokens;
|
|
451
|
+
modelTotal.cacheRead += usage.cacheReadTokens ?? 0;
|
|
452
|
+
modelTotal.cacheWrite += usage.cacheWriteTokens ?? 0;
|
|
453
|
+
modelTotal.reasoning += usage.reasoningTokens ?? 0;
|
|
454
|
+
modelTotal.billedInput += billed;
|
|
455
|
+
modelTotals[modelName] = modelTotal;
|
|
363
456
|
const turnModel = turns.get(Number(turnStr));
|
|
364
457
|
const stepModel = turnModel?.steps.find((s) => s.step === Number(stepStr));
|
|
365
458
|
if (stepModel) stepModel.usage = usage;
|
|
@@ -381,6 +474,7 @@ function buildTimeline(session) {
|
|
|
381
474
|
turns: orderedTurns,
|
|
382
475
|
items,
|
|
383
476
|
totals,
|
|
477
|
+
modelTotals,
|
|
384
478
|
usageSamples,
|
|
385
479
|
toolCalls,
|
|
386
480
|
durationMs: endTime - startTime,
|
|
@@ -393,129 +487,383 @@ function buildTimeline(session) {
|
|
|
393
487
|
};
|
|
394
488
|
}
|
|
395
489
|
|
|
490
|
+
// src/core/redact.ts
|
|
491
|
+
var RULES = [
|
|
492
|
+
{ kind: "openai-key", re: /\bsk-[A-Za-z0-9_-]{16,}\b/g, replacement: "[REDACTED:openai-key]" },
|
|
493
|
+
{
|
|
494
|
+
kind: "github-token",
|
|
495
|
+
re: /\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})\b/g,
|
|
496
|
+
replacement: "[REDACTED:github-token]"
|
|
497
|
+
},
|
|
498
|
+
{ kind: "aws-key", re: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: "[REDACTED:aws-key]" },
|
|
499
|
+
{ kind: "google-key", re: /\bAIza[0-9A-Za-z_-]{20,}\b/g, replacement: "[REDACTED:google-key]" },
|
|
500
|
+
{ kind: "slack-token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, replacement: "[REDACTED:slack-token]" },
|
|
501
|
+
{
|
|
502
|
+
kind: "private-key",
|
|
503
|
+
re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
504
|
+
replacement: "[REDACTED:private-key]"
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
kind: "jwt",
|
|
508
|
+
re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g,
|
|
509
|
+
replacement: "[REDACTED:jwt]"
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
kind: "env-assignment",
|
|
513
|
+
re: /(\b[A-Z][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PRIVATE|CREDENTIAL)[A-Z0-9_]*\s*=\s*)(["']?)([^\s"'\\]{4,})\2/g,
|
|
514
|
+
replacement: (_m, prefix, quote) => `${prefix}${quote}[REDACTED]${quote}`
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
kind: "key-value",
|
|
518
|
+
re: /(["']?(?:api[_-]?key|secret|token|password|passwd|authorization|jwt[_-]?secret)["']?\s*[:=]\s*["']?)([^"'\s,}]{4,})/gi,
|
|
519
|
+
replacement: (_m, prefix) => `${prefix}[REDACTED]`
|
|
520
|
+
},
|
|
521
|
+
{ kind: "bearer", re: /(\bBearer\s+)[A-Za-z0-9._~+/-]{8,}/gi, replacement: "$1[REDACTED]" },
|
|
522
|
+
{
|
|
523
|
+
kind: "basic-auth-url",
|
|
524
|
+
re: /(https?:\/\/[^/\s:@]+:)([^@\s/]+)(@)/g,
|
|
525
|
+
replacement: "$1[REDACTED]$3"
|
|
526
|
+
},
|
|
527
|
+
{ kind: "windows-user", re: /([A-Za-z]:\\Users\\)([^\\\s"']+)/g, replacement: "$1[user]" },
|
|
528
|
+
{ kind: "unix-home", re: /(\/(?:home|Users)\/)([^/\s"']+)/g, replacement: "$1[user]" },
|
|
529
|
+
{ kind: "email", re: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, replacement: "[email]" },
|
|
530
|
+
{ kind: "session-id", re: /\bsession-[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\b/gi, replacement: "session-[id]" }
|
|
531
|
+
];
|
|
532
|
+
var CREDENTIAL_KINDS = /* @__PURE__ */ new Set([
|
|
533
|
+
"openai-key",
|
|
534
|
+
"github-token",
|
|
535
|
+
"aws-key",
|
|
536
|
+
"google-key",
|
|
537
|
+
"slack-token",
|
|
538
|
+
"private-key",
|
|
539
|
+
"jwt",
|
|
540
|
+
"env-assignment",
|
|
541
|
+
"key-value",
|
|
542
|
+
"bearer",
|
|
543
|
+
"basic-auth-url"
|
|
544
|
+
]);
|
|
545
|
+
function redactText(input) {
|
|
546
|
+
let text = input;
|
|
547
|
+
let count = 0;
|
|
548
|
+
const kinds = /* @__PURE__ */ new Set();
|
|
549
|
+
for (const rule of RULES) {
|
|
550
|
+
rule.re.lastIndex = 0;
|
|
551
|
+
const matches = text.match(rule.re);
|
|
552
|
+
if (matches === null) continue;
|
|
553
|
+
count += matches.length;
|
|
554
|
+
kinds.add(rule.kind);
|
|
555
|
+
rule.re.lastIndex = 0;
|
|
556
|
+
text = text.replace(rule.re, rule.replacement);
|
|
557
|
+
}
|
|
558
|
+
return { text, count, kinds: [...kinds] };
|
|
559
|
+
}
|
|
560
|
+
function containsSecret(input) {
|
|
561
|
+
for (const rule of RULES) {
|
|
562
|
+
if (!CREDENTIAL_KINDS.has(rule.kind)) continue;
|
|
563
|
+
rule.re.lastIndex = 0;
|
|
564
|
+
const hit = rule.re.test(input);
|
|
565
|
+
rule.re.lastIndex = 0;
|
|
566
|
+
if (hit) return true;
|
|
567
|
+
}
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
function redactValue(value, depth = 0) {
|
|
571
|
+
if (depth > 20) return { value, count: 0 };
|
|
572
|
+
if (typeof value === "string") {
|
|
573
|
+
const result = redactText(value);
|
|
574
|
+
return { value: result.text, count: result.count };
|
|
575
|
+
}
|
|
576
|
+
if (Array.isArray(value)) {
|
|
577
|
+
let count = 0;
|
|
578
|
+
const next = value.map((item) => {
|
|
579
|
+
const result = redactValue(item, depth + 1);
|
|
580
|
+
count += result.count;
|
|
581
|
+
return result.value;
|
|
582
|
+
});
|
|
583
|
+
return { value: next, count };
|
|
584
|
+
}
|
|
585
|
+
if (typeof value === "object" && value !== null) {
|
|
586
|
+
let count = 0;
|
|
587
|
+
const next = {};
|
|
588
|
+
for (const [key, item] of Object.entries(value)) {
|
|
589
|
+
const result = redactValue(item, depth + 1);
|
|
590
|
+
count += result.count;
|
|
591
|
+
next[key] = result.value;
|
|
592
|
+
}
|
|
593
|
+
return { value: next, count };
|
|
594
|
+
}
|
|
595
|
+
return { value, count: 0 };
|
|
596
|
+
}
|
|
597
|
+
function redactSession(session) {
|
|
598
|
+
let count = 0;
|
|
599
|
+
const kinds = /* @__PURE__ */ new Set();
|
|
600
|
+
const header = { ...session.header };
|
|
601
|
+
for (const key of ["cwd", "title"]) {
|
|
602
|
+
const value = header[key];
|
|
603
|
+
if (typeof value === "string") {
|
|
604
|
+
const result = redactText(value);
|
|
605
|
+
header[key] = result.text;
|
|
606
|
+
count += result.count;
|
|
607
|
+
for (const kind of result.kinds) kinds.add(kind);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
const events = session.events.map((event) => {
|
|
611
|
+
const data = redactValue(event.data);
|
|
612
|
+
count += data.count;
|
|
613
|
+
if (data.count > 0) {
|
|
614
|
+
for (const rule of RULES) {
|
|
615
|
+
rule.re.lastIndex = 0;
|
|
616
|
+
if (rule.re.test(JSON.stringify(event.data))) kinds.add(rule.kind);
|
|
617
|
+
rule.re.lastIndex = 0;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return { ...event, data: data.value };
|
|
621
|
+
});
|
|
622
|
+
return { session: { ...session, header, events }, count, kinds: [...kinds] };
|
|
623
|
+
}
|
|
624
|
+
|
|
396
625
|
// src/core/audit.ts
|
|
397
626
|
function get2(obj, key) {
|
|
398
627
|
return typeof obj === "object" && obj !== null ? obj[key] : void 0;
|
|
399
628
|
}
|
|
400
629
|
function str(value) {
|
|
401
|
-
|
|
630
|
+
if (typeof value === "string") return value;
|
|
631
|
+
try {
|
|
632
|
+
return JSON.stringify(value) ?? "";
|
|
633
|
+
} catch {
|
|
634
|
+
return "";
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
function clipped(value, max = 500) {
|
|
638
|
+
return redactText(value).text.slice(0, max);
|
|
402
639
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
640
|
+
function toolName(event) {
|
|
641
|
+
return str(get2(event.data, "name"));
|
|
642
|
+
}
|
|
643
|
+
function argsOf(event) {
|
|
644
|
+
return str(get2(event.data, "arguments"));
|
|
645
|
+
}
|
|
646
|
+
function parseArgs(raw) {
|
|
647
|
+
try {
|
|
648
|
+
const value = JSON.parse(raw);
|
|
649
|
+
return typeof value === "object" && value !== null ? value : void 0;
|
|
650
|
+
} catch {
|
|
651
|
+
return void 0;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
var PATH_KEYS = [
|
|
655
|
+
"path",
|
|
656
|
+
"file_path",
|
|
657
|
+
"filePath",
|
|
658
|
+
"target",
|
|
659
|
+
"destination",
|
|
660
|
+
"dest",
|
|
661
|
+
"filename",
|
|
662
|
+
"file",
|
|
663
|
+
"name"
|
|
664
|
+
];
|
|
665
|
+
function pathOf(raw) {
|
|
666
|
+
const args = parseArgs(raw);
|
|
667
|
+
if (args === void 0) return void 0;
|
|
668
|
+
for (const key of PATH_KEYS) {
|
|
669
|
+
const value = args[key];
|
|
670
|
+
if (typeof value === "string" && value !== "" && /[\\/.]/u.test(value)) return value;
|
|
671
|
+
}
|
|
672
|
+
return void 0;
|
|
673
|
+
}
|
|
674
|
+
var SHELL_TOOLS = /^(bash|shell|exec|run_command|terminal|subprocess|pwsh|powershell|cmd|run_code|code_execution)/iu;
|
|
675
|
+
var WRITE_TOOLS = /^(write|write_file|edit|edit_file|create_file|str_replace|multi_edit|apply_patch|notebook_edit|fs_write|mv|move|rename|copy|rm|remove|delete|del|remove_item|rmdir|rd)/iu;
|
|
676
|
+
var DESTRUCTIVE_TOOLS = /^(rm|remove|delete|del|remove_item|rmdir|rd|mv|move|rename)/iu;
|
|
677
|
+
var READ_TOOLS = /^(read|read_file|view|open|cat|type|get-content|grep|search|glob|list|find|head|tail|less|more|strings|xxd)/iu;
|
|
678
|
+
var NETWORK_TOOLS = /^(fetch|web_fetch|http|curl|download|web_search|browser|invoke-webrequest|iwr|invoke-restmethod|start-bitstransfer)/iu;
|
|
406
679
|
var DANGEROUS_SHELL = [
|
|
407
|
-
|
|
408
|
-
{ re: /\
|
|
409
|
-
{ re: /\
|
|
410
|
-
{ re: /\b(
|
|
411
|
-
{ re: /\
|
|
412
|
-
|
|
413
|
-
{ re:
|
|
414
|
-
{ re: /\b(
|
|
680
|
+
// --- destructive deletes -------------------------------------------------
|
|
681
|
+
{ re: /\brm\s+(-[a-z]*[rf][a-z]*\s+)+/iu, why: "recursive/forced delete", key: "rm-rf", severity: "critical" },
|
|
682
|
+
{ re: /\bremove-item\b[^\n]*(?:-recurse|-force)/iu, why: "recursive/forced delete", key: "rm-rf", severity: "critical" },
|
|
683
|
+
{ re: /\b(?:del|erase)\b[^\n]*\/[sq]/iu, why: "forced delete", key: "rm-rf", severity: "critical" },
|
|
684
|
+
{ re: /\b(?:rd|rmdir)\b[^\n]*\/s/iu, why: "recursive directory delete", key: "rm-rf", severity: "critical" },
|
|
685
|
+
// --- privilege / system --------------------------------------------------
|
|
686
|
+
{ re: /\bsudo\b/iu, why: "privilege escalation", key: "sudo", severity: "critical" },
|
|
687
|
+
{ re: /\b(?:takeown|icacls)\b[^\n]*(?:grant|\/grant|\/f|\/reset)/iu, why: "ownership/permission change", key: "chmod", severity: "serious" },
|
|
688
|
+
{ re: /\bchmod\s+([0-7]{3,4}|\+x)/u, why: "permission change", key: "chmod", severity: "serious" },
|
|
689
|
+
{ re: /\bset-executionpolicy\b/iu, why: "script execution policy change", key: "exec-policy", severity: "serious" },
|
|
690
|
+
{ re: /\b(?:bcdedit|vssadmin\s+delete|wevtutil\s+cl)\b/iu, why: "boot/shadow/log tampering", key: "system-tamper", severity: "critical" },
|
|
691
|
+
{ re: /\b(?:reg\s+delete|schtasks\b[^\n]*\/create|sc\s+create|net\s+user|net\s+localgroup)\b/iu, why: "persistence or account change", key: "persistence", severity: "serious" },
|
|
692
|
+
// --- disk / remote code --------------------------------------------------
|
|
693
|
+
{ re: /\b(?:dd|mkfs|fdisk|diskpart|format)\b/iu, why: "raw disk operation", key: "disk", severity: "critical" },
|
|
694
|
+
{ re: /\b(?:iwr|invoke-webrequest|curl|wget|start-bitstransfer)\b[^\n|;]*(?:\||;)\s*(?:iex|invoke-expression)\b/iu, why: "pipe remote script to shell", key: "curl-pipe", severity: "critical" },
|
|
695
|
+
{ re: /\b(?:downloadstring|downloadfile)\b/iu, why: "download and execute", key: "curl-pipe", severity: "critical" },
|
|
696
|
+
{ re: /\b(?:powershell|pwsh)\b[^\n]*-e(?:nc|ncodedcommand)?\s+[A-Za-z0-9+/=]{40,}/iu, why: "encoded PowerShell command", key: "encoded-shell", severity: "critical" },
|
|
697
|
+
// --- credentials / exfiltration -----------------------------------------
|
|
698
|
+
{ re: /\b(?:export|\$env:|\bset\b|\becho\b)[^\n]*(?:api[_-]?key|token|secret|password|passwd)/iu, why: "credential material in command", key: "creds", severity: "serious" },
|
|
699
|
+
{ re: /\b(?:get-content|type|cat)\b[^\n]*(?:\.env\b|id_rsa|\.ssh[\\/]|credentials)/iu, why: "credential file read", key: "creds", severity: "serious" },
|
|
700
|
+
{ re: /\b(?:curl|wget|iwr|invoke-webrequest|invoke-restmethod)\b[^\n]*(?:-d\b|--data|--upload-file|-f\b|\.env\b|secret|token|password)/iu, why: "possible credential exfiltration", key: "exfil", severity: "critical" },
|
|
701
|
+
{ re: /\b(?:ssh|scp|rsync)\b[^\n]*(?:@|\bremote\b)/iu, why: "remote host access", key: "remote", severity: "warning" },
|
|
702
|
+
{ re: /\b(?:invoke-command|enter-pssession|winrs)\b/iu, why: "remote execution", key: "remote", severity: "serious" },
|
|
703
|
+
// --- supply chain / repo ------------------------------------------------
|
|
704
|
+
{ re: /\bgit\s+push\s+[^\n]*--force/iu, why: "force push", key: "force-push", severity: "serious" },
|
|
705
|
+
{ re: /\b(?:npm|pnpm|yarn)\s+publish\b/iu, why: "package publish", key: "publish", severity: "warning" },
|
|
706
|
+
{ re: /\bgh\s+release\s+create\b/iu, why: "release publish", key: "publish", severity: "warning" }
|
|
415
707
|
];
|
|
416
|
-
var SENSITIVE_PATH = /(
|
|
708
|
+
var SENSITIVE_PATH = /(?:^|[\\/])(?:\.env(?:\.[\w-]+)?(?:[\\/]|$)|\.ssh(?:[\\/]|$)|id_rsa|id_ed25519|\.aws(?:[\\/]|$)|\.kube(?:[\\/]|$)|\.docker(?:[\\/]config\.json|$)|credentials|\.npmrc|\.netrc|secrets?\.|\.pem|\.pfx|\.p12|ntuser\.dat|\.git[\\/]config|\.config[\\/]gh[\\/]hosts\.yml)/iu;
|
|
709
|
+
var NETWORK_RISK = /(?:--data|-d\s|--upload-file|-f\s|body\s*[:=]|\.env\b|secret|token|password|passwd|private[_-]?key|authorization|api[_-]?key)/iu;
|
|
710
|
+
function messageText(message) {
|
|
711
|
+
const content = get2(message, "content");
|
|
712
|
+
if (!Array.isArray(content)) return "";
|
|
713
|
+
return content.map((block) => typeof get2(block, "text") === "string" ? get2(block, "text") : "").join("");
|
|
714
|
+
}
|
|
715
|
+
function findDanger(event) {
|
|
716
|
+
if (event.type !== "tool/call") return void 0;
|
|
717
|
+
const name = toolName(event);
|
|
718
|
+
if (!SHELL_TOOLS.test(name)) return void 0;
|
|
719
|
+
const args = argsOf(event);
|
|
720
|
+
return DANGEROUS_SHELL.find((pattern) => pattern.re.test(args));
|
|
721
|
+
}
|
|
417
722
|
var DEFAULT_RULES = [
|
|
418
723
|
{
|
|
419
724
|
id: "shell-exec",
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
return DANGEROUS_SHELL.some((p) => p.re.test(args)) ? "critical" : "warning";
|
|
423
|
-
},
|
|
725
|
+
category: "shell",
|
|
726
|
+
severity: (event) => findDanger(event)?.severity ?? "warning",
|
|
424
727
|
match: (event) => {
|
|
425
728
|
if (event.type !== "tool/call") return void 0;
|
|
426
|
-
const name =
|
|
729
|
+
const name = toolName(event);
|
|
427
730
|
if (!SHELL_TOOLS.test(name)) return void 0;
|
|
428
|
-
const args =
|
|
429
|
-
const hit = DANGEROUS_SHELL.find((
|
|
731
|
+
const args = argsOf(event);
|
|
732
|
+
const hit = DANGEROUS_SHELL.find((pattern) => pattern.re.test(args));
|
|
733
|
+
if (hit === void 0) return void 0;
|
|
430
734
|
return {
|
|
431
|
-
title:
|
|
432
|
-
detail: args
|
|
433
|
-
messageKey:
|
|
434
|
-
params:
|
|
735
|
+
title: `shell: ${hit.why}`,
|
|
736
|
+
detail: clipped(args),
|
|
737
|
+
messageKey: "f.shell.dangerous",
|
|
738
|
+
params: { whyKey: hit.key, why: hit.why, name }
|
|
435
739
|
};
|
|
436
740
|
}
|
|
437
741
|
},
|
|
438
742
|
{
|
|
439
743
|
id: "file-write",
|
|
440
|
-
|
|
744
|
+
category: "file",
|
|
745
|
+
severity: (event) => {
|
|
746
|
+
const args = argsOf(event);
|
|
747
|
+
const path = pathOf(args) ?? args;
|
|
748
|
+
return SENSITIVE_PATH.test(path) || containsSecret(args) ? "critical" : "warning";
|
|
749
|
+
},
|
|
441
750
|
match: (event) => {
|
|
442
751
|
if (event.type !== "tool/call") return void 0;
|
|
443
|
-
const name =
|
|
752
|
+
const name = toolName(event);
|
|
444
753
|
if (!WRITE_TOOLS.test(name)) return void 0;
|
|
445
|
-
const args =
|
|
446
|
-
const
|
|
754
|
+
const args = argsOf(event);
|
|
755
|
+
const path = pathOf(args) ?? args;
|
|
756
|
+
const sensitive = SENSITIVE_PATH.test(path);
|
|
757
|
+
const secret = containsSecret(args);
|
|
758
|
+
if (!sensitive && !secret && !DESTRUCTIVE_TOOLS.test(name)) return void 0;
|
|
759
|
+
const reason = sensitive ? "file write touches a sensitive path" : secret ? "credential material written to a file" : `destructive file operation via ${name}`;
|
|
447
760
|
return {
|
|
448
|
-
title:
|
|
449
|
-
detail: args
|
|
450
|
-
messageKey: sensitive ? "f.file.sensitive" : "f.file.
|
|
451
|
-
params: { name }
|
|
761
|
+
title: reason,
|
|
762
|
+
detail: clipped(args),
|
|
763
|
+
messageKey: sensitive ? "f.file.sensitive" : secret ? "f.file.secret" : "f.file.destructive",
|
|
764
|
+
params: { name, ...path !== args ? { path } : {} }
|
|
452
765
|
};
|
|
453
766
|
}
|
|
454
767
|
},
|
|
455
768
|
{
|
|
456
769
|
id: "sensitive-read",
|
|
770
|
+
category: "file",
|
|
457
771
|
severity: "serious",
|
|
458
772
|
match: (event) => {
|
|
459
773
|
if (event.type !== "tool/call") return void 0;
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
if (
|
|
463
|
-
|
|
774
|
+
const name = toolName(event);
|
|
775
|
+
if (WRITE_TOOLS.test(name) || SHELL_TOOLS.test(name) || NETWORK_TOOLS.test(name)) return void 0;
|
|
776
|
+
if (!READ_TOOLS.test(name)) return void 0;
|
|
777
|
+
const args = argsOf(event);
|
|
778
|
+
const path = pathOf(args) ?? args;
|
|
779
|
+
if (!SENSITIVE_PATH.test(path)) return void 0;
|
|
464
780
|
return {
|
|
465
781
|
title: `access to sensitive path via ${name}`,
|
|
466
|
-
detail: args
|
|
782
|
+
detail: clipped(args),
|
|
467
783
|
messageKey: "f.read.sensitive",
|
|
468
|
-
params: { name }
|
|
784
|
+
params: { name, ...path !== args ? { path } : {} }
|
|
469
785
|
};
|
|
470
786
|
}
|
|
471
787
|
},
|
|
472
788
|
{
|
|
473
789
|
id: "network",
|
|
474
|
-
|
|
790
|
+
category: "network",
|
|
791
|
+
severity: (event) => {
|
|
792
|
+
const args = argsOf(event);
|
|
793
|
+
return containsSecret(args) || SENSITIVE_PATH.test(args) ? "critical" : "warning";
|
|
794
|
+
},
|
|
475
795
|
match: (event) => {
|
|
476
796
|
if (event.type !== "tool/call") return void 0;
|
|
477
|
-
const name =
|
|
797
|
+
const name = toolName(event);
|
|
478
798
|
if (!NETWORK_TOOLS.test(name)) return void 0;
|
|
799
|
+
const args = argsOf(event);
|
|
800
|
+
if (!NETWORK_RISK.test(args)) return void 0;
|
|
479
801
|
return {
|
|
480
|
-
title: `network
|
|
481
|
-
detail:
|
|
802
|
+
title: `network request may carry sensitive data via ${name}`,
|
|
803
|
+
detail: clipped(args),
|
|
482
804
|
messageKey: "f.network",
|
|
483
805
|
params: { name }
|
|
484
806
|
};
|
|
485
807
|
}
|
|
486
808
|
},
|
|
809
|
+
{
|
|
810
|
+
id: "secret-output",
|
|
811
|
+
category: "secret",
|
|
812
|
+
severity: "critical",
|
|
813
|
+
match: (event) => {
|
|
814
|
+
if (event.type !== "tool/result") return void 0;
|
|
815
|
+
const message = get2(event.data, "message");
|
|
816
|
+
const text = messageText(message) || str(message) || str(get2(event.data, "error"));
|
|
817
|
+
if (text === "" || !containsSecret(text)) return void 0;
|
|
818
|
+
return {
|
|
819
|
+
title: "tool output contains credential material",
|
|
820
|
+
detail: clipped(text, 300),
|
|
821
|
+
messageKey: "f.output.secret"
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
},
|
|
487
825
|
{
|
|
488
826
|
id: "approval-denied",
|
|
827
|
+
category: "approval",
|
|
489
828
|
severity: "warning",
|
|
490
829
|
match: (event) => {
|
|
491
830
|
if (event.type !== "approval/decided") return void 0;
|
|
492
831
|
const outcome = str(get2(event.data, "outcome") ?? get2(event.data, "decision"));
|
|
493
|
-
if (!/
|
|
494
|
-
return { title: "user denied an approval", detail: outcome, messageKey: "f.approval.denied" };
|
|
832
|
+
if (!/deny|denied|reject|refus/iu.test(outcome)) return void 0;
|
|
833
|
+
return { title: "user denied an approval", detail: clipped(outcome), messageKey: "f.approval.denied" };
|
|
495
834
|
}
|
|
496
835
|
},
|
|
497
836
|
{
|
|
498
837
|
id: "sandbox-change",
|
|
499
|
-
|
|
500
|
-
|
|
838
|
+
category: "config",
|
|
839
|
+
severity: (event) => {
|
|
840
|
+
const mode = str(get2(event.data, "mode"));
|
|
841
|
+
return /danger|full/iu.test(mode) ? "critical" : "serious";
|
|
842
|
+
},
|
|
843
|
+
match: (event) => event.type === "sandbox/mode" ? {
|
|
844
|
+
title: `sandbox mode changed to ${str(get2(event.data, "mode")) || "unknown"}`,
|
|
845
|
+
detail: clipped(JSON.stringify(event.data)),
|
|
846
|
+
messageKey: "f.sandbox.change",
|
|
847
|
+
params: { mode: str(get2(event.data, "mode")) }
|
|
848
|
+
} : void 0
|
|
501
849
|
},
|
|
502
850
|
{
|
|
503
851
|
id: "permission-change",
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
params: { mode: str(get2(event.data, "mode")) }
|
|
852
|
+
category: "config",
|
|
853
|
+
severity: (event) => {
|
|
854
|
+
const preset = str(get2(event.data, "preset"));
|
|
855
|
+
return /danger|full/iu.test(preset) ? "critical" : "serious";
|
|
856
|
+
},
|
|
857
|
+
match: (event) => event.type === "permission/preset" ? {
|
|
858
|
+
title: `permission preset changed to ${str(get2(event.data, "preset")) || "unknown"}`,
|
|
859
|
+
detail: clipped(JSON.stringify(event.data)),
|
|
860
|
+
messageKey: "f.permission.change",
|
|
861
|
+
params: { preset: str(get2(event.data, "preset")) }
|
|
515
862
|
} : void 0
|
|
516
863
|
},
|
|
517
864
|
{
|
|
518
865
|
id: "tool-error",
|
|
866
|
+
category: "error",
|
|
519
867
|
severity: "warning",
|
|
520
868
|
match: (event) => {
|
|
521
869
|
if (event.type !== "tool/result") return void 0;
|
|
@@ -523,32 +871,62 @@ var DEFAULT_RULES = [
|
|
|
523
871
|
if (error === void 0) return void 0;
|
|
524
872
|
return {
|
|
525
873
|
title: `tool failed: ${str(get2(error, "name"))}`,
|
|
526
|
-
detail: str(get2(error, "code")),
|
|
874
|
+
detail: clipped(str(get2(error, "code"))),
|
|
527
875
|
messageKey: "f.tool.error",
|
|
528
|
-
params: { name: str(get2(error, "name")) }
|
|
876
|
+
params: { name: str(get2(error, "name")), code: str(get2(error, "code")) }
|
|
529
877
|
};
|
|
530
878
|
}
|
|
531
879
|
},
|
|
532
880
|
{
|
|
533
881
|
id: "turn-error",
|
|
882
|
+
category: "error",
|
|
534
883
|
severity: "warning",
|
|
535
884
|
match: (event) => {
|
|
536
885
|
if (event.type !== "turn/end") return void 0;
|
|
537
886
|
const kind = str(get2(get2(event.data, "reason"), "kind"));
|
|
538
|
-
if (kind === "completed" || kind === "") return void 0;
|
|
887
|
+
if (kind === "" || kind === "completed" || kind === "aborted") return void 0;
|
|
539
888
|
return { title: `turn ended abnormally: ${kind}`, messageKey: "f.turn.error", params: { kind } };
|
|
540
889
|
}
|
|
541
890
|
}
|
|
542
891
|
];
|
|
543
892
|
var SEVERITY_ORDER = ["critical", "serious", "warning", "info"];
|
|
893
|
+
var CONFIG_RULES = /* @__PURE__ */ new Set(["sandbox-change", "permission-change"]);
|
|
544
894
|
function auditSession(session, rules = DEFAULT_RULES) {
|
|
895
|
+
const lastConfig = /* @__PURE__ */ new Map();
|
|
896
|
+
const changedConfig = /* @__PURE__ */ new Set();
|
|
897
|
+
let suppressed = 0;
|
|
898
|
+
for (const event of session.events) {
|
|
899
|
+
if (event.type !== "permission/preset" && event.type !== "sandbox/mode" && event.type !== "approval/policy") {
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
const value = JSON.stringify(event.data);
|
|
903
|
+
if (!lastConfig.has(event.type)) {
|
|
904
|
+
lastConfig.set(event.type, value);
|
|
905
|
+
suppressed++;
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
if (lastConfig.get(event.type) === value) {
|
|
909
|
+
suppressed++;
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
lastConfig.set(event.type, value);
|
|
913
|
+
changedConfig.add(event.seq);
|
|
914
|
+
}
|
|
545
915
|
const findings = [];
|
|
546
916
|
for (const event of session.events) {
|
|
547
917
|
for (const rule of rules) {
|
|
918
|
+
if (CONFIG_RULES.has(rule.id) && !changedConfig.has(event.seq)) continue;
|
|
548
919
|
const hit = rule.match(event);
|
|
549
920
|
if (hit === void 0) continue;
|
|
550
921
|
const severity = typeof rule.severity === "function" ? rule.severity(event) : rule.severity;
|
|
551
|
-
findings.push({
|
|
922
|
+
findings.push({
|
|
923
|
+
ruleId: rule.id,
|
|
924
|
+
severity,
|
|
925
|
+
category: hit.category ?? rule.category,
|
|
926
|
+
seq: event.seq,
|
|
927
|
+
time: event.time,
|
|
928
|
+
...hit
|
|
929
|
+
});
|
|
552
930
|
}
|
|
553
931
|
}
|
|
554
932
|
findings.sort(
|
|
@@ -556,7 +934,7 @@ function auditSession(session, rules = DEFAULT_RULES) {
|
|
|
556
934
|
);
|
|
557
935
|
const counts = { critical: 0, serious: 0, warning: 0, info: 0 };
|
|
558
936
|
for (const finding of findings) counts[finding.severity]++;
|
|
559
|
-
return { findings, counts };
|
|
937
|
+
return { findings, counts, scanned: session.events.length, rules: rules.length, suppressed };
|
|
560
938
|
}
|
|
561
939
|
|
|
562
940
|
// src/core/forktree.ts
|
|
@@ -654,54 +1032,139 @@ function stats(session, timeline, title) {
|
|
|
654
1032
|
function eventFingerprint(event) {
|
|
655
1033
|
return `${event.type}\0${JSON.stringify(event.data)}`;
|
|
656
1034
|
}
|
|
657
|
-
function
|
|
1035
|
+
function relatedBy(a, b, parentOf) {
|
|
1036
|
+
if (a.header.id === b.header.id) return "same";
|
|
1037
|
+
if (b.header.parentSession === a.header.id) return "a-parent-of-b";
|
|
1038
|
+
if (a.header.parentSession === b.header.id) return "b-parent-of-a";
|
|
1039
|
+
const ancestorsOfB = /* @__PURE__ */ new Set();
|
|
1040
|
+
let cursor = b.header.parentSession;
|
|
1041
|
+
while (cursor !== void 0 && !ancestorsOfB.has(cursor)) {
|
|
1042
|
+
ancestorsOfB.add(cursor);
|
|
1043
|
+
cursor = parentOf(cursor);
|
|
1044
|
+
}
|
|
1045
|
+
if (ancestorsOfB.has(a.header.id)) return "a-parent-of-b";
|
|
1046
|
+
const ancestorsOfA = /* @__PURE__ */ new Set();
|
|
1047
|
+
cursor = a.header.parentSession;
|
|
1048
|
+
while (cursor !== void 0 && !ancestorsOfA.has(cursor)) {
|
|
1049
|
+
ancestorsOfA.add(cursor);
|
|
1050
|
+
cursor = parentOf(cursor);
|
|
1051
|
+
}
|
|
1052
|
+
if (ancestorsOfA.has(b.header.id)) return "b-parent-of-a";
|
|
1053
|
+
if (a.header.parentSession !== void 0 && a.header.parentSession === b.header.parentSession) {
|
|
1054
|
+
return "siblings";
|
|
1055
|
+
}
|
|
1056
|
+
return "none";
|
|
1057
|
+
}
|
|
1058
|
+
function compareSessions(a, b, titles, parentOf = () => void 0) {
|
|
658
1059
|
const timelineA = buildTimeline(a);
|
|
659
1060
|
const timelineB = buildTimeline(b);
|
|
660
1061
|
const statsA = stats(a, timelineA, titles?.a);
|
|
661
1062
|
const statsB = stats(b, timelineB, titles?.b);
|
|
662
1063
|
const toolNames = [.../* @__PURE__ */ new Set([...Object.keys(statsA.toolMix), ...Object.keys(statsB.toolMix)])].sort();
|
|
663
|
-
const
|
|
1064
|
+
const relation = relatedBy(a, b, parentOf);
|
|
1065
|
+
const related = relation !== "none";
|
|
664
1066
|
let divergenceSeq;
|
|
1067
|
+
let divergenceA;
|
|
1068
|
+
let divergenceB;
|
|
665
1069
|
const shared = Math.min(a.events.length, b.events.length);
|
|
666
1070
|
for (let i = 0; i < shared; i++) {
|
|
667
1071
|
if (a.events[i].type === "session/end-seed" || b.events[i].type === "session/end-seed") {
|
|
668
1072
|
if (a.events[i].type !== b.events[i].type) {
|
|
669
1073
|
divergenceSeq = Math.min(a.events[i].seq, b.events[i].seq);
|
|
1074
|
+
divergenceA = a.events[i];
|
|
1075
|
+
divergenceB = b.events[i];
|
|
670
1076
|
break;
|
|
671
1077
|
}
|
|
672
1078
|
continue;
|
|
673
1079
|
}
|
|
674
1080
|
if (eventFingerprint(a.events[i]) !== eventFingerprint(b.events[i])) {
|
|
675
1081
|
divergenceSeq = a.events[i].seq;
|
|
1082
|
+
divergenceA = a.events[i];
|
|
1083
|
+
divergenceB = b.events[i];
|
|
676
1084
|
break;
|
|
677
1085
|
}
|
|
678
1086
|
}
|
|
679
1087
|
if (divergenceSeq === void 0 && a.events.length !== b.events.length) {
|
|
680
1088
|
divergenceSeq = shared > 0 ? (a.events[shared - 1]?.seq ?? 0) + 1 : 0;
|
|
1089
|
+
divergenceA = a.events[shared];
|
|
1090
|
+
divergenceB = b.events[shared];
|
|
681
1091
|
}
|
|
682
|
-
return { a: statsA, b: statsB, toolNames, divergenceSeq, related };
|
|
1092
|
+
return { a: statsA, b: statsB, toolNames, divergenceSeq, divergenceA, divergenceB, related, relation };
|
|
683
1093
|
}
|
|
684
1094
|
|
|
685
1095
|
// src/core/cost.ts
|
|
686
1096
|
var DEFAULT_PRICING = [
|
|
687
|
-
{
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
1097
|
+
{
|
|
1098
|
+
match: /deepseek.*pro/iu,
|
|
1099
|
+
rate: { input: 1.1, cacheRead: 0.27, output: 4.4 },
|
|
1100
|
+
label: "DeepSeek Pro",
|
|
1101
|
+
updatedAt: "2026-08-18",
|
|
1102
|
+
source: "provider list price (estimate)"
|
|
1103
|
+
},
|
|
1104
|
+
{
|
|
1105
|
+
match: /deepseek/iu,
|
|
1106
|
+
rate: { input: 0.27, cacheRead: 0.07, output: 1.1 },
|
|
1107
|
+
label: "DeepSeek",
|
|
1108
|
+
updatedAt: "2026-08-18",
|
|
1109
|
+
source: "provider list price (estimate)"
|
|
1110
|
+
},
|
|
1111
|
+
{
|
|
1112
|
+
match: /claude|anthropic/iu,
|
|
1113
|
+
rate: { input: 3, cacheRead: 0.3, cacheWrite: 3.75, output: 15 },
|
|
1114
|
+
label: "Claude",
|
|
1115
|
+
updatedAt: "2026-08-18",
|
|
1116
|
+
source: "provider list price (estimate)"
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
match: /gpt|^o\d|openai|codex/iu,
|
|
1120
|
+
rate: { input: 2.5, cacheRead: 1.25, output: 10 },
|
|
1121
|
+
label: "OpenAI",
|
|
1122
|
+
updatedAt: "2026-08-18",
|
|
1123
|
+
source: "provider list price (estimate)"
|
|
1124
|
+
},
|
|
1125
|
+
{
|
|
1126
|
+
match: /gemini/iu,
|
|
1127
|
+
rate: { input: 1.25, cacheRead: 0.31, output: 10 },
|
|
1128
|
+
label: "Gemini",
|
|
1129
|
+
updatedAt: "2026-08-18",
|
|
1130
|
+
source: "provider list price (estimate)"
|
|
1131
|
+
}
|
|
692
1132
|
];
|
|
1133
|
+
function costOf(totals, rate) {
|
|
1134
|
+
return (totals.input * rate.input + totals.cacheRead * rate.cacheRead + totals.cacheWrite * (rate.cacheWrite ?? rate.input) + totals.output * rate.output) / 1e6;
|
|
1135
|
+
}
|
|
693
1136
|
function estimateCost(totals, models, pricing = DEFAULT_PRICING) {
|
|
694
1137
|
const entry = pricing.find((p) => models.some((m) => p.match.test(m)));
|
|
695
1138
|
if (entry === void 0) return void 0;
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
1139
|
+
return { usd: costOf(totals, entry.rate), label: entry.label };
|
|
1140
|
+
}
|
|
1141
|
+
function estimateCostDetailed(modelTotals, pricing = DEFAULT_PRICING) {
|
|
1142
|
+
if (modelTotals === void 0 || modelTotals === null) return void 0;
|
|
1143
|
+
const parts = [];
|
|
1144
|
+
for (const [model, totals] of Object.entries(modelTotals)) {
|
|
1145
|
+
const entry = pricing.find((p) => p.match.test(model));
|
|
1146
|
+
if (entry === void 0) continue;
|
|
1147
|
+
parts.push({ model, label: entry.label, usd: costOf(totals, entry.rate), totals });
|
|
1148
|
+
}
|
|
1149
|
+
if (parts.length === 0) return void 0;
|
|
1150
|
+
const usd = parts.reduce((sum, part) => sum + part.usd, 0);
|
|
1151
|
+
const labels = [...new Set(parts.map((part) => part.label))];
|
|
1152
|
+
return { usd, label: labels.length === 1 ? labels[0] : `Mixed (${labels.length} models)`, parts };
|
|
699
1153
|
}
|
|
700
1154
|
function formatUsd(usd) {
|
|
701
1155
|
if (usd >= 1) return `$${usd.toFixed(2)}`;
|
|
702
1156
|
if (usd >= 0.01) return `$${usd.toFixed(3)}`;
|
|
703
1157
|
return `$${usd.toFixed(4)}`;
|
|
704
1158
|
}
|
|
1159
|
+
function formatCny(usd, rate = 7.2) {
|
|
1160
|
+
const cny = usd * rate;
|
|
1161
|
+
if (cny >= 1) return `\xA5${cny.toFixed(2)}`;
|
|
1162
|
+
if (cny >= 0.01) return `\xA5${cny.toFixed(3)}`;
|
|
1163
|
+
return `\xA5${cny.toFixed(4)}`;
|
|
1164
|
+
}
|
|
1165
|
+
function formatMoney(usd, currency, rate = 7.2) {
|
|
1166
|
+
return currency === "CNY" ? formatCny(usd, rate) : formatUsd(usd);
|
|
1167
|
+
}
|
|
705
1168
|
export {
|
|
706
1169
|
DEFAULT_PRICING,
|
|
707
1170
|
DEFAULT_RULES,
|
|
@@ -709,12 +1172,20 @@ export {
|
|
|
709
1172
|
buildForkForest,
|
|
710
1173
|
buildTimeline,
|
|
711
1174
|
compareSessions,
|
|
1175
|
+
containsSecret,
|
|
712
1176
|
decodeStorageRecord,
|
|
713
1177
|
estimateCost,
|
|
1178
|
+
estimateCostDetailed,
|
|
714
1179
|
expandChunkRow,
|
|
1180
|
+
formatCny,
|
|
1181
|
+
formatMoney,
|
|
715
1182
|
formatUsd,
|
|
716
1183
|
fromInspection,
|
|
717
1184
|
isChunkRow,
|
|
718
1185
|
lineageOf,
|
|
719
|
-
parseSessionJsonl
|
|
1186
|
+
parseSessionJsonl,
|
|
1187
|
+
redactSession,
|
|
1188
|
+
redactText,
|
|
1189
|
+
redactValue,
|
|
1190
|
+
summarizeToolArguments
|
|
720
1191
|
};
|