claude-inspect 0.4.1 → 0.5.1

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.de.md CHANGED
@@ -1,17 +1,33 @@
1
- # claude-inspect
1
+ # claude-inspect – Claude-Code-Dashboard & Monitor für Agenten, Subagenten und Verbrauch
2
2
 
3
- > English: [README.md](README.md) · Website: [brokoskokoli.github.io/claude_inspect](https://brokoskokoli.github.io/claude_inspect/) · Screenshots: [docs/screenshots](docs/screenshots)
3
+ [![npm](https://img.shields.io/npm/v/claude-inspect)](https://www.npmjs.com/package/claude-inspect)
4
+ [![npm downloads](https://img.shields.io/npm/dm/claude-inspect)](https://www.npmjs.com/package/claude-inspect)
5
+ [![CI](https://github.com/brokoskokoli/claude_inspect/actions/workflows/ci.yml/badge.svg)](https://github.com/brokoskokoli/claude_inspect/actions/workflows/ci.yml)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/brokoskokoli/claude_inspect/blob/main/LICENSE)
7
+ [![GitHub stars](https://img.shields.io/github/stars/brokoskokoli/claude_inspect?style=social)](https://github.com/brokoskokoli/claude_inspect)
8
+
9
+ **htop plus Flugschreiber für Claude Code. Keine Hooks, kein Proxy, ein `npx`.**
10
+
11
+ Lokales Live-Dashboard und Monitor für Claude-Code-Agenten: laufende Sessions, Subagenten,
12
+ per `claude -p` gestartete Worker, Tool-Aufrufe, Zeitleisten, CPU- und Token-Verbrauch
13
+ und ein Transcript-Viewer mit durchsuchbarer History.
14
+
15
+ ```bash
16
+ npx claude-inspect --open # Node.js ≥ 22 · macOS, Linux, Windows
17
+ ```
18
+
19
+ Liest **passiv** die Dateien unter `~/.claude`, ohne Hooks und ohne Konfigurationsänderung.
20
+ Das Einzige, was es schreibt, ist ein CPU-Protokoll im eigenen Datenverzeichnis (siehe *CPU*).
21
+
22
+ > English: [README.md](https://github.com/brokoskokoli/claude_inspect/blob/main/README.md) · Website: [brokoskokoli.github.io/claude_inspect](https://brokoskokoli.github.io/claude_inspect/) · Mitmachen: [CONTRIBUTING.md](https://github.com/brokoskokoli/claude_inspect/blob/main/CONTRIBUTING.md)
4
23
  >
5
24
  > Die Oberfläche ist englisch; hell, dunkel oder nach Systemeinstellung (Umschalter oben rechts).
6
25
 
7
- Lokales Live-Dashboard für Claude-Code-Agenten. Liest **passiv** die Dateien unter
8
- `~/.claude`, ohne Hooks und ohne Konfigurationsänderung. Das Einzige, was es schreibt, ist ein
9
- CPU-Protokoll im eigenen Datenverzeichnis (siehe *CPU*).
26
+ ![Live-Dashboard mit laufenden Agenten, Subagenten und einer gestarteten Worker-Session](https://raw.githubusercontent.com/brokoskokoli/claude_inspect/main/docs/screenshots/dashboard.jpg)
10
27
 
11
- ## Start
28
+ ## Installation
12
29
 
13
30
  ```bash
14
- npx claude-inspect --open # einmal ausprobieren
15
31
  npm install -g claude-inspect # dauerhaft installieren
16
32
  claude-inspect service install # Autostart → http://localhost:47717 (Lesezeichen)
17
33
  claude-inspect demo --open # ohne eigene Daten ausprobieren
@@ -47,6 +63,10 @@ Aus den Quellen: `git clone …`, `npm install`, `npm run build`, `npm start`
47
63
  - *Tasks*: Task-Liste der Session als Board
48
64
  - *CPU*: Auslastung des Claude-Prozesses samt aller Unterprozesse (Shells, Builds, Tests,
49
65
  MCP-Server …) als gestapelter Verlauf je Programmname, dazu die Prozesse mit der meisten CPU-Zeit
66
+ - *Cost*: geschätzte Kosten (API-Listenpreis) der Session samt aller Subagenten und per
67
+ `claude` gestarteten Sitzungen, je Agent und je Modell mit Tokens (Input, Output, Cache)
68
+ und als Zeitleiste mit den teuersten Momenten
69
+ - Beim Hochscrollen blendet sich eine kompakte Kopfzeile mit Sprung nach oben/unten ein
50
70
  - **CPU**: alle 3 s aus der Prozesstabelle gemessen (100 % = ein Kern), Verlaufslinie auf der
51
71
  Dashboard-Karte. Pro Session eine CSV-Datei im App-Datenverzeichnis (macOS
52
72
  `~/Library/Application Support/claude-inspect/cpu`, Linux `~/.local/share/claude-inspect/cpu`,
@@ -90,11 +110,11 @@ tests/ Fixtures je Claude-Code-Version, Snapshots, Tests
90
110
  scripts/ Fixture-Generator
91
111
  ```
92
112
 
93
- Neue Dateiformate: siehe [docs/FORMATS.md](docs/FORMATS.md). Gesamtkonzept: [KONZEPT.md](KONZEPT.md).
113
+ Neue Dateiformate: siehe [docs/FORMATS.md](https://github.com/brokoskokoli/claude_inspect/blob/main/docs/FORMATS.md). Gesamtkonzept: [KONZEPT.md](https://github.com/brokoskokoli/claude_inspect/blob/main/KONZEPT.md).
94
114
 
95
115
  ## Speicher & Leistung
96
116
 
97
- Nichts wird auf Platte geschrieben. Beim Start liest der Server nur Prozess-, Job- und
117
+ Außer dem CPU-Log im eigenen Datenverzeichnis wird nichts auf Platte geschrieben. Beim Start liest der Server nur Prozess-, Job- und
98
118
  Kopfdaten (unter 1 s); die komplette History (hier ~600 MB) wird im Hintergrund in
99
119
  2–3 s aggregiert und danach nur noch inkrementell nachgeführt. Vollständige Verläufe
100
120
  werden erst beim Öffnen geladen und mit einer Obergrenze im Speicher gehalten.
package/README.md CHANGED
@@ -1,23 +1,33 @@
1
- # claude-inspect – live dashboard & history viewer for Claude Code agents
1
+ # claude-inspect – Claude Code dashboard & monitor for agents, subagents and usage
2
2
 
3
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
4
3
  [![npm](https://img.shields.io/npm/v/claude-inspect)](https://www.npmjs.com/package/claude-inspect)
4
+ [![npm downloads](https://img.shields.io/npm/dm/claude-inspect)](https://www.npmjs.com/package/claude-inspect)
5
+ [![CI](https://github.com/brokoskokoli/claude_inspect/actions/workflows/ci.yml/badge.svg)](https://github.com/brokoskokoli/claude_inspect/actions/workflows/ci.yml)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/brokoskokoli/claude_inspect/blob/main/LICENSE)
7
+ [![GitHub stars](https://img.shields.io/github/stars/brokoskokoli/claude_inspect?style=social)](https://github.com/brokoskokoli/claude_inspect)
5
8
  ![Node.js ≥ 22](https://img.shields.io/badge/node-%E2%89%A5%2022-green)
6
- ![TypeScript](https://img.shields.io/badge/TypeScript-Svelte%205-3178c6)
7
- ![Dark mode](https://img.shields.io/badge/dark%20mode-%E2%9C%93-2b2a27)
8
9
  ![No hooks](https://img.shields.io/badge/Claude%20Code-no%20hooks%20needed-d97757)
9
10
 
10
- **See what your [Claude Code](https://claude.com/claude-code) agents are doing – right now and in the past.**
11
- claude-inspect is a local web UI that reads the files Claude Code already writes to `~/.claude`
11
+ **htop plus a flight recorder for [Claude Code](https://claude.com/claude-code). No hooks, no proxy, one `npx`.**
12
+
13
+ claude-inspect is a local Claude Code dashboard and monitor that reads the files Claude Code already writes to `~/.claude`
12
14
  and shows running sessions, subagents, `claude -p` worker sessions started by other agents,
13
- every tool call with its result, timelines, a call graph, token usage and a searchable history.
15
+ every tool call with its result, timelines, a call graph, CPU and token usage
16
+ and a transcript viewer with a searchable history.
17
+
18
+ ```bash
19
+ npx claude-inspect --open # Node.js ≥ 22 · macOS, Linux, Windows
20
+ ```
21
+
22
+ No Claude Code data at hand? `npx claude-inspect demo --open` starts it with a fictional data set.
23
+ Permanent install with autostart: see [Installation](#installation).
14
24
 
15
25
  It is **read-only and passive** towards `~/.claude`: no hooks, no settings changes, no proxy, no data leaves
16
26
  your machine. The only thing it writes is a CPU log in its own data directory (see *CPU usage*).
17
27
 
18
- > Website: [brokoskokoli.github.io/claude_inspect](https://brokoskokoli.github.io/claude_inspect/) · Deutsch: [README.de.md](README.de.md) · Design notes (German): [KONZEPT.md](KONZEPT.md)
28
+ > Website: [brokoskokoli.github.io/claude_inspect](https://brokoskokoli.github.io/claude_inspect/) · Deutsch: [README.de.md](https://github.com/brokoskokoli/claude_inspect/blob/main/README.de.md) · Design notes (German): [KONZEPT.md](https://github.com/brokoskokoli/claude_inspect/blob/main/KONZEPT.md) · [Contributing](https://github.com/brokoskokoli/claude_inspect/blob/main/CONTRIBUTING.md)
19
29
 
20
- ![Live dashboard with running agents, subagents and a spawned worker session](docs/screenshots/dashboard.jpg)
30
+ ![Live dashboard with running agents, subagents and a spawned worker session](https://raw.githubusercontent.com/brokoskokoli/claude_inspect/main/docs/screenshots/dashboard.jpg)
21
31
 
22
32
  ## Why
23
33
 
@@ -40,6 +50,7 @@ becomes hard to tell *who is doing what*. claude-inspect answers:
40
50
  | **Agent tree & call graph** | Main agent → subagents (nested) → `claude` sessions started via Bash, including fork/resume relations. Finished agents are greyed out. |
41
51
  | **Timeline** | One lane per agent, tool calls as bars, spawn links between lanes, idle gaps > 10 min collapsed, zoom. Click a bar to jump to the call. |
42
52
  | **Spawn linking** | Links sessions started by `claude -p …` to the calling tool use via the process tree (exact), `--session-id`, prompt text, `--agent`, time window and working directory – with a confidence label. |
53
+ | **Cost per agent tree** | Cost tab per session: API list-price estimate for the main agent plus all its subagents and spawned `claude` sessions, per agent and per model with input, output and cache tokens, plus a cost timeline with the most expensive moments. |
43
54
  | **Statistics** | API list-price estimate, tokens per model/day/project, cache share, activity heatmap, tool statistics with error rate and p95 duration. |
44
55
  | **CPU usage** | CPU of each Claude Code process plus all its subprocesses (shells, builds, test runners, MCP servers …), measured every 3 s from the process table. Sparkline on the dashboard card, a *CPU* tab per session with a stacked chart by process name and the processes that used the most CPU time. Recorded as one CSV per session in the app data directory (`~/Library/Application Support/claude-inspect/cpu` on macOS, `~/.local/share/claude-inspect/cpu` on Linux, `%LOCALAPPDATA%\claude-inspect\cpu` on Windows), so it stays available after the session ended; files older than 30 days are deleted. Only measured while claude-inspect runs; processes that live less than 3 s are missed. `--no-cpu-log` turns recording off. |
45
56
  | **Tool explorer & files** | Every tool call across all sessions, filterable; which files agents read, edited or wrote. |
@@ -49,29 +60,29 @@ becomes hard to tell *who is doing what*. claude-inspect answers:
49
60
  | **Dark mode** | Light, dark or system theme, remembered per browser. |
50
61
  | **Phone-friendly** | On narrow screens (phones, small iframes) the navigation collapses into a menu, tables turn into compact lists, the session view is single-column and long code, paths and JSON scroll inside their block. The server still listens on 127.0.0.1 only – to open it from a phone you need your own tunnel or reverse proxy that forwards to it with a local `Host` header (other hosts are rejected). |
51
62
 
52
- ![CPU tab: CPU usage of a worker session and its subprocesses (vitest, tsc, esbuild …) over time](docs/screenshots/cpu.jpg)
63
+ ![CPU tab: CPU usage of a worker session and its subprocesses (vitest, tsc, esbuild …) over time](https://raw.githubusercontent.com/brokoskokoli/claude_inspect/main/docs/screenshots/cpu.jpg)
53
64
 
54
65
  <table>
55
66
  <tr>
56
- <td><img src="docs/screenshots/timeline.jpg" alt="Timeline of an orchestrator with subagents and a spawned worker session"></td>
57
- <td><img src="docs/screenshots/call-graph.jpg" alt="Call graph: main agent, subagents and a claude -p worker session"></td>
67
+ <td><img src="https://raw.githubusercontent.com/brokoskokoli/claude_inspect/main/docs/screenshots/timeline.jpg" alt="Timeline of an orchestrator with subagents and a spawned worker session"></td>
68
+ <td><img src="https://raw.githubusercontent.com/brokoskokoli/claude_inspect/main/docs/screenshots/call-graph.jpg" alt="Call graph: main agent, subagents and a claude -p worker session"></td>
58
69
  </tr>
59
70
  <tr>
60
- <td><img src="docs/screenshots/transcript.jpg" alt="Transcript viewer with an expanded Edit tool call shown as diff"></td>
61
- <td><img src="docs/screenshots/statistics.jpg" alt="Statistics: API cost estimate per day and model, activity heatmap"></td>
71
+ <td><img src="https://raw.githubusercontent.com/brokoskokoli/claude_inspect/main/docs/screenshots/transcript.jpg" alt="Transcript viewer with an expanded Edit tool call shown as diff"></td>
72
+ <td><img src="https://raw.githubusercontent.com/brokoskokoli/claude_inspect/main/docs/screenshots/statistics.jpg" alt="Statistics: API cost estimate per day and model, activity heatmap"></td>
62
73
  </tr>
63
74
  </table>
64
75
 
65
76
  <table>
66
77
  <tr>
67
- <td><img src="docs/screenshots/tool-explorer.jpg" alt="Tool explorer listing tool calls across all sessions"></td>
68
- <td><img src="docs/screenshots/dashboard-dark.jpg" alt="Live dashboard in dark mode"></td>
78
+ <td><img src="https://raw.githubusercontent.com/brokoskokoli/claude_inspect/main/docs/screenshots/tool-explorer.jpg" alt="Tool explorer listing tool calls across all sessions"></td>
79
+ <td><img src="https://raw.githubusercontent.com/brokoskokoli/claude_inspect/main/docs/screenshots/dashboard-dark.jpg" alt="Live dashboard in dark mode"></td>
69
80
  </tr>
70
81
  </table>
71
82
 
72
83
  *All screenshots show the built-in demo data set (`claude-inspect demo`), not real sessions.*
73
84
 
74
- ## Quick start
85
+ ## Installation
75
86
 
76
87
  Requirements: Node.js ≥ 22, macOS, Linux or Windows, Claude Code writing to `~/.claude`. On Windows, live process status is read via PowerShell; the autostart service and the live `demo` are macOS/Linux only (`demo --out <dir>` works everywhere).
77
88
 
@@ -129,7 +140,7 @@ Light, dark or follow the OS – switch with the button in the header (the choic
129
140
  FormatRegistry → versioned decoders → normalized model → REST + SSE → Svelte UI
130
141
  ```
131
142
 
132
- - **Per-record format detection:** every decoder scores a record (`type`, version range, structure); the best one wins, a fallback keeps unknown records. Adding support for a new Claude Code version = adding a decoder file. See [docs/FORMATS.md](docs/FORMATS.md).
143
+ - **Per-record format detection:** every decoder scores a record (`type`, version range, structure); the best one wins, a fallback keeps unknown records. Adding support for a new Claude Code version = adding a decoder file. See [docs/FORMATS.md](https://github.com/brokoskokoli/claude_inspect/blob/main/docs/FORMATS.md).
133
144
  - **In memory only:** startup reads process/job data and file heads (< 1 s); the full history (hundreds of MB) is aggregated in the background in a few seconds and then followed incrementally.
134
145
  - **Privacy & security:** listens on `127.0.0.1` only (never reachable from the network), rejects requests whose `Host` header isn't `localhost`/`127.0.0.1` (DNS rebinding), sends no CORS headers (other websites in your browser can't read the API), is read-only towards `~/.claude` (the CPU log goes to its own data directory and contains only timestamps, PIDs, program names and CPU percentages – no command lines), never reads credential files (`*.key`, `daemon/auth`, `ide/*.lock`, …) and masks token/secret fields in the raw view. The access token is an extra layer against *other local processes or users* on the same machine; on a single-user machine `--no-auth` (used by the autostart service) is a reasonable trade-off. Cost figures are estimates from public API list prices – with a subscription you do not pay these amounts directly.
135
146
 
@@ -159,7 +170,7 @@ tests/ anonymized fixtures per Claude Code version, snapshots, tests
159
170
  scripts/ fixture generator (demo data: src/server/cli/demo.ts)
160
171
  ```
161
172
 
162
- Contributions welcome – especially decoders for new Claude Code versions (run `npm run fixtures` and
173
+ Contributions welcome – see [CONTRIBUTING.md](https://github.com/brokoskokoli/claude_inspect/blob/main/CONTRIBUTING.md), especially decoders for new Claude Code versions (run `npm run fixtures` and
163
174
  `npm test`, then check the *Formats* page for unknown fields).
164
175
 
165
176
  ## Disclaimer
@@ -169,4 +180,4 @@ may change at any time; claude-inspect is designed to degrade gracefully (unknow
169
180
 
170
181
  ## License
171
182
 
172
- [MIT](LICENSE)
183
+ [MIT](https://github.com/brokoskokoli/claude_inspect/blob/main/LICENSE)
@@ -42,27 +42,7 @@ export class HistoryIndex {
42
42
  seen.add(p);
43
43
  continue;
44
44
  }
45
- const dir = subagentDir(f.path);
46
- let names = [];
47
- try {
48
- names = await readdir(dir);
49
- }
50
- catch {
51
- /* keine Subagenten */
52
- }
53
- for (const n of names) {
54
- const m = /^agent-(.+)\.jsonl$/.exec(n);
55
- if (!m)
56
- continue;
57
- const path = join(dir, n);
58
- seen.add(path);
59
- try {
60
- await this.scan(path, (await stat(path)).size, f.sessionId, m[1], project);
61
- }
62
- catch {
63
- /* gelöscht */
64
- }
65
- }
45
+ await this.scanSubagents(f, project, seen);
66
46
  }
67
47
  // Von Claude Code aufgeräumte Dateien vergessen
68
48
  for (const p of [...this.files.keys()])
@@ -71,7 +51,52 @@ export class HistoryIndex {
71
51
  this.totalFiles = this.files.size;
72
52
  this.ready = true;
73
53
  }
54
+ async scanSubagents(f, project, seen) {
55
+ const dir = subagentDir(f.path);
56
+ let names = [];
57
+ try {
58
+ names = await readdir(dir);
59
+ }
60
+ catch {
61
+ /* keine Subagenten */
62
+ }
63
+ for (const n of names) {
64
+ const m = /^agent-(.+)\.jsonl$/.exec(n);
65
+ if (!m)
66
+ continue;
67
+ const path = join(dir, n);
68
+ seen?.add(path);
69
+ try {
70
+ await this.scan(path, (await stat(path)).size, f.sessionId, m[1], project);
71
+ }
72
+ catch {
73
+ /* gelöscht */
74
+ }
75
+ }
76
+ }
77
+ /**
78
+ * Eine Session samt Subagenten sofort auf Stand bringen, ohne auf den ersten
79
+ * vollständigen Scan zu warten (für die Kostenansicht direkt nach dem Start).
80
+ */
81
+ async ensureSession(f, project) {
82
+ try {
83
+ await this.scan(f.path, (await stat(f.path)).size, f.sessionId, undefined, project);
84
+ }
85
+ catch {
86
+ return; // gelöscht
87
+ }
88
+ await this.scanSubagents(f, project);
89
+ }
90
+ /** Läuft parallel zum Hintergrund-Scan: pro Datei nur ein Lesevorgang gleichzeitig. */
91
+ scanning = new Map();
74
92
  async scan(path, size, sessionId, agentId, project) {
93
+ for (let running = this.scanning.get(path); running; running = this.scanning.get(path))
94
+ await running;
95
+ const p = this.scanUnlocked(path, size, sessionId, agentId, project).finally(() => this.scanning.delete(path));
96
+ this.scanning.set(path, p);
97
+ return p;
98
+ }
99
+ async scanUnlocked(path, size, sessionId, agentId, project) {
75
100
  let agg = this.files.get(path);
76
101
  if (agg && agg.size === size)
77
102
  return;
@@ -261,6 +286,33 @@ export class HistoryIndex {
261
286
  heat,
262
287
  };
263
288
  }
289
+ /**
290
+ * Tokens je Modell eines Transkripts (Hauptagent oder Subagent) für die Kostenansicht.
291
+ * `seen` enthält bereits gezählte Message-Ids, damit bei Fork/Resume kopierte Antworten
292
+ * im selben Baum nicht doppelt zählen.
293
+ */
294
+ usageOf(sessionId, agentId, seen, each) {
295
+ const out = new Map();
296
+ for (const a of this.files.values()) {
297
+ if (a.sessionId !== sessionId || a.agentId !== agentId)
298
+ continue;
299
+ for (const [messageId, u] of a.usage) {
300
+ if (seen.has(messageId))
301
+ continue;
302
+ seen.add(messageId);
303
+ let t = out.get(u.model);
304
+ if (!t)
305
+ out.set(u.model, (t = emptyTotals()));
306
+ addUsage(t, u.usage);
307
+ if (each) {
308
+ const one = emptyTotals();
309
+ addUsage(one, u.usage);
310
+ each(u.model, u.ts, one);
311
+ }
312
+ }
313
+ }
314
+ return out;
315
+ }
264
316
  toolCalls(q) {
265
317
  const { aggs, since } = this.selected(q);
266
318
  const needle = q.q?.toLowerCase();
@@ -155,6 +155,11 @@ export function startServer(inspector, opts) {
155
155
  m = /^\/api\/cpu\/([\w-]+)$/.exec(p);
156
156
  if (m)
157
157
  return json(res, 200, await inspector.cpuHistory(m[1]));
158
+ m = /^\/api\/cost\/([\w-]+)$/.exec(p);
159
+ if (m) {
160
+ const cost = await inspector.sessionCost(m[1]);
161
+ return cost ? json(res, 200, cost) : json(res, 404, { error: 'unknown session' });
162
+ }
158
163
  m = /^\/api\/flow\/([\w-]+)$/.exec(p);
159
164
  if (m) {
160
165
  const flow = await inspector.flow(m[1]);
@@ -6,6 +6,7 @@ import { CLAUDE_DIR, relToClaude } from './config.js';
6
6
  import { CpuMonitor } from './cpu-monitor.js';
7
7
  import { registry } from './formats/index.js';
8
8
  import { HistoryIndex } from './history/aggregator.js';
9
+ import { emptyTotals, estimateCost, mergeTotals } from '../shared/pricing.js';
9
10
  import { search } from './history/search.js';
10
11
  import { analyze } from './model/analyze.js';
11
12
  import { laneData } from './model/flow.js';
@@ -500,7 +501,129 @@ export class Inspector extends EventEmitter {
500
501
  }
501
502
  return { rootSessionId, lanes, origin };
502
503
  }
504
+ /**
505
+ * Geschätzte Kosten einer Session samt aller Subagenten und per `claude` gestarteten
506
+ * Sitzungen (rekursiv, gleicher Baum wie Zeitleiste und Graph).
507
+ */
508
+ async sessionCost(rootSessionId) {
509
+ const flow = await this.flow(rootSessionId);
510
+ if (!flow)
511
+ return undefined;
512
+ // nur die Dateien dieses Baums einlesen, statt auf den ersten vollständigen History-Scan zu warten
513
+ for (const sid of new Set(flow.lanes.map((l) => l.sessionId))) {
514
+ const f = this.index.get(sid);
515
+ if (f)
516
+ await this.history.ensureSession(f, await this.projectOf(f));
517
+ }
518
+ const seen = new Set();
519
+ const total = emptyTotals();
520
+ const byModel = new Map();
521
+ const unpriced = new Set();
522
+ let costUSD = 0;
523
+ const points = [];
524
+ const rows = flow.lanes.map((lane) => {
525
+ const usage = this.history.usageOf(lane.sessionId, lane.agentId, seen, (model, ts, one) => {
526
+ if (ts)
527
+ points.push({ ts, cost: estimateCost(model, one) ?? 0, output: one.output, model, lane });
528
+ });
529
+ const tokens = emptyTotals();
530
+ let cost = 0;
531
+ const models = [...usage.entries()].map(([model, t]) => {
532
+ mergeTotals(tokens, t);
533
+ let m = byModel.get(model);
534
+ if (!m)
535
+ byModel.set(model, (m = emptyTotals()));
536
+ mergeTotals(m, t);
537
+ const c = estimateCost(model, t);
538
+ if (c === undefined)
539
+ unpriced.add(model);
540
+ else
541
+ cost += c;
542
+ return { model, tokens: t, costUSD: c };
543
+ });
544
+ mergeTotals(total, tokens);
545
+ costUSD += cost;
546
+ return {
547
+ id: lane.id,
548
+ sessionId: lane.sessionId,
549
+ agentId: lane.agentId,
550
+ kind: lane.kind,
551
+ label: lane.label,
552
+ sublabel: lane.sublabel,
553
+ parentId: lane.parentId,
554
+ depth: lane.depth,
555
+ status: lane.status,
556
+ byModel: models.sort((a, b) => (b.costUSD ?? 0) - (a.costUSD ?? 0)),
557
+ tokens,
558
+ costUSD: cost,
559
+ };
560
+ });
561
+ const rootFile = this.index.get(rootSessionId);
562
+ return {
563
+ rootSessionId,
564
+ rows,
565
+ total,
566
+ costUSD,
567
+ byModel: [...byModel.entries()]
568
+ .map(([model, tokens]) => ({ model, tokens, costUSD: estimateCost(model, tokens) }))
569
+ .sort((a, b) => (b.costUSD ?? 0) - (a.costUSD ?? 0)),
570
+ unpricedModels: [...unpriced],
571
+ reportedCostUSD: rootFile ? (await this.index.summary(rootFile)).costUSD : undefined,
572
+ timeline: costTimeline(points),
573
+ };
574
+ }
503
575
  formats() {
504
576
  return registry.report();
505
577
  }
506
578
  }
579
+ const MIN = 60_000;
580
+ const BUCKET_SIZES = [1, 2, 5, 10, 15, 30, 60, 120, 180, 360, 720, 1440].map((m) => m * MIN);
581
+ /** Verteilt die Modellantworten auf höchstens ~120 gleich lange Zeitabschnitte. */
582
+ function costTimeline(points) {
583
+ if (!points.length)
584
+ return { bucketMs: MIN, buckets: [] };
585
+ let first = Infinity;
586
+ let last = -Infinity;
587
+ for (const p of points) {
588
+ first = Math.min(first, p.ts);
589
+ last = Math.max(last, p.ts);
590
+ }
591
+ const span = last - first;
592
+ const bucketMs = BUCKET_SIZES.find((b) => span / b <= 120) ?? Math.ceil(span / 120 / (1440 * MIN)) * 1440 * MIN;
593
+ // an der lokalen Uhr ausrichten (volle Minuten/Stunden/Tage)
594
+ const offset = new Date(first).getTimezoneOffset() * MIN;
595
+ const start = Math.floor((first - offset) / bucketMs) * bucketMs + offset;
596
+ const n = Math.floor((last - start) / bucketMs) + 1;
597
+ const buckets = Array.from({ length: n }, (_, i) => ({
598
+ start: start + i * bucketMs,
599
+ costUSD: 0,
600
+ outputTokens: 0,
601
+ requests: 0,
602
+ byKind: {},
603
+ byModel: {},
604
+ top: [],
605
+ lanes: new Map(),
606
+ }));
607
+ for (const p of points) {
608
+ const b = buckets[Math.floor((p.ts - start) / bucketMs)];
609
+ b.costUSD += p.cost;
610
+ b.outputTokens += p.output;
611
+ b.requests++;
612
+ b.byKind[p.lane.kind] = (b.byKind[p.lane.kind] ?? 0) + p.cost;
613
+ b.byModel[p.model] = (b.byModel[p.model] ?? 0) + p.cost;
614
+ let l = b.lanes.get(p.lane.id);
615
+ if (!l)
616
+ b.lanes.set(p.lane.id, (l = { label: p.lane.label, sublabel: p.lane.sublabel, costUSD: 0 }));
617
+ l.costUSD += p.cost;
618
+ }
619
+ return {
620
+ bucketMs,
621
+ buckets: buckets.map(({ lanes, ...b }) => ({
622
+ ...b,
623
+ top: [...lanes.entries()]
624
+ .map(([id, l]) => ({ id, ...l }))
625
+ .sort((x, y) => y.costUSD - x.costUSD)
626
+ .slice(0, 3),
627
+ })),
628
+ };
629
+ }