claude-inspect 0.7.1 → 0.8.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.
- package/dist/app/server/cli.js +4 -0
- package/dist/app/server/history/aggregator.js +41 -24
- package/dist/app/server/history/search.js +14 -1
- package/dist/app/server/inspector.js +50 -1
- package/dist/app/server/sources/spawns.js +17 -13
- package/dist/app/server/util/jsonl.js +6 -4
- package/dist/web/assets/index-DKNJESE2.js +19 -0
- package/dist/web/assets/index-G8obC5Be.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-BH5nprTU.js +0 -19
- package/dist/web/assets/index-BNso7xYj.css +0 -1
package/dist/app/server/cli.js
CHANGED
|
@@ -12,6 +12,10 @@ import { readFileSync } from 'node:fs';
|
|
|
12
12
|
import { dirname, join } from 'node:path';
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
14
|
import { parseArgs } from 'node:util';
|
|
15
|
+
import v8 from 'node:v8';
|
|
16
|
+
// Läuft dauerhaft im Hintergrund: kleiner Heap statt Höchsttempo. Gemessen mit ~1.600 Transcripts
|
|
17
|
+
// 140 statt 700 MB Speicher, der erste Scan dauert dafür ~20 % länger. Keine harte Grenze.
|
|
18
|
+
v8.setFlagsFromString('--optimize-for-size');
|
|
15
19
|
const env = process.env;
|
|
16
20
|
function version() {
|
|
17
21
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -5,6 +5,22 @@ import { toolSummary } from '../../shared/tools.js';
|
|
|
5
5
|
import { registry } from '../formats/index.js';
|
|
6
6
|
import { subagentDir } from '../sources/subagents.js';
|
|
7
7
|
import { JsonlTail } from '../util/jsonl.js';
|
|
8
|
+
/** Wiederkehrende Texte (Modell- und Tool-Namen) nur einmal im Speicher halten. */
|
|
9
|
+
const interned = new Map();
|
|
10
|
+
function intern(s) {
|
|
11
|
+
let v = interned.get(s);
|
|
12
|
+
if (v === undefined)
|
|
13
|
+
interned.set(s, (v = s));
|
|
14
|
+
return v;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Eigene Kopie eines gekürzten Strings. V8 legt Teilstrings als Verweis auf den Ausgangstext an –
|
|
18
|
+
* eine 200-Zeichen-Zusammenfassung hielte sonst den kompletten (evtl. mehrere KB großen) Befehl fest.
|
|
19
|
+
*/
|
|
20
|
+
function own(s) {
|
|
21
|
+
return s.length < 13 ? s : Buffer.from(s, 'utf8').toString('utf8');
|
|
22
|
+
}
|
|
23
|
+
const CHUNK_BYTES = 2 * 1024 * 1024;
|
|
8
24
|
const FILE_TOOLS = {
|
|
9
25
|
Read: 'reads',
|
|
10
26
|
Edit: 'edits',
|
|
@@ -110,19 +126,22 @@ export class HistoryIndex {
|
|
|
110
126
|
}
|
|
111
127
|
agg.size = size;
|
|
112
128
|
agg.project = project;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
129
|
+
// in Stücken lesen: große Transcripts (50–100 MB) nicht komplett geparst im Speicher halten
|
|
130
|
+
for (let more = true; more;) {
|
|
131
|
+
let lines;
|
|
132
|
+
try {
|
|
133
|
+
({ lines, more } = await agg.tail.readNew(undefined, CHUNK_BYTES));
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
for (const l of lines) {
|
|
139
|
+
if (l.value === undefined)
|
|
140
|
+
continue;
|
|
141
|
+
const { out } = registry.decode('transcript', l.value, path, l.line, { silent: true });
|
|
142
|
+
for (const e of out)
|
|
143
|
+
this.fold(agg, e);
|
|
144
|
+
}
|
|
126
145
|
}
|
|
127
146
|
}
|
|
128
147
|
fold(agg, e) {
|
|
@@ -144,8 +163,8 @@ export class HistoryIndex {
|
|
|
144
163
|
agentId: agg.agentId,
|
|
145
164
|
project: agg.project,
|
|
146
165
|
toolUseId: e.toolUseId,
|
|
147
|
-
name: e.name,
|
|
148
|
-
summary: toolSummary(e.name, e.input).slice(0, 200),
|
|
166
|
+
name: intern(e.name),
|
|
167
|
+
summary: own(toolSummary(e.name, e.input).slice(0, 200)),
|
|
149
168
|
ts: Number.isNaN(ts) ? 0 : ts,
|
|
150
169
|
file,
|
|
151
170
|
});
|
|
@@ -168,7 +187,9 @@ export class HistoryIndex {
|
|
|
168
187
|
break;
|
|
169
188
|
}
|
|
170
189
|
if ((e.kind === 'assistant-text' || e.kind === 'thinking' || e.kind === 'tool-use') && e.usage && e.messageId && e.model && e.model !== '<synthetic>') {
|
|
171
|
-
|
|
190
|
+
const tokens = emptyTotals();
|
|
191
|
+
addUsage(tokens, e.usage);
|
|
192
|
+
agg.usage.set(e.messageId, { model: intern(e.model), ts: Number.isNaN(ts) ? 0 : ts, tokens });
|
|
172
193
|
}
|
|
173
194
|
}
|
|
174
195
|
// ---------------------------------------------------------------------------
|
|
@@ -209,8 +230,7 @@ export class HistoryIndex {
|
|
|
209
230
|
for (const u of a.usage.values()) {
|
|
210
231
|
if (u.ts < since)
|
|
211
232
|
continue;
|
|
212
|
-
const one =
|
|
213
|
-
addUsage(one, u.usage);
|
|
233
|
+
const one = u.tokens;
|
|
214
234
|
let m = byModel.get(u.model);
|
|
215
235
|
if (!m)
|
|
216
236
|
byModel.set(u.model, (m = emptyTotals()));
|
|
@@ -307,12 +327,9 @@ export class HistoryIndex {
|
|
|
307
327
|
let t = out.get(u.model);
|
|
308
328
|
if (!t)
|
|
309
329
|
out.set(u.model, (t = emptyTotals()));
|
|
310
|
-
|
|
311
|
-
if (each)
|
|
312
|
-
|
|
313
|
-
addUsage(one, u.usage);
|
|
314
|
-
each(u.model, u.ts, one);
|
|
315
|
-
}
|
|
330
|
+
mergeTotals(t, u.tokens);
|
|
331
|
+
if (each)
|
|
332
|
+
each(u.model, u.ts, { ...u.tokens });
|
|
316
333
|
}
|
|
317
334
|
}
|
|
318
335
|
return out;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
2
3
|
import { toolSummary } from '../../shared/tools.js';
|
|
3
4
|
import { paths } from '../config.js';
|
|
4
5
|
import { registry } from '../formats/index.js';
|
|
5
6
|
import { pathTail } from '../sources/projects.js';
|
|
7
|
+
const home = homedir();
|
|
8
|
+
const tildify = (p) => (home && p.startsWith(home) ? '~' + p.slice(home.length) : p);
|
|
6
9
|
function snippet(text, needle, radius = 90) {
|
|
7
10
|
const i = text.toLowerCase().indexOf(needle);
|
|
8
11
|
if (i < 0)
|
|
@@ -33,7 +36,7 @@ function searchable(e) {
|
|
|
33
36
|
* nach dem Suchbegriff durchsucht, nur Trefferzeilen werden dekodiert.
|
|
34
37
|
* Neueste Dateien zuerst; bricht bei `limit` Treffern oder nach `budgetMs` ab.
|
|
35
38
|
*/
|
|
36
|
-
export async function search(query, files, knownSessions, limit = 200, budgetMs =
|
|
39
|
+
export async function search(query, files, knownSessions, limit = 200, budgetMs = 15_000, perFile = 5) {
|
|
37
40
|
const t0 = Date.now();
|
|
38
41
|
const needle = query.trim().toLowerCase();
|
|
39
42
|
const hits = [];
|
|
@@ -41,6 +44,16 @@ export async function search(query, files, knownSessions, limit = 200, budgetMs
|
|
|
41
44
|
let truncated = false;
|
|
42
45
|
if (needle.length < 2)
|
|
43
46
|
return { query, hits, scannedFiles: 0, totalFiles: files.length, truncated, ms: 0 };
|
|
47
|
+
// Dateinamen und Pfade der Transcripts (z. B. "agent-a1b2….jsonl", Session-Id, Pfadteile)
|
|
48
|
+
const pathNeedle = (needle.startsWith('~') && home ? home.toLowerCase() + needle.slice(1) : needle).replace(/\\/g, '/');
|
|
49
|
+
for (const f of files) {
|
|
50
|
+
const p = f.path.replace(/\\/g, '/');
|
|
51
|
+
if (!p.toLowerCase().includes(pathNeedle))
|
|
52
|
+
continue;
|
|
53
|
+
hits.push({ sessionId: f.sessionId, agentId: f.agentId, project: f.project, title: f.title, line: 0, kind: 'file', snippet: tildify(f.path) });
|
|
54
|
+
if (hits.length >= 50)
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
44
57
|
outer: for (const f of files) {
|
|
45
58
|
if (Date.now() - t0 > budgetMs) {
|
|
46
59
|
truncated = true;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import { watch } from 'node:fs';
|
|
3
3
|
import { stat } from 'node:fs/promises';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
4
5
|
import { basename, join } from 'node:path';
|
|
5
6
|
import { CLAUDE_DIR, DATA_DIR, relToClaude } from './config.js';
|
|
6
7
|
import { buildDigest } from './review/digest.js';
|
|
@@ -483,14 +484,62 @@ export class Inspector extends EventEmitter {
|
|
|
483
484
|
}
|
|
484
485
|
async search(q) {
|
|
485
486
|
const files = [];
|
|
487
|
+
const subMeta = new Map();
|
|
486
488
|
for (const f of this.index.list()) {
|
|
487
489
|
const sum = await this.index.summary(f);
|
|
488
490
|
files.push({ path: f.path, sessionId: f.sessionId, project: sum.project, title: sum.title });
|
|
489
491
|
for (const sf of await listSubagents(f.path)) {
|
|
490
492
|
files.push({ path: sf.path, sessionId: f.sessionId, agentId: sf.agentId, project: sum.project, title: sf.meta.description ?? sum.title });
|
|
493
|
+
subMeta.set(`${f.sessionId}/${sf.agentId}`, { type: sf.meta.agentType, description: sf.meta.description, path: sf.path });
|
|
491
494
|
}
|
|
492
495
|
}
|
|
493
|
-
|
|
496
|
+
const res = await search(q, files, new Set(this.index.list().map((f) => f.sessionId)));
|
|
497
|
+
// Agenten der Treffer samt Vorfahren (Hauptagent, per claude -p aufrufender Agent) für die Baumansicht
|
|
498
|
+
const agents = {};
|
|
499
|
+
const home = homedir();
|
|
500
|
+
const tilde = (p) => (home && p.startsWith(home) ? '~' + p.slice(home.length) : p);
|
|
501
|
+
const add = async (sessionId, agentId, depth = 0) => {
|
|
502
|
+
const key = agentId ? `${sessionId}/${agentId}` : sessionId;
|
|
503
|
+
if (agents[key])
|
|
504
|
+
return key;
|
|
505
|
+
const f = this.index.get(sessionId);
|
|
506
|
+
if (!f || depth > 8)
|
|
507
|
+
return undefined;
|
|
508
|
+
const sum = await this.index.summary(f);
|
|
509
|
+
if (agentId) {
|
|
510
|
+
const m = subMeta.get(key);
|
|
511
|
+
agents[key] = {
|
|
512
|
+
key,
|
|
513
|
+
sessionId,
|
|
514
|
+
agentId,
|
|
515
|
+
kind: 'subagent',
|
|
516
|
+
label: m?.type ?? 'subagent',
|
|
517
|
+
sublabel: m?.description,
|
|
518
|
+
project: sum.project,
|
|
519
|
+
parentKey: await add(sessionId, undefined, depth + 1),
|
|
520
|
+
file: tilde(m?.path ?? f.path),
|
|
521
|
+
};
|
|
522
|
+
return key;
|
|
523
|
+
}
|
|
524
|
+
const link = this.spawns.parentOf(sessionId);
|
|
525
|
+
const proc = this.processes.find((p) => p.sessionId === sessionId);
|
|
526
|
+
agents[key] = {
|
|
527
|
+
key,
|
|
528
|
+
sessionId,
|
|
529
|
+
kind: link ? 'spawn' : 'main',
|
|
530
|
+
label: proc?.name ?? sum.title ?? sessionId.slice(0, 8),
|
|
531
|
+
sublabel: link ? (this.spawns.childInfo(sessionId)?.agentSetting ?? 'claude -p') : undefined,
|
|
532
|
+
project: sum.project,
|
|
533
|
+
file: tilde(f.path),
|
|
534
|
+
};
|
|
535
|
+
if (link)
|
|
536
|
+
agents[key].parentKey = await add(link.parentSessionId, link.parentAgentId, depth + 1);
|
|
537
|
+
return key;
|
|
538
|
+
};
|
|
539
|
+
for (const h of res.hits)
|
|
540
|
+
if (h.kind !== 'history-prompt')
|
|
541
|
+
await add(h.sessionId, h.agentId);
|
|
542
|
+
return { ...res, agents };
|
|
494
543
|
}
|
|
495
544
|
/**
|
|
496
545
|
* Ablauf einer Session für Zeitleiste und Graph: Hauptagent, Subagenten
|
|
@@ -189,19 +189,23 @@ export class SpawnIndex {
|
|
|
189
189
|
}
|
|
190
190
|
st.size = size;
|
|
191
191
|
try {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
st.
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (
|
|
204
|
-
|
|
192
|
+
// in Stücken lesen, damit große Dateien nicht komplett im Speicher landen
|
|
193
|
+
for (let more = true; more;) {
|
|
194
|
+
const r = await st.tail.readNew((t) => t.includes('"Bash"') && t.includes('claude'), 8 * 1024 * 1024);
|
|
195
|
+
more = r.more;
|
|
196
|
+
if (r.reset)
|
|
197
|
+
st.calls = [];
|
|
198
|
+
for (const l of r.lines) {
|
|
199
|
+
const raw = l.value;
|
|
200
|
+
if (!isObj(raw) || raw.type !== 'assistant')
|
|
201
|
+
continue;
|
|
202
|
+
const content = obj(raw.message)?.content;
|
|
203
|
+
if (!Array.isArray(content))
|
|
204
|
+
continue;
|
|
205
|
+
for (const b of content) {
|
|
206
|
+
if (isObj(b) && b.type === 'tool_use' && b.name === 'Bash')
|
|
207
|
+
st.calls.push(...parseCall(raw, b, sessionId, agentId));
|
|
208
|
+
}
|
|
205
209
|
}
|
|
206
210
|
}
|
|
207
211
|
}
|
|
@@ -22,8 +22,10 @@ export class JsonlTail {
|
|
|
22
22
|
/**
|
|
23
23
|
* @param prefilter Optional: nur Zeilen parsen, deren Rohtext passt (spart bei
|
|
24
24
|
* großen Dateien den Großteil der JSON.parse-Arbeit).
|
|
25
|
+
* @param maxBytes Optional: höchstens so viel lesen; `more` sagt, ob noch etwas folgt.
|
|
26
|
+
* Hält den Speicherbedarf bei großen Dateien klein (statt alles auf einmal).
|
|
25
27
|
*/
|
|
26
|
-
async readNew(prefilter) {
|
|
28
|
+
async readNew(prefilter, maxBytes = Infinity) {
|
|
27
29
|
const st = await stat(this.path);
|
|
28
30
|
let reset = false;
|
|
29
31
|
if (st.size < this.offset) {
|
|
@@ -34,10 +36,10 @@ export class JsonlTail {
|
|
|
34
36
|
reset = true;
|
|
35
37
|
}
|
|
36
38
|
if (st.size === this.offset)
|
|
37
|
-
return { lines: [], reset };
|
|
39
|
+
return { lines: [], reset, more: false };
|
|
38
40
|
const fh = await open(this.path, 'r');
|
|
39
41
|
try {
|
|
40
|
-
const len = st.size - this.offset;
|
|
42
|
+
const len = Math.min(st.size - this.offset, maxBytes);
|
|
41
43
|
const buf = Buffer.alloc(len);
|
|
42
44
|
const { bytesRead } = await fh.read(buf, 0, len, this.offset);
|
|
43
45
|
const chunk = Buffer.concat([this.leftover, buf.subarray(0, bytesRead)]);
|
|
@@ -53,7 +55,7 @@ export class JsonlTail {
|
|
|
53
55
|
start = i + 1;
|
|
54
56
|
}
|
|
55
57
|
this.leftover = Buffer.from(chunk.subarray(start));
|
|
56
|
-
return { lines, reset };
|
|
58
|
+
return { lines, reset, more: this.offset < st.size };
|
|
57
59
|
}
|
|
58
60
|
finally {
|
|
59
61
|
await fh.close();
|