@mingozhou/dsh-replay 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +164 -0
  3. package/README.zh.md +164 -0
  4. package/cordis.patch.yml +6 -0
  5. package/lib/client.js +2478 -0
  6. package/lib/core.js +720 -0
  7. package/lib/index.js +111 -0
  8. package/lib/types/demo/main.d.ts +1 -0
  9. package/lib/types/demo/samples.d.ts +21 -0
  10. package/lib/types/src/client/api.d.ts +43 -0
  11. package/lib/types/src/client/components/AuditView.d.ts +8 -0
  12. package/lib/types/src/client/components/CompareView.d.ts +9 -0
  13. package/lib/types/src/client/components/ForkTreeView.d.ts +7 -0
  14. package/lib/types/src/client/components/ItemDetail.d.ts +6 -0
  15. package/lib/types/src/client/components/OverviewView.d.ts +8 -0
  16. package/lib/types/src/client/components/ReplayApp.d.ts +12 -0
  17. package/lib/types/src/client/components/ReplayModal.d.ts +8 -0
  18. package/lib/types/src/client/components/SessionPicker.d.ts +6 -0
  19. package/lib/types/src/client/components/TimelineView.d.ts +6 -0
  20. package/lib/types/src/client/export.d.ts +13 -0
  21. package/lib/types/src/client/i18n.d.ts +24 -0
  22. package/lib/types/src/client/index.d.ts +12 -0
  23. package/lib/types/src/client/mascot-image.d.ts +1 -0
  24. package/lib/types/src/client/mascot.d.ts +17 -0
  25. package/lib/types/src/client/viewer-entry.d.ts +1 -0
  26. package/lib/types/src/core/audit.d.ts +39 -0
  27. package/lib/types/src/core/compare.d.ts +31 -0
  28. package/lib/types/src/core/cost.d.ts +30 -0
  29. package/lib/types/src/core/decode.d.ts +25 -0
  30. package/lib/types/src/core/forktree.d.ts +33 -0
  31. package/lib/types/src/core/index.d.ts +7 -0
  32. package/lib/types/src/core/timeline.d.ts +96 -0
  33. package/lib/types/src/core/types.d.ts +88 -0
  34. package/lib/types/src/index.d.ts +20 -0
  35. package/lib/viewer.js +812 -0
  36. package/package.json +107 -0
  37. package/scripts/build.mjs +131 -0
  38. package/src/client/api.ts +119 -0
  39. package/src/client/components/AuditView.tsx +57 -0
  40. package/src/client/components/CompareView.tsx +186 -0
  41. package/src/client/components/ForkTreeView.tsx +123 -0
  42. package/src/client/components/ItemDetail.tsx +117 -0
  43. package/src/client/components/OverviewView.tsx +258 -0
  44. package/src/client/components/ReplayApp.tsx +205 -0
  45. package/src/client/components/ReplayModal.tsx +53 -0
  46. package/src/client/components/SessionPicker.tsx +60 -0
  47. package/src/client/components/TimelineView.tsx +296 -0
  48. package/src/client/export.ts +43 -0
  49. package/src/client/i18n.ts +360 -0
  50. package/src/client/index.ts +146 -0
  51. package/src/client/mascot-image.ts +1 -0
  52. package/src/client/mascot.tsx +192 -0
  53. package/src/client/styles.css +719 -0
  54. package/src/client/viewer-entry.tsx +66 -0
  55. package/src/core/audit.ts +227 -0
  56. package/src/core/compare.ts +0 -0
  57. package/src/core/cost.ts +61 -0
  58. package/src/core/decode.ts +125 -0
  59. package/src/core/forktree.ts +104 -0
  60. package/src/core/index.ts +7 -0
  61. package/src/core/timeline.ts +396 -0
  62. package/src/core/types.ts +90 -0
  63. package/src/index.ts +157 -0
  64. package/tsconfig.json +18 -0
  65. package/types.d.ts +15 -0
package/lib/core.js ADDED
@@ -0,0 +1,720 @@
1
+ // src/core/decode.ts
2
+ var CHUNK_ROW_TYPES = /* @__PURE__ */ new Set(["text-chunks", "reasoning-chunks", "tool-call-chunks"]);
3
+ function isChunkRow(record) {
4
+ return CHUNK_ROW_TYPES.has(record.type);
5
+ }
6
+ function expandChunkRow(row) {
7
+ const { seq0, time0, data } = row;
8
+ const { turn, step, index, dt } = data;
9
+ const events = [];
10
+ const count = (row.type === "tool-call-chunks" ? data.args?.length : data.texts?.length) ?? 0;
11
+ let time = time0;
12
+ for (let k = 0; k < count; k++) {
13
+ if (k > 0) time += dt[k - 1] ?? 0;
14
+ let chunk;
15
+ if (row.type === "text-chunks") {
16
+ chunk = { type: "text-delta", index, text: data.texts[k] };
17
+ } else if (row.type === "reasoning-chunks") {
18
+ chunk = { type: "reasoning-delta", index, text: data.texts[k] };
19
+ } else {
20
+ chunk = {
21
+ type: "tool-call-delta",
22
+ index,
23
+ id: data.id,
24
+ ...k === 0 && data.name !== void 0 ? { name: data.name } : {},
25
+ args: data.args[k]
26
+ };
27
+ }
28
+ events.push({
29
+ type: "assistant/chunk",
30
+ seq: seq0 + k,
31
+ time,
32
+ data: { turn, step, chunk }
33
+ });
34
+ }
35
+ return events;
36
+ }
37
+ function decodeStorageRecord(record) {
38
+ if (isChunkRow(record)) return expandChunkRow(record);
39
+ return [record];
40
+ }
41
+ function isHeaderLine(value) {
42
+ return value["type"] === "session" && typeof value["id"] === "string";
43
+ }
44
+ function parseSessionJsonl(text) {
45
+ const lines = text.split("\n");
46
+ const malformed = [];
47
+ let header;
48
+ const events = [];
49
+ const endsWithNewline = text.endsWith("\n");
50
+ for (let i = 0; i < lines.length; i++) {
51
+ const raw = lines[i];
52
+ if (raw.trim() === "") continue;
53
+ const isLastLine = i === lines.length - 1;
54
+ let value;
55
+ try {
56
+ value = JSON.parse(raw);
57
+ } catch (error) {
58
+ if (isLastLine && !endsWithNewline) {
59
+ malformed.push({ line: i + 1, raw, error: "torn tail (uncommitted final record)" });
60
+ } else {
61
+ malformed.push({ line: i + 1, raw, error: String(error) });
62
+ }
63
+ continue;
64
+ }
65
+ if (header === void 0) {
66
+ if (!isHeaderLine(value)) {
67
+ malformed.push({ line: i + 1, raw, error: "expected session header line" });
68
+ continue;
69
+ }
70
+ header = value;
71
+ continue;
72
+ }
73
+ try {
74
+ events.push(...decodeStorageRecord(value));
75
+ } catch (error) {
76
+ malformed.push({ line: i + 1, raw, error: String(error) });
77
+ }
78
+ }
79
+ if (header === void 0) {
80
+ throw new Error("session log has no header line");
81
+ }
82
+ events.sort((a, b) => a.seq - b.seq);
83
+ return { header, events, malformed };
84
+ }
85
+ function fromInspection(header, records) {
86
+ const events = records.flatMap(decodeStorageRecord).sort((a, b) => a.seq - b.seq);
87
+ return { header, events, malformed: [] };
88
+ }
89
+
90
+ // src/core/timeline.ts
91
+ function contentText(message) {
92
+ if (!message?.content) return "";
93
+ return message.content.filter((b) => typeof b.text === "string").map((b) => b.text).join("");
94
+ }
95
+ function truncate(text, max = 96) {
96
+ const oneLine = text.replaceAll(/\s+/gu, " ").trim();
97
+ return oneLine.length > max ? `${oneLine.slice(0, max - 1)}\u2026` : oneLine;
98
+ }
99
+ function tryPretty(raw) {
100
+ try {
101
+ return JSON.stringify(JSON.parse(raw), null, 2);
102
+ } catch {
103
+ return void 0;
104
+ }
105
+ }
106
+ function get(obj, key) {
107
+ return typeof obj === "object" && obj !== null ? obj[key] : void 0;
108
+ }
109
+ var HIDDEN_TYPES = /* @__PURE__ */ new Set(["assistant/chunk", "step/start", "step/end", "turn/start"]);
110
+ function metaLabel(event) {
111
+ const d = event.data;
112
+ switch (event.type) {
113
+ case "turn/end": {
114
+ const reason = get(get(d, "reason"), "kind");
115
+ return reason === "completed" ? void 0 : `turn ended: ${String(reason)}`;
116
+ }
117
+ case "compaction/summary":
118
+ return "context compacted (summary)";
119
+ case "compaction/prune":
120
+ return "context pruned";
121
+ case "approval/asked":
122
+ return `approval requested: ${String(get(d, "toolName") ?? "")}`;
123
+ case "approval/decided": {
124
+ const outcome = get(d, "outcome") ?? get(d, "decision");
125
+ return `approval decided: ${typeof outcome === "string" ? outcome : JSON.stringify(outcome)}`;
126
+ }
127
+ case "sandbox/mode":
128
+ return `sandbox mode \u2192 ${JSON.stringify(d)}`;
129
+ case "permission/preset":
130
+ return `permission preset \u2192 ${JSON.stringify(d)}`;
131
+ case "plan/mode":
132
+ return `plan mode change`;
133
+ case "subagent/descriptor":
134
+ return `subagent spawned (${String(get(d, "mode") ?? "")})`;
135
+ case "session/end-seed":
136
+ return "seed boundary (fork/resume point)";
137
+ case "llm/retry":
138
+ case "llm/retry-started":
139
+ return "model request retried";
140
+ case "command/run":
141
+ return `command: ${truncate(JSON.stringify(d), 60)}`;
142
+ case "goal/change":
143
+ return "goal changed";
144
+ case "session/title":
145
+ return void 0;
146
+ default:
147
+ return void 0;
148
+ }
149
+ }
150
+ function shadowedSeqs(events) {
151
+ const surfaceOrder = [];
152
+ const shadowed = /* @__PURE__ */ new Set();
153
+ for (const event of events) {
154
+ const op = event.surfaceOp;
155
+ if (op === void 0) continue;
156
+ if (op === "append") {
157
+ surfaceOrder.push(event.seq);
158
+ } else if (typeof op === "object" && op.op === "replace") {
159
+ const removed = surfaceOrder.splice(op.start, op.end - op.start + 1, event.seq);
160
+ for (const seq of removed) shadowed.add(seq);
161
+ }
162
+ }
163
+ return shadowed;
164
+ }
165
+ function buildTimeline(session) {
166
+ const { events } = session;
167
+ const turns = /* @__PURE__ */ new Map();
168
+ const items = [];
169
+ const usageSamples = [];
170
+ const toolCallsBySeq = /* @__PURE__ */ new Map();
171
+ const toolCalls = [];
172
+ const usageByStep = /* @__PURE__ */ new Map();
173
+ const models = /* @__PURE__ */ new Set();
174
+ const shadowed = shadowedSeqs(events);
175
+ let seedBoundarySeq;
176
+ let activeTurn;
177
+ let sessionTitle;
178
+ const ensureTurn = (turn, event) => {
179
+ let model = turns.get(turn);
180
+ if (model === void 0) {
181
+ model = { turn, startSeq: event.seq, startTime: event.time, steps: [], items: [] };
182
+ turns.set(turn, model);
183
+ }
184
+ return model;
185
+ };
186
+ const ensureStep = (turnModel, step, event) => {
187
+ let model = turnModel.steps.find((s) => s.step === step);
188
+ if (model === void 0) {
189
+ model = { turn: turnModel.turn, step, startSeq: event.seq, startTime: event.time, items: [] };
190
+ turnModel.steps.push(model);
191
+ }
192
+ return model;
193
+ };
194
+ const push = (item) => {
195
+ if (shadowed.has(item.seq)) item.shadowed = true;
196
+ items.push(item);
197
+ if (item.turn !== void 0) {
198
+ const turnModel = ensureTurn(item.turn, item.event);
199
+ turnModel.items.push(item);
200
+ if (item.step !== void 0) ensureStep(turnModel, item.step, item.event).items.push(item);
201
+ }
202
+ };
203
+ for (const event of events) {
204
+ const d = event.data;
205
+ const turn = get(d, "turn");
206
+ const step = get(d, "step");
207
+ switch (event.type) {
208
+ case "turn/start":
209
+ activeTurn = turn;
210
+ ensureTurn(activeTurn, event);
211
+ break;
212
+ case "turn/end": {
213
+ const model = ensureTurn(turn, event);
214
+ model.endSeq = event.seq;
215
+ model.endTime = event.time;
216
+ model.endReason = String(get(get(d, "reason"), "kind") ?? "completed");
217
+ activeTurn = void 0;
218
+ break;
219
+ }
220
+ case "step/start":
221
+ ensureStep(ensureTurn(turn, event), step, event);
222
+ break;
223
+ case "step/end": {
224
+ const stepModel = ensureStep(ensureTurn(turn, event), step, event);
225
+ stepModel.endSeq = event.seq;
226
+ stepModel.endTime = event.time;
227
+ break;
228
+ }
229
+ case "user/message": {
230
+ const message = d;
231
+ const text = contentText(message);
232
+ push({
233
+ kind: "user",
234
+ seq: event.seq,
235
+ time: event.time,
236
+ type: event.type,
237
+ turn: activeTurn,
238
+ label: truncate(text) || "(user message)",
239
+ text,
240
+ event
241
+ });
242
+ break;
243
+ }
244
+ case "assistant/message": {
245
+ const message = get(d, "message");
246
+ const usage = get(d, "usage");
247
+ const text = contentText(message);
248
+ const source = message?.source;
249
+ if (typeof source?.["model"] === "string") models.add(source["model"]);
250
+ if (usage) {
251
+ usageByStep.set(`${String(turn)}/${String(step)}`, usage);
252
+ }
253
+ push({
254
+ kind: "assistant",
255
+ seq: event.seq,
256
+ time: event.time,
257
+ turn,
258
+ step,
259
+ type: event.type,
260
+ label: truncate(text) || "(assistant message)",
261
+ text,
262
+ usage,
263
+ event
264
+ });
265
+ break;
266
+ }
267
+ case "assistant/chunk": {
268
+ const chunk = get(d, "chunk");
269
+ if (get(chunk, "type") === "usage") {
270
+ const usage = get(chunk, "usage");
271
+ usageByStep.set(`${String(turn)}/${String(step)}`, usage);
272
+ }
273
+ break;
274
+ }
275
+ case "tool/call": {
276
+ const info = {
277
+ callId: String(get(d, "callId") ?? ""),
278
+ name: String(get(d, "name") ?? ""),
279
+ argumentsRaw: String(get(d, "arguments") ?? ""),
280
+ argumentsPretty: tryPretty(String(get(d, "arguments") ?? "")),
281
+ callSeq: event.seq,
282
+ callTime: event.time
283
+ };
284
+ toolCallsBySeq.set(info.callId, info);
285
+ toolCalls.push(info);
286
+ push({
287
+ kind: "tool",
288
+ seq: event.seq,
289
+ time: event.time,
290
+ turn,
291
+ step,
292
+ type: event.type,
293
+ label: `${info.name}(${truncate(info.argumentsRaw, 48)})`,
294
+ toolCall: info,
295
+ event
296
+ });
297
+ break;
298
+ }
299
+ case "tool/result": {
300
+ const message = get(d, "message");
301
+ const source = message?.source;
302
+ const callId = String(source?.["callId"] ?? "");
303
+ const info = toolCallsBySeq.get(callId);
304
+ const block = message?.content?.[0];
305
+ const resultText = typeof get(block, "text") === "string" ? get(block, "text") : contentText({ content: get(block, "content") });
306
+ const error = get(d, "error");
307
+ if (info) {
308
+ info.resultSeq = event.seq;
309
+ info.resultTime = event.time;
310
+ info.durationMs = event.time - info.callTime;
311
+ info.resultText = resultText;
312
+ info.error = error;
313
+ }
314
+ break;
315
+ }
316
+ case "session/end-seed":
317
+ seedBoundarySeq = event.seq;
318
+ push({
319
+ kind: "meta",
320
+ seq: event.seq,
321
+ time: event.time,
322
+ type: event.type,
323
+ label: "seed boundary (fork/resume point)",
324
+ event
325
+ });
326
+ break;
327
+ case "request/context": {
328
+ const model = get(d, "model");
329
+ if (typeof model === "string") models.add(model);
330
+ break;
331
+ }
332
+ case "session/title": {
333
+ const title = get(d, "title");
334
+ if (typeof title === "string" && title !== "") sessionTitle = title;
335
+ break;
336
+ }
337
+ default: {
338
+ if (HIDDEN_TYPES.has(event.type)) break;
339
+ const label = metaLabel(event);
340
+ if (label !== void 0) {
341
+ push({
342
+ kind: "meta",
343
+ seq: event.seq,
344
+ time: event.time,
345
+ turn: typeof turn === "number" ? turn : void 0,
346
+ step: typeof step === "number" ? step : void 0,
347
+ type: event.type,
348
+ label,
349
+ event
350
+ });
351
+ }
352
+ }
353
+ }
354
+ }
355
+ const totals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, billedInput: 0 };
356
+ for (const [key, usage] of usageByStep) {
357
+ const [turnStr, stepStr] = key.split("/");
358
+ totals.input += usage.inputTokens;
359
+ totals.output += usage.outputTokens;
360
+ totals.cacheRead += usage.cacheReadTokens ?? 0;
361
+ totals.cacheWrite += usage.cacheWriteTokens ?? 0;
362
+ totals.reasoning += usage.reasoningTokens ?? 0;
363
+ const turnModel = turns.get(Number(turnStr));
364
+ const stepModel = turnModel?.steps.find((s) => s.step === Number(stepStr));
365
+ if (stepModel) stepModel.usage = usage;
366
+ const anchor = stepModel?.items.find((i) => i.kind === "assistant");
367
+ usageSamples.push({
368
+ seq: anchor?.seq ?? stepModel?.startSeq ?? 0,
369
+ turn: Number(turnStr),
370
+ step: Number(stepStr),
371
+ usage
372
+ });
373
+ }
374
+ usageSamples.sort((a, b) => a.seq - b.seq);
375
+ totals.billedInput = totals.input + totals.cacheRead + totals.cacheWrite;
376
+ const orderedTurns = [...turns.values()].sort((a, b) => a.turn - b.turn);
377
+ for (const t of orderedTurns) t.steps.sort((a, b) => a.step - b.step);
378
+ const startTime = events[0]?.time ?? session.header.createdAt;
379
+ const endTime = events.at(-1)?.time ?? startTime;
380
+ return {
381
+ turns: orderedTurns,
382
+ items,
383
+ totals,
384
+ usageSamples,
385
+ toolCalls,
386
+ durationMs: endTime - startTime,
387
+ startTime,
388
+ endTime,
389
+ eventCount: events.length,
390
+ seedBoundarySeq,
391
+ models: [...models],
392
+ title: sessionTitle
393
+ };
394
+ }
395
+
396
+ // src/core/audit.ts
397
+ function get2(obj, key) {
398
+ return typeof obj === "object" && obj !== null ? obj[key] : void 0;
399
+ }
400
+ function str(value) {
401
+ return typeof value === "string" ? value : JSON.stringify(value) ?? "";
402
+ }
403
+ var SHELL_TOOLS = /^(bash|shell|exec|run_command|terminal|subprocess)/iu;
404
+ var WRITE_TOOLS = /^(write|edit|create_file|str_replace|apply_patch|notebook_edit|fs_write|mv|rm|delete)/iu;
405
+ var NETWORK_TOOLS = /^(fetch|web_fetch|http|curl|download|web_search|browser)/iu;
406
+ var DANGEROUS_SHELL = [
407
+ { re: /\brm\s+(-[a-z]*[rf][a-z]*\s+)+/iu, why: "recursive/forced delete", key: "rm-rf" },
408
+ { re: /\bsudo\b/u, why: "privilege escalation", key: "sudo" },
409
+ { re: /\bchmod\s+([0-7]{3,4}|\+x)/u, why: "permission change", key: "chmod" },
410
+ { re: /\b(curl|wget)\b[^|;&]*\|\s*(ba)?sh\b/iu, why: "pipe remote script to shell", key: "curl-pipe" },
411
+ { re: /\bgit\s+push\s+.*--force/iu, why: "force push", key: "force-push" },
412
+ { re: /\b(dd|mkfs|fdisk)\b/u, why: "raw disk operation", key: "disk" },
413
+ { re: /(\bssh\b|\bscp\b|\brsync\b.*@)/u, why: "remote host access", key: "remote" },
414
+ { re: /\b(export|echo)\b.*\b(API_?KEY|TOKEN|SECRET|PASSWORD)\b/iu, why: "credential material in command", key: "creds" }
415
+ ];
416
+ var SENSITIVE_PATH = /(\.env\b|\.ssh\/|id_rsa|\.aws\/|credentials|\.npmrc|\.netrc|secrets?\.|\.pem\b)/iu;
417
+ var DEFAULT_RULES = [
418
+ {
419
+ id: "shell-exec",
420
+ severity: (event) => {
421
+ const args = str(get2(event.data, "arguments"));
422
+ return DANGEROUS_SHELL.some((p) => p.re.test(args)) ? "critical" : "warning";
423
+ },
424
+ match: (event) => {
425
+ if (event.type !== "tool/call") return void 0;
426
+ const name = str(get2(event.data, "name"));
427
+ if (!SHELL_TOOLS.test(name)) return void 0;
428
+ const args = str(get2(event.data, "arguments"));
429
+ const hit = DANGEROUS_SHELL.find((p) => p.re.test(args));
430
+ return {
431
+ title: hit ? `shell: ${hit.why}` : `shell command via ${name}`,
432
+ detail: args.slice(0, 400),
433
+ messageKey: hit ? "f.shell.dangerous" : "f.shell.plain",
434
+ params: hit ? { whyKey: hit.key, why: hit.why } : { name }
435
+ };
436
+ }
437
+ },
438
+ {
439
+ id: "file-write",
440
+ severity: (event) => SENSITIVE_PATH.test(str(get2(event.data, "arguments"))) ? "critical" : "info",
441
+ match: (event) => {
442
+ if (event.type !== "tool/call") return void 0;
443
+ const name = str(get2(event.data, "name"));
444
+ if (!WRITE_TOOLS.test(name)) return void 0;
445
+ const args = str(get2(event.data, "arguments"));
446
+ const sensitive = SENSITIVE_PATH.test(args);
447
+ return {
448
+ title: sensitive ? `file write touches sensitive path` : `file mutation via ${name}`,
449
+ detail: args.slice(0, 400),
450
+ messageKey: sensitive ? "f.file.sensitive" : "f.file.write",
451
+ params: { name }
452
+ };
453
+ }
454
+ },
455
+ {
456
+ id: "sensitive-read",
457
+ severity: "serious",
458
+ match: (event) => {
459
+ if (event.type !== "tool/call") return void 0;
460
+ const args = str(get2(event.data, "arguments"));
461
+ const name = str(get2(event.data, "name"));
462
+ if (WRITE_TOOLS.test(name) || SHELL_TOOLS.test(name)) return void 0;
463
+ if (!SENSITIVE_PATH.test(args)) return void 0;
464
+ return {
465
+ title: `access to sensitive path via ${name}`,
466
+ detail: args.slice(0, 400),
467
+ messageKey: "f.read.sensitive",
468
+ params: { name }
469
+ };
470
+ }
471
+ },
472
+ {
473
+ id: "network",
474
+ severity: "info",
475
+ match: (event) => {
476
+ if (event.type !== "tool/call") return void 0;
477
+ const name = str(get2(event.data, "name"));
478
+ if (!NETWORK_TOOLS.test(name)) return void 0;
479
+ return {
480
+ title: `network access via ${name}`,
481
+ detail: str(get2(event.data, "arguments")).slice(0, 400),
482
+ messageKey: "f.network",
483
+ params: { name }
484
+ };
485
+ }
486
+ },
487
+ {
488
+ id: "approval-denied",
489
+ severity: "warning",
490
+ match: (event) => {
491
+ if (event.type !== "approval/decided") return void 0;
492
+ const outcome = str(get2(event.data, "outcome") ?? get2(event.data, "decision"));
493
+ if (!/den|reject|refus/iu.test(outcome)) return void 0;
494
+ return { title: "user denied an approval", detail: outcome, messageKey: "f.approval.denied" };
495
+ }
496
+ },
497
+ {
498
+ id: "sandbox-change",
499
+ severity: "serious",
500
+ match: (event) => event.type === "sandbox/mode" ? { title: "sandbox mode changed", detail: JSON.stringify(event.data), messageKey: "f.sandbox.change" } : void 0
501
+ },
502
+ {
503
+ id: "permission-change",
504
+ severity: "serious",
505
+ match: (event) => event.type === "permission/preset" ? { title: "permission preset changed", detail: JSON.stringify(event.data), messageKey: "f.permission.change" } : void 0
506
+ },
507
+ {
508
+ id: "subagent",
509
+ severity: "info",
510
+ match: (event) => event.type === "subagent/descriptor" ? {
511
+ title: `subagent spawned (${str(get2(event.data, "mode"))})`,
512
+ detail: str(get2(event.data, "persona") ?? get2(event.data, "label") ?? ""),
513
+ messageKey: "f.subagent",
514
+ params: { mode: str(get2(event.data, "mode")) }
515
+ } : void 0
516
+ },
517
+ {
518
+ id: "tool-error",
519
+ severity: "warning",
520
+ match: (event) => {
521
+ if (event.type !== "tool/result") return void 0;
522
+ const error = get2(event.data, "error");
523
+ if (error === void 0) return void 0;
524
+ return {
525
+ title: `tool failed: ${str(get2(error, "name"))}`,
526
+ detail: str(get2(error, "code")),
527
+ messageKey: "f.tool.error",
528
+ params: { name: str(get2(error, "name")) }
529
+ };
530
+ }
531
+ },
532
+ {
533
+ id: "turn-error",
534
+ severity: "warning",
535
+ match: (event) => {
536
+ if (event.type !== "turn/end") return void 0;
537
+ const kind = str(get2(get2(event.data, "reason"), "kind"));
538
+ if (kind === "completed" || kind === "") return void 0;
539
+ return { title: `turn ended abnormally: ${kind}`, messageKey: "f.turn.error", params: { kind } };
540
+ }
541
+ }
542
+ ];
543
+ var SEVERITY_ORDER = ["critical", "serious", "warning", "info"];
544
+ function auditSession(session, rules = DEFAULT_RULES) {
545
+ const findings = [];
546
+ for (const event of session.events) {
547
+ for (const rule of rules) {
548
+ const hit = rule.match(event);
549
+ if (hit === void 0) continue;
550
+ const severity = typeof rule.severity === "function" ? rule.severity(event) : rule.severity;
551
+ findings.push({ ruleId: rule.id, severity, seq: event.seq, time: event.time, ...hit });
552
+ }
553
+ }
554
+ findings.sort(
555
+ (a, b) => SEVERITY_ORDER.indexOf(a.severity) - SEVERITY_ORDER.indexOf(b.severity) || a.seq - b.seq
556
+ );
557
+ const counts = { critical: 0, serious: 0, warning: 0, info: 0 };
558
+ for (const finding of findings) counts[finding.severity]++;
559
+ return { findings, counts };
560
+ }
561
+
562
+ // src/core/forktree.ts
563
+ function buildForkForest(entries) {
564
+ const nodes = /* @__PURE__ */ new Map();
565
+ for (const { header, title } of entries) {
566
+ nodes.set(header.id, {
567
+ id: header.id,
568
+ header,
569
+ title,
570
+ children: [],
571
+ seedLength: header.seedLength,
572
+ depth: 0,
573
+ row: 0
574
+ });
575
+ }
576
+ const roots = [];
577
+ for (const node of nodes.values()) {
578
+ const parentId = node.header.parentSession;
579
+ if (parentId === void 0) {
580
+ roots.push(node);
581
+ continue;
582
+ }
583
+ const parent = nodes.get(parentId);
584
+ if (parent === void 0) {
585
+ node.orphan = true;
586
+ roots.push(node);
587
+ } else {
588
+ parent.children.push(node);
589
+ }
590
+ }
591
+ const byCreated = (a, b) => a.header.createdAt - b.header.createdAt || a.id.localeCompare(b.id);
592
+ roots.sort(byCreated);
593
+ for (const node of nodes.values()) node.children.sort(byCreated);
594
+ let row = 0;
595
+ let maxDepth = 0;
596
+ const visited = /* @__PURE__ */ new Set();
597
+ const assign = (node, depth) => {
598
+ if (visited.has(node.id)) return;
599
+ visited.add(node.id);
600
+ node.depth = depth;
601
+ node.row = row++;
602
+ maxDepth = Math.max(maxDepth, depth);
603
+ for (const child of node.children) assign(child, depth + 1);
604
+ };
605
+ for (const root of roots) assign(root, 0);
606
+ for (const node of nodes.values()) {
607
+ if (!visited.has(node.id)) {
608
+ node.orphan = true;
609
+ roots.push(node);
610
+ assign(node, 0);
611
+ }
612
+ }
613
+ return { roots, nodes, rowCount: row, maxDepth };
614
+ }
615
+ function lineageOf(forest, id) {
616
+ const path = [];
617
+ let current = forest.nodes.get(id);
618
+ const guard = /* @__PURE__ */ new Set();
619
+ while (current !== void 0 && !guard.has(current.id)) {
620
+ guard.add(current.id);
621
+ path.unshift(current);
622
+ const parentId = current.header.parentSession;
623
+ current = parentId === void 0 ? void 0 : forest.nodes.get(parentId);
624
+ }
625
+ return path;
626
+ }
627
+
628
+ // src/core/compare.ts
629
+ function stats(session, timeline, title) {
630
+ const toolMix = {};
631
+ let toolErrors = 0;
632
+ for (const call of timeline.toolCalls) {
633
+ toolMix[call.name] = (toolMix[call.name] ?? 0) + 1;
634
+ if (call.error !== void 0) toolErrors++;
635
+ }
636
+ const steps = timeline.turns.reduce((n, t) => n + t.steps.length, 0);
637
+ return {
638
+ id: session.header.id,
639
+ title,
640
+ turns: timeline.turns.length,
641
+ steps,
642
+ events: timeline.eventCount,
643
+ toolCallCount: timeline.toolCalls.length,
644
+ toolErrors,
645
+ inputTokens: timeline.totals.input,
646
+ billedInputTokens: timeline.totals.billedInput,
647
+ outputTokens: timeline.totals.output,
648
+ cacheReadTokens: timeline.totals.cacheRead,
649
+ durationMs: timeline.durationMs,
650
+ models: timeline.models,
651
+ toolMix
652
+ };
653
+ }
654
+ function eventFingerprint(event) {
655
+ return `${event.type}\0${JSON.stringify(event.data)}`;
656
+ }
657
+ function compareSessions(a, b, titles) {
658
+ const timelineA = buildTimeline(a);
659
+ const timelineB = buildTimeline(b);
660
+ const statsA = stats(a, timelineA, titles?.a);
661
+ const statsB = stats(b, timelineB, titles?.b);
662
+ const toolNames = [.../* @__PURE__ */ new Set([...Object.keys(statsA.toolMix), ...Object.keys(statsB.toolMix)])].sort();
663
+ const related = a.header.parentSession === b.header.id || b.header.parentSession === a.header.id || a.header.parentSession !== void 0 && a.header.parentSession === b.header.parentSession;
664
+ let divergenceSeq;
665
+ const shared = Math.min(a.events.length, b.events.length);
666
+ for (let i = 0; i < shared; i++) {
667
+ if (a.events[i].type === "session/end-seed" || b.events[i].type === "session/end-seed") {
668
+ if (a.events[i].type !== b.events[i].type) {
669
+ divergenceSeq = Math.min(a.events[i].seq, b.events[i].seq);
670
+ break;
671
+ }
672
+ continue;
673
+ }
674
+ if (eventFingerprint(a.events[i]) !== eventFingerprint(b.events[i])) {
675
+ divergenceSeq = a.events[i].seq;
676
+ break;
677
+ }
678
+ }
679
+ if (divergenceSeq === void 0 && a.events.length !== b.events.length) {
680
+ divergenceSeq = shared > 0 ? (a.events[shared - 1]?.seq ?? 0) + 1 : 0;
681
+ }
682
+ return { a: statsA, b: statsB, toolNames, divergenceSeq, related };
683
+ }
684
+
685
+ // src/core/cost.ts
686
+ var DEFAULT_PRICING = [
687
+ { match: /deepseek.*pro/iu, rate: { input: 1.1, cacheRead: 0.27, output: 4.4 }, label: "DeepSeek Pro" },
688
+ { match: /deepseek/iu, rate: { input: 0.27, cacheRead: 0.07, output: 1.1 }, label: "DeepSeek" },
689
+ { match: /claude|anthropic/iu, rate: { input: 3, cacheRead: 0.3, cacheWrite: 3.75, output: 15 }, label: "Claude" },
690
+ { match: /gpt|^o\d|openai|codex/iu, rate: { input: 2.5, cacheRead: 1.25, output: 10 }, label: "OpenAI" },
691
+ { match: /gemini/iu, rate: { input: 1.25, cacheRead: 0.31, output: 10 }, label: "Gemini" }
692
+ ];
693
+ function estimateCost(totals, models, pricing = DEFAULT_PRICING) {
694
+ const entry = pricing.find((p) => models.some((m) => p.match.test(m)));
695
+ if (entry === void 0) return void 0;
696
+ const { rate } = entry;
697
+ const usd = (totals.input * rate.input + totals.cacheRead * rate.cacheRead + totals.cacheWrite * (rate.cacheWrite ?? rate.input) + totals.output * rate.output) / 1e6;
698
+ return { usd, label: entry.label };
699
+ }
700
+ function formatUsd(usd) {
701
+ if (usd >= 1) return `$${usd.toFixed(2)}`;
702
+ if (usd >= 0.01) return `$${usd.toFixed(3)}`;
703
+ return `$${usd.toFixed(4)}`;
704
+ }
705
+ export {
706
+ DEFAULT_PRICING,
707
+ DEFAULT_RULES,
708
+ auditSession,
709
+ buildForkForest,
710
+ buildTimeline,
711
+ compareSessions,
712
+ decodeStorageRecord,
713
+ estimateCost,
714
+ expandChunkRow,
715
+ formatUsd,
716
+ fromInspection,
717
+ isChunkRow,
718
+ lineageOf,
719
+ parseSessionJsonl
720
+ };