claude-inspect 0.1.0

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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.de.md +84 -0
  3. package/README.md +167 -0
  4. package/dist/app/server/cli/demo.js +321 -0
  5. package/dist/app/server/cli/service.js +170 -0
  6. package/dist/app/server/cli-snapshot.js +34 -0
  7. package/dist/app/server/cli.js +84 -0
  8. package/dist/app/server/config.js +41 -0
  9. package/dist/app/server/formats/history/prompt.js +17 -0
  10. package/dist/app/server/formats/index.js +35 -0
  11. package/dist/app/server/formats/job/job.js +132 -0
  12. package/dist/app/server/formats/process/session-file.js +77 -0
  13. package/dist/app/server/formats/registry.js +87 -0
  14. package/dist/app/server/formats/subagent/meta.js +28 -0
  15. package/dist/app/server/formats/task/task.js +33 -0
  16. package/dist/app/server/formats/transcript/common.js +33 -0
  17. package/dist/app/server/formats/transcript/message.js +206 -0
  18. package/dist/app/server/formats/transcript/other.js +130 -0
  19. package/dist/app/server/formats/types.js +10 -0
  20. package/dist/app/server/formats/util.js +22 -0
  21. package/dist/app/server/history/aggregator.js +318 -0
  22. package/dist/app/server/history/search.js +138 -0
  23. package/dist/app/server/http.js +169 -0
  24. package/dist/app/server/inspector.js +468 -0
  25. package/dist/app/server/main.js +2 -0
  26. package/dist/app/server/model/analyze.js +112 -0
  27. package/dist/app/server/model/flow.js +47 -0
  28. package/dist/app/server/run.js +37 -0
  29. package/dist/app/server/sources/jobs.js +59 -0
  30. package/dist/app/server/sources/processes.js +99 -0
  31. package/dist/app/server/sources/projects.js +165 -0
  32. package/dist/app/server/sources/spawns.js +297 -0
  33. package/dist/app/server/sources/subagents.js +62 -0
  34. package/dist/app/server/sources/tasks.js +30 -0
  35. package/dist/app/server/sources/transcripts.js +121 -0
  36. package/dist/app/server/util/jsonl.js +124 -0
  37. package/dist/app/server/util/sanitize.js +42 -0
  38. package/dist/app/server/util/semver.js +33 -0
  39. package/dist/app/shared/pricing.js +57 -0
  40. package/dist/app/shared/tools.js +62 -0
  41. package/dist/app/shared/types.js +3 -0
  42. package/dist/web/assets/index-DdxYQpDc.js +14 -0
  43. package/dist/web/assets/index-VdEOo0XQ.css +1 -0
  44. package/dist/web/index.html +22 -0
  45. package/package.json +69 -0
@@ -0,0 +1,468 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { watch } from 'node:fs';
3
+ import { stat } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { CLAUDE_DIR, relToClaude } from './config.js';
6
+ import { registry } from './formats/index.js';
7
+ import { HistoryIndex } from './history/aggregator.js';
8
+ import { search } from './history/search.js';
9
+ import { analyze } from './model/analyze.js';
10
+ import { laneData } from './model/flow.js';
11
+ import { readTasks } from './sources/tasks.js';
12
+ import { readJobs } from './sources/jobs.js';
13
+ import { readProcesses } from './sources/processes.js';
14
+ import { ProjectIndex } from './sources/projects.js';
15
+ import { SpawnIndex } from './sources/spawns.js';
16
+ import { agentCallIds, listSubagents } from './sources/subagents.js';
17
+ import { TranscriptStore } from './sources/transcripts.js';
18
+ /** Subagenten, deren Datei so lange unverändert ist, gelten ohne Abschluss als "stale". */
19
+ const SUBAGENT_STALE_MS = 10 * 60_000;
20
+ /** Subagenten erscheinen auf dem Dashboard, wenn sie so kürzlich aktiv waren. */
21
+ const SUBAGENT_DASHBOARD_MS = 30 * 60_000;
22
+ export class Inspector extends EventEmitter {
23
+ index = new ProjectIndex();
24
+ store = new TranscriptStore();
25
+ /** Verknüpfung per Bash gestarteter claude-Sitzungen mit ihrem Aufrufer. */
26
+ spawns = new SpawnIndex();
27
+ spawnsReady = false;
28
+ /** Aggregierte History (Tokens, Kosten, Tool-Aufrufe, Dateien) – nur im Speicher. */
29
+ history = new HistoryIndex();
30
+ historyReady = false;
31
+ historyReadyResolve;
32
+ /** Erfüllt, sobald der erste History-Scan durch ist (API-Aufrufe warten darauf). */
33
+ historyLoaded = new Promise((r) => (this.historyReadyResolve = r));
34
+ processes = [];
35
+ jobs = [];
36
+ dashboard;
37
+ dashboardJson = '';
38
+ watcher;
39
+ dirty = { processes: true, jobs: true, index: false, dashboard: true };
40
+ timer;
41
+ poller;
42
+ building;
43
+ async start() {
44
+ await this.index.scan();
45
+ await this.rebuild();
46
+ // Erster Komplettscan nach claude-Aufrufen läuft im Hintergrund, damit der Start schnell bleibt.
47
+ void this.spawns
48
+ .refresh(this.index.list(), this.processes, true)
49
+ .then(() => {
50
+ this.spawnsReady = true;
51
+ this.schedule(0);
52
+ return this.history.refresh(this.index.list(), this.projectOf, true);
53
+ })
54
+ .then(() => {
55
+ this.historyReady = true;
56
+ this.historyReadyResolve();
57
+ });
58
+ try {
59
+ // macOS/Windows: rekursives Watching nativ. Linux (Node ≥ 20): ebenfalls unterstützt.
60
+ this.watcher = watch(CLAUDE_DIR, { recursive: true }, (_ev, filename) => {
61
+ if (filename)
62
+ this.onFsEvent(join(CLAUDE_DIR, filename.toString()));
63
+ });
64
+ this.watcher.on('error', () => undefined);
65
+ }
66
+ catch {
67
+ /* Fallback: nur Polling */
68
+ }
69
+ // Polling als Netz: Prozess-Liveness (kein Datei-Event beim Absturz) und verpasste Events.
70
+ this.poller = setInterval(() => void this.poll(), 3000);
71
+ }
72
+ stop() {
73
+ this.watcher?.close();
74
+ clearInterval(this.poller);
75
+ clearTimeout(this.timer);
76
+ }
77
+ onFsEvent(abs) {
78
+ const rel = relToClaude(abs);
79
+ if (rel.startsWith('sessions/'))
80
+ this.dirty.processes = true;
81
+ else if (rel.startsWith('jobs/') || rel.startsWith('daemon/'))
82
+ this.dirty.jobs = true;
83
+ else if (rel.startsWith('projects/')) {
84
+ if (rel.endsWith('.jsonl')) {
85
+ void this.onTranscriptChanged(abs);
86
+ if (!rel.includes('/subagents/'))
87
+ void this.index.touch(abs);
88
+ }
89
+ this.dirty.dashboard = true;
90
+ }
91
+ else
92
+ return;
93
+ this.schedule();
94
+ }
95
+ async onTranscriptChanged(path) {
96
+ const r = await this.store.refreshIfLoaded(path);
97
+ if (r)
98
+ this.emitTranscript(r.transcript);
99
+ }
100
+ emitTranscript(t) {
101
+ this.emit('event', { type: 'transcript', data: { sessionId: t.sessionId, agentId: t.agentId, total: t.entries.length } });
102
+ }
103
+ async poll() {
104
+ for (const t of this.store.all()) {
105
+ try {
106
+ const st = await stat(t.path);
107
+ if (st.size !== t.bytes)
108
+ await this.onTranscriptChanged(t.path);
109
+ }
110
+ catch {
111
+ /* gelöscht */
112
+ }
113
+ }
114
+ this.dirty.processes = true;
115
+ this.dirty.jobs = true;
116
+ this.schedule(0);
117
+ }
118
+ schedule(delay = 250) {
119
+ clearTimeout(this.timer);
120
+ this.timer = setTimeout(() => void this.rebuild(), delay);
121
+ }
122
+ async rebuild() {
123
+ if (this.building) {
124
+ this.schedule();
125
+ return;
126
+ }
127
+ this.building = (async () => {
128
+ if (this.dirty.processes) {
129
+ this.dirty.processes = false;
130
+ this.processes = await readProcesses();
131
+ }
132
+ if (this.dirty.jobs) {
133
+ this.dirty.jobs = false;
134
+ this.jobs = await readJobs();
135
+ }
136
+ if (this.spawnsReady)
137
+ await this.spawns.refresh(this.index.list(), this.processes, false);
138
+ if (this.historyReady)
139
+ await this.history.refresh(this.index.list(), this.projectOf, false);
140
+ this.dirty.dashboard = false;
141
+ const d = await this.buildDashboard();
142
+ const { generatedAt: _g, ...cmp } = d;
143
+ const json = JSON.stringify(cmp);
144
+ this.dashboard = d;
145
+ if (json !== this.dashboardJson) {
146
+ this.dashboardJson = json;
147
+ this.emit('event', { type: 'dashboard', data: d });
148
+ }
149
+ })().finally(() => (this.building = undefined));
150
+ await this.building;
151
+ }
152
+ async getDashboard() {
153
+ if (!this.dashboard)
154
+ await this.rebuild();
155
+ return this.dashboard;
156
+ }
157
+ // -------------------------------------------------------------------------
158
+ jobFor(p) {
159
+ return this.jobs.find((j) => j.short === p.jobId) ?? this.jobs.find((j) => j.short === p.parkedJobId);
160
+ }
161
+ async buildDashboard() {
162
+ const procs = [...this.processes].sort((a, b) => Number(b.alive) - Number(a.alive) || (b.updatedAt ?? b.startedAt ?? 0) - (a.updatedAt ?? a.startedAt ?? 0));
163
+ const processes = [];
164
+ for (const p of procs) {
165
+ const dp = { process: p, job: this.jobFor(p), spawnedBy: await this.spawnParent(p.sessionId) };
166
+ if (p.alive) {
167
+ try {
168
+ dp.session = await this.sessionLive(p.sessionId, p);
169
+ }
170
+ catch {
171
+ /* Transcript nicht lesbar */
172
+ }
173
+ }
174
+ processes.push(dp);
175
+ }
176
+ const usedJobs = new Set(processes.flatMap((p) => [p.process.jobId, p.process.parkedJobId]).filter(Boolean));
177
+ const orphanJobs = this.jobs.filter((j) => !usedJobs.has(j.short)).sort((a, b) => (b.updatedAt ?? '').localeCompare(a.updatedAt ?? ''));
178
+ const liveIds = new Set(processes.filter((p) => p.process.alive).map((p) => p.process.sessionId));
179
+ const recentSessions = [];
180
+ for (const f of this.index.list().slice(0, 25)) {
181
+ const s = await this.index.summary(f);
182
+ recentSessions.push({ ...s, live: liveIds.has(s.sessionId), spawnedBy: this.spawns.parentOf(s.sessionId)?.parentSessionId });
183
+ }
184
+ return { generatedAt: Date.now(), claudeDir: CLAUDE_DIR, processes, orphanJobs, recentSessions };
185
+ }
186
+ async sessionLive(sessionId, p) {
187
+ const f = this.index.get(sessionId);
188
+ if (!f)
189
+ return undefined;
190
+ const t = await this.store.get(f.path, sessionId);
191
+ const a = analyze(t.entries);
192
+ const busy = p.status !== 'idle';
193
+ const subFiles = await listSubagents(f.path);
194
+ const recent = subFiles.filter((s) => Date.now() - s.mtime < SUBAGENT_DASHBOARD_MS);
195
+ const subagents = await this.resolveSubagents(f.path, recent, true);
196
+ let current = a.current;
197
+ if (!busy && current && current.kind !== 'error')
198
+ current = { kind: 'idle', label: current.kind === 'idle' ? current.label : 'waiting for input', since: current.since };
199
+ return {
200
+ sessionId,
201
+ title: a.title,
202
+ model: a.model,
203
+ effort: a.effort,
204
+ permissionMode: a.permissionMode,
205
+ gitBranch: (await this.index.summary(f)).gitBranch,
206
+ lastActivityAt: a.lastActivityAt,
207
+ contextTokens: a.contextTokens,
208
+ outputTokens: a.outputTokens,
209
+ toolCalls: a.toolCalls,
210
+ current,
211
+ pendingTools: busy ? a.pendingTools : [],
212
+ subagents,
213
+ subagentTotal: subFiles.length,
214
+ costUSD: a.costUSD,
215
+ lastPrompt: a.lastPrompt,
216
+ spawned: await this.spawnedSessions(sessionId),
217
+ };
218
+ }
219
+ async spawnParent(sessionId) {
220
+ const link = this.spawns.parentOf(sessionId);
221
+ if (!link)
222
+ return undefined;
223
+ const pf = this.index.get(link.parentSessionId);
224
+ const proc = this.processes.find((p) => p.alive && p.sessionId === link.parentSessionId);
225
+ return { ...link, parentTitle: proc?.name ?? (pf ? (await this.index.summary(pf)).title : undefined) };
226
+ }
227
+ /** Von einer Session per `claude`-Aufruf gestartete Sitzungen mit Status. */
228
+ async spawnedSessions(parentSessionId) {
229
+ const out = [];
230
+ for (const link of this.spawns.childrenOf(parentSessionId)) {
231
+ const f = this.index.get(link.childSessionId);
232
+ if (!f)
233
+ continue;
234
+ const proc = this.processes.find((p) => p.alive && p.sessionId === link.childSessionId);
235
+ const child = this.spawns.childInfo(link.childSessionId);
236
+ const s = {
237
+ ...link,
238
+ agent: proc?.agent ?? child?.agentSetting,
239
+ pid: proc?.pid,
240
+ status: proc ? (proc.status === 'idle' ? 'idle' : 'running') : 'done',
241
+ };
242
+ if (proc) {
243
+ const a = analyze((await this.store.get(f.path, link.childSessionId)).entries);
244
+ Object.assign(s, { title: a.title, model: a.model, lastActivityAt: a.lastActivityAt, current: a.current });
245
+ if (a.finished && proc.status === 'idle')
246
+ s.status = 'idle';
247
+ }
248
+ else {
249
+ const sum = await this.index.summary(f);
250
+ Object.assign(s, { title: sum.title, model: sum.model, lastActivityAt: sum.lastTimestamp });
251
+ }
252
+ // Ohne Titel (z. B. `claude -p "/usage"`): erste Zeile des Auftrags anzeigen
253
+ s.title ??= child?.prompt?.split('\n', 1)[0].slice(0, 120);
254
+ out.push(s);
255
+ }
256
+ return out.sort((a, b) => (a.lastActivityAt ?? '').localeCompare(b.lastActivityAt ?? ''));
257
+ }
258
+ /**
259
+ * Baut SubagentInfo inkl. Eltern-Zuordnung. `load` = Transcripts laden, um
260
+ * Status und aktuelle Aktivität zu bestimmen (für Dashboard und Detailansicht).
261
+ */
262
+ async resolveSubagents(sessionPath, files, load) {
263
+ const sessionId = sessionPath.replace(/^.*\//, '').replace(/\.jsonl$/, '');
264
+ const out = [];
265
+ for (const sf of files) {
266
+ const info = {
267
+ agentId: sf.agentId,
268
+ ...sf.meta,
269
+ status: 'stale',
270
+ lastActivityAt: new Date(sf.mtime).toISOString(),
271
+ };
272
+ if (load) {
273
+ const t = await this.store.get(sf.path, sessionId, sf.agentId);
274
+ const a = analyze(t.entries);
275
+ const fresh = Date.now() - sf.mtime < SUBAGENT_STALE_MS;
276
+ info.status = a.errored ? 'error' : a.finished ? 'done' : fresh ? 'running' : 'stale';
277
+ info.model = a.model;
278
+ info.current = info.status === 'running' ? a.current : null;
279
+ info.toolCalls = a.toolCalls;
280
+ info.outputTokens = a.outputTokens;
281
+ info.lastActivityAt = a.lastActivityAt ?? info.lastActivityAt;
282
+ if (!info.description)
283
+ info.description = a.title;
284
+ }
285
+ out.push(info);
286
+ }
287
+ // Eltern: Tiefe 1 → Hauptagent. Tiefer: der Subagent, dessen Datei den Agent-Aufruf enthält.
288
+ const all = files.length && out.some((s) => (s.spawnDepth ?? 1) > 1) ? await listSubagents(sessionPath) : [];
289
+ for (const s of out) {
290
+ if ((s.spawnDepth ?? 1) <= 1 || !s.toolUseId)
291
+ continue;
292
+ for (const cand of all) {
293
+ if (cand.agentId === s.agentId)
294
+ continue;
295
+ if ((await agentCallIds(cand.path, cand.size)).has(s.toolUseId)) {
296
+ s.parentAgentId = cand.agentId;
297
+ break;
298
+ }
299
+ }
300
+ }
301
+ return out.sort((a, b) => (a.lastActivityAt ?? '').localeCompare(b.lastActivityAt ?? ''));
302
+ }
303
+ // -------------------------------------------------------------------------
304
+ // API
305
+ async sessions(offset, limit, q) {
306
+ let files = this.index.list();
307
+ const liveIds = new Set(this.processes.filter((p) => p.alive).map((p) => p.sessionId));
308
+ if (q) {
309
+ const needle = q.toLowerCase();
310
+ const matches = [];
311
+ for (const f of files) {
312
+ const s = await this.index.summary(f);
313
+ if ([s.project, s.title, s.lastPrompt, s.sessionId, s.cwd].some((x) => x?.toLowerCase().includes(needle)))
314
+ matches.push(s);
315
+ }
316
+ return {
317
+ total: matches.length,
318
+ items: matches.slice(offset, offset + limit).map((s) => ({ ...s, live: liveIds.has(s.sessionId), spawnedBy: this.spawns.parentOf(s.sessionId)?.parentSessionId })),
319
+ };
320
+ }
321
+ const total = files.length;
322
+ files = files.slice(offset, offset + limit);
323
+ const items = await Promise.all(files.map(async (f) => ({ ...(await this.index.summary(f)), live: liveIds.has(f.sessionId), spawnedBy: this.spawns.parentOf(f.sessionId)?.parentSessionId })));
324
+ return { total, items };
325
+ }
326
+ async sessionDetail(sessionId) {
327
+ const f = this.index.get(sessionId);
328
+ if (!f)
329
+ return undefined;
330
+ const summary = await this.index.summary(f);
331
+ const files = await listSubagents(f.path);
332
+ // Status aller Subagenten braucht deren Inhalt; kleine Dateien, daher vertretbar.
333
+ const subagents = await this.resolveSubagents(f.path, files, true);
334
+ const process = this.processes.find((p) => p.sessionId === sessionId && p.alive) ?? this.processes.find((p) => p.sessionId === sessionId);
335
+ const job = process ? this.jobFor(process) : this.jobs.find((j) => j.sessionId === sessionId || j.resumeSessionId === sessionId);
336
+ return {
337
+ summary: { ...summary, live: !!process?.alive },
338
+ subagents,
339
+ spawned: await this.spawnedSessions(sessionId),
340
+ spawnedBy: await this.spawnParent(sessionId),
341
+ tasks: await readTasks(sessionId),
342
+ process,
343
+ job,
344
+ };
345
+ }
346
+ async transcriptFor(sessionId, agentId) {
347
+ const f = this.index.get(sessionId);
348
+ if (!f)
349
+ return undefined;
350
+ if (!agentId)
351
+ return this.store.get(f.path, sessionId);
352
+ const sub = (await listSubagents(f.path)).find((s) => s.agentId === agentId);
353
+ return sub ? this.store.get(sub.path, sessionId, agentId) : undefined;
354
+ }
355
+ async transcript(sessionId, agentId, from) {
356
+ const t = await this.transcriptFor(sessionId, agentId);
357
+ if (!t)
358
+ return undefined;
359
+ return { sessionId, agentId, entries: t.entries.slice(from), total: t.entries.length, from };
360
+ }
361
+ async rawLine(sessionId, agentId, line) {
362
+ const t = await this.transcriptFor(sessionId, agentId);
363
+ return t?.rawLine(line);
364
+ }
365
+ projectOf = async (f) => (await this.index.summary(f)).project;
366
+ async historyStats(filter) {
367
+ await this.historyLoaded;
368
+ return this.history.stats(filter);
369
+ }
370
+ async toolCalls(q) {
371
+ await this.historyLoaded;
372
+ return this.history.toolCalls(q);
373
+ }
374
+ async fileStats(filter) {
375
+ await this.historyLoaded;
376
+ return this.history.fileStats(filter);
377
+ }
378
+ async historyProjects() {
379
+ await this.historyLoaded;
380
+ return this.history.projects();
381
+ }
382
+ async search(q) {
383
+ const files = [];
384
+ for (const f of this.index.list()) {
385
+ const sum = await this.index.summary(f);
386
+ files.push({ path: f.path, sessionId: f.sessionId, project: sum.project, title: sum.title });
387
+ for (const sf of await listSubagents(f.path)) {
388
+ files.push({ path: sf.path, sessionId: f.sessionId, agentId: sf.agentId, project: sum.project, title: sf.meta.description ?? sum.title });
389
+ }
390
+ }
391
+ return search(q, files, new Set(this.index.list().map((f) => f.sessionId)));
392
+ }
393
+ /**
394
+ * Ablauf einer Session für Zeitleiste und Graph: Hauptagent, Subagenten
395
+ * (verschachtelt) und per claude-Aufruf gestartete Sitzungen (rekursiv).
396
+ */
397
+ async flow(rootSessionId) {
398
+ const rootFile = this.index.get(rootSessionId);
399
+ if (!rootFile)
400
+ return undefined;
401
+ const lanes = [];
402
+ const visited = new Set();
403
+ const addSession = async (f, kind, depth, parent) => {
404
+ if (visited.has(f.sessionId) || depth > 6)
405
+ return;
406
+ visited.add(f.sessionId);
407
+ const t = await this.store.get(f.path, f.sessionId);
408
+ const a = analyze(t.entries);
409
+ const sum = await this.index.summary(f);
410
+ const proc = this.processes.find((p) => p.alive && p.sessionId === f.sessionId);
411
+ const mainId = f.sessionId;
412
+ lanes.push({
413
+ id: mainId,
414
+ sessionId: f.sessionId,
415
+ kind,
416
+ label: proc?.name ?? sum.title ?? this.spawns.childInfo(f.sessionId)?.prompt?.split('\n', 1)[0].slice(0, 80) ?? f.sessionId.slice(0, 8),
417
+ sublabel: kind === 'spawn' ? (this.spawns.childInfo(f.sessionId)?.agentSetting ?? 'claude') : sum.project,
418
+ parentId: parent?.id,
419
+ viaToolUseId: parent?.via,
420
+ confidence: parent?.confidence,
421
+ depth,
422
+ status: proc ? (proc.status === 'idle' ? 'idle' : 'running') : a.errored ? 'error' : 'done',
423
+ ...laneData(t.entries),
424
+ });
425
+ const subFiles = await listSubagents(f.path);
426
+ const subs = await this.resolveSubagents(f.path, subFiles, true);
427
+ const subIds = new Set(subs.map((x) => x.agentId));
428
+ const depthOf = (x) => (x.spawnDepth ?? 1);
429
+ for (const sub of [...subs].sort((x, y) => depthOf(x) - depthOf(y))) {
430
+ const sf = subFiles.find((x) => x.agentId === sub.agentId);
431
+ const st = await this.store.get(sf.path, f.sessionId, sub.agentId);
432
+ lanes.push({
433
+ id: `${f.sessionId}/${sub.agentId}`,
434
+ sessionId: f.sessionId,
435
+ agentId: sub.agentId,
436
+ kind: 'subagent',
437
+ label: sub.agentType ?? 'agent',
438
+ sublabel: sub.description,
439
+ parentId: sub.parentAgentId && subIds.has(sub.parentAgentId) ? `${f.sessionId}/${sub.parentAgentId}` : mainId,
440
+ viaToolUseId: sub.toolUseId,
441
+ depth: depth + depthOf(sub),
442
+ status: sub.status,
443
+ ...laneData(st.entries),
444
+ });
445
+ }
446
+ for (const link of this.spawns.childrenOf(f.sessionId)) {
447
+ const cf = this.index.get(link.childSessionId);
448
+ if (!cf)
449
+ continue;
450
+ const parentId = link.parentAgentId && subIds.has(link.parentAgentId) ? `${f.sessionId}/${link.parentAgentId}` : mainId;
451
+ const parentDepth = lanes.find((l) => l.id === parentId)?.depth ?? depth;
452
+ await addSession(cf, 'spawn', parentDepth + 1, { id: parentId, via: link.toolUseId, confidence: link.confidence });
453
+ }
454
+ };
455
+ await addSession(rootFile, 'main', 0);
456
+ let origin;
457
+ const job = this.jobs.find((j) => j.sessionId === rootSessionId || j.resumeSessionId === rootSessionId);
458
+ const from = job?.fork?.parentSessionId ?? job?.launch?.fromSessionId;
459
+ if (from && from !== rootSessionId) {
460
+ const of = this.index.get(from);
461
+ origin = { kind: job?.fork || job?.launch?.fork ? 'fork' : 'resume', sessionId: from, title: of ? (await this.index.summary(of)).title : undefined };
462
+ }
463
+ return { rootSessionId, lanes, origin };
464
+ }
465
+ formats() {
466
+ return registry.report();
467
+ }
468
+ }
@@ -0,0 +1,2 @@
1
+ // Kept for `tsx src/server/main.ts` (npm run dev) – the actual entry point is cli.ts.
2
+ import './cli.js';
@@ -0,0 +1,112 @@
1
+ import { toolSummary } from '../../shared/tools.js';
2
+ import { num, str } from '../formats/util.js';
3
+ const SIGNIFICANT = new Set(['user-text', 'assistant-text', 'thinking', 'tool-use', 'tool-result']);
4
+ function short(s, max = 140) {
5
+ const one = s
6
+ .replace(/```[\s\S]*?```/g, ' ')
7
+ .replace(/\*\*|__|`|^#+\s*/gm, '')
8
+ .replace(/\s+/g, ' ')
9
+ .trim();
10
+ return one.length > max ? one.slice(0, max - 1) + '…' : one;
11
+ }
12
+ export function analyze(entries) {
13
+ const a = { outputTokens: 0, toolCalls: 0, pendingTools: [], current: null, finished: false, errored: false };
14
+ const open = new Map();
15
+ const outputByMessage = new Map();
16
+ const titles = {};
17
+ for (const e of entries) {
18
+ if (e.timestamp && (!a.lastActivityAt || e.timestamp > a.lastActivityAt))
19
+ a.lastActivityAt = e.timestamp;
20
+ switch (e.kind) {
21
+ case 'tool-use':
22
+ a.toolCalls++;
23
+ open.set(e.toolUseId, e);
24
+ break;
25
+ case 'tool-result':
26
+ open.delete(e.toolUseId);
27
+ break;
28
+ case 'user-text':
29
+ if (e.permissionMode)
30
+ a.permissionMode = e.permissionMode;
31
+ if (!e.isMeta && e.text && !e.text.startsWith('<'))
32
+ a.lastPrompt = e.text;
33
+ break;
34
+ case 'meta': {
35
+ const d = e.data;
36
+ if (e.metaType === 'permission-mode' && str(d.permissionMode))
37
+ a.permissionMode = str(d.permissionMode);
38
+ if (e.metaType === 'custom-title' && str(d.customTitle))
39
+ titles.custom = str(d.customTitle);
40
+ if (e.metaType === 'agent-name' && str(d.agentName))
41
+ titles.agent = str(d.agentName);
42
+ if (e.metaType === 'ai-title' && str(d.aiTitle))
43
+ titles.ai = str(d.aiTitle);
44
+ if (e.metaType === 'last-prompt' && str(d.lastPrompt))
45
+ a.lastPrompt = str(d.lastPrompt);
46
+ if (e.metaType === 'cost-state' && num(d.totalCostUSD) !== undefined)
47
+ a.costUSD = num(d.totalCostUSD);
48
+ break;
49
+ }
50
+ }
51
+ if ((e.kind === 'assistant-text' || e.kind === 'thinking' || e.kind === 'tool-use') && e.usage) {
52
+ if (e.model && e.model !== '<synthetic>')
53
+ a.model = e.model;
54
+ if (e.effort)
55
+ a.effort = e.effort;
56
+ const u = e.usage;
57
+ a.contextTokens = (u.input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
58
+ if (e.messageId)
59
+ outputByMessage.set(e.messageId, u.output_tokens ?? 0);
60
+ }
61
+ }
62
+ for (const v of outputByMessage.values())
63
+ a.outputTokens += v;
64
+ a.title = titles.custom ?? titles.agent ?? titles.ai;
65
+ a.pendingTools = [...open.values()].map((t) => ({
66
+ toolUseId: t.toolUseId,
67
+ name: t.name,
68
+ summary: toolSummary(t.name, t.input),
69
+ since: t.timestamp,
70
+ }));
71
+ // Aktuelle Aktivität aus dem letzten bedeutsamen Eintrag
72
+ let last;
73
+ for (let i = entries.length - 1; i >= 0; i--) {
74
+ const e = entries[i];
75
+ if (e.kind === 'system' && e.subtype === 'api_error') {
76
+ a.current = { kind: 'error', label: short(e.text ?? String(e.data.error?.message ?? 'API error')), since: e.timestamp };
77
+ a.errored = true;
78
+ return a;
79
+ }
80
+ if (SIGNIFICANT.has(e.kind) && !e.isMeta) {
81
+ last = e;
82
+ break;
83
+ }
84
+ }
85
+ const pending = a.pendingTools.at(-1);
86
+ if (pending) {
87
+ a.current = { kind: 'tool', label: pending.summary, toolName: pending.name, since: pending.since };
88
+ }
89
+ else if (last) {
90
+ switch (last.kind) {
91
+ case 'assistant-text':
92
+ if (last.stopReason === 'end_turn' || last.stopReason === 'stop_sequence') {
93
+ a.finished = true;
94
+ a.current = { kind: 'idle', label: short(last.text), since: last.timestamp };
95
+ }
96
+ else {
97
+ a.current = { kind: 'text', label: short(last.text), since: last.timestamp };
98
+ }
99
+ break;
100
+ case 'thinking':
101
+ a.current = { kind: 'thinking', label: short(last.text) || 'thinking', since: last.timestamp };
102
+ break;
103
+ case 'tool-result':
104
+ a.current = { kind: 'waiting', label: 'model working (after tool result)', since: last.timestamp };
105
+ break;
106
+ case 'user-text':
107
+ a.current = { kind: 'waiting', label: 'new input: ' + short(last.text, 100), since: last.timestamp };
108
+ break;
109
+ }
110
+ }
111
+ return a;
112
+ }
@@ -0,0 +1,47 @@
1
+ import { toolSummary } from '../../shared/tools.js';
2
+ /** Tool-Spannen, Antwortzeitpunkte und Zeitraum eines Transcripts. */
3
+ export function laneData(entries) {
4
+ const spans = [];
5
+ const byId = new Map();
6
+ const turns = [];
7
+ const outByMsg = new Map();
8
+ let start;
9
+ let end;
10
+ let model;
11
+ for (const e of entries) {
12
+ const ts = e.timestamp ? Date.parse(e.timestamp) : NaN;
13
+ if (Number.isNaN(ts))
14
+ continue;
15
+ if (e.kind === 'meta' || e.kind === 'attachment')
16
+ continue;
17
+ start = start === undefined ? ts : Math.min(start, ts);
18
+ end = end === undefined ? ts : Math.max(end, ts);
19
+ if (e.kind === 'tool-use') {
20
+ // Fortgesetzte/geforkte Sessions enthalten Records teils doppelt
21
+ if (byId.has(e.toolUseId))
22
+ continue;
23
+ const s = { toolUseId: e.toolUseId, name: e.name, summary: toolSummary(e.name, e.input).slice(0, 160), start: ts };
24
+ spans.push(s);
25
+ byId.set(e.toolUseId, s);
26
+ }
27
+ else if (e.kind === 'tool-result') {
28
+ const s = byId.get(e.toolUseId);
29
+ if (s) {
30
+ s.end = Math.max(ts, s.start);
31
+ if (e.isError || e.denied)
32
+ s.isError = true;
33
+ }
34
+ }
35
+ if ((e.kind === 'assistant-text' || e.kind === 'tool-use' || e.kind === 'thinking') && e.messageId) {
36
+ if (!outByMsg.has(e.messageId))
37
+ turns.push(ts);
38
+ outByMsg.set(e.messageId, e.usage?.output_tokens ?? 0);
39
+ if (e.model && e.model !== '<synthetic>')
40
+ model = e.model;
41
+ }
42
+ }
43
+ let outputTokens = 0;
44
+ for (const v of outByMsg.values())
45
+ outputTokens += v;
46
+ return { spans, turns, start, end, toolCalls: spans.length, outputTokens, model };
47
+ }
@@ -0,0 +1,37 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { CLAUDE_DIR } from './config.js';
6
+ import { startServer } from './http.js';
7
+ import { Inspector } from './inspector.js';
8
+ /** Locates the built web UI – works from src/server (tsx), dist/app/server and an installed npm package. */
9
+ function findWebRoot() {
10
+ const here = dirname(fileURLToPath(import.meta.url));
11
+ const candidates = [join(here, '..', '..', 'dist', 'web'), join(here, '..', '..', 'web')];
12
+ return candidates.find((d) => existsSync(join(d, 'index.html'))) ?? candidates[0];
13
+ }
14
+ export async function runServer(opts) {
15
+ const inspector = new Inspector();
16
+ const t0 = Date.now();
17
+ await inspector.start();
18
+ const noAuth = !!(opts.dev || opts.noAuth);
19
+ const { server, token } = startServer(inspector, { port: opts.port, host: '127.0.0.1', webRoot: findWebRoot(), noAuth, token: opts.token });
20
+ server.on('error', (e) => {
21
+ console.error(e.code === 'EADDRINUSE' ? `Port ${opts.port} is already in use – choose another one with --port.` : String(e));
22
+ process.exit(1);
23
+ });
24
+ const url = noAuth ? `http://localhost:${opts.port}/` : `http://localhost:${opts.port}/?t=${token}`;
25
+ console.log(`claude-inspect is reading ${CLAUDE_DIR} (started in ${Date.now() - t0} ms)`);
26
+ console.log(opts.dev ? `API (dev, no token): ${url} – UI: npm run dev:web` : `Dashboard: ${url}`);
27
+ if (opts.open && process.platform === 'darwin')
28
+ execFile('open', [url]);
29
+ else if (opts.open && process.platform === 'linux')
30
+ execFile('xdg-open', [url]);
31
+ const shutdown = () => {
32
+ inspector.stop();
33
+ process.exit(0);
34
+ };
35
+ process.on('SIGINT', shutdown);
36
+ process.on('SIGTERM', shutdown);
37
+ }