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,30 @@
1
+ import { readdir } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { paths } from '../config.js';
4
+ import { registry } from '../formats/index.js';
5
+ import { readJson } from '../util/jsonl.js';
6
+ export async function readTasks(sessionId) {
7
+ if (!/^[\w-]+$/.test(sessionId))
8
+ return [];
9
+ const dir = join(paths.tasks, sessionId);
10
+ let names;
11
+ try {
12
+ names = await readdir(dir);
13
+ }
14
+ catch {
15
+ return [];
16
+ }
17
+ const out = [];
18
+ for (const n of names) {
19
+ if (!n.endsWith('.json'))
20
+ continue;
21
+ const file = join(dir, n);
22
+ try {
23
+ out.push(registry.decode('task', await readJson(file), file).out);
24
+ }
25
+ catch {
26
+ /* halb geschrieben */
27
+ }
28
+ }
29
+ return out.sort((a, b) => Number(a.id) - Number(b.id) || a.id.localeCompare(b.id));
30
+ }
@@ -0,0 +1,121 @@
1
+ import { LIMITS } from '../config.js';
2
+ import { registry } from '../formats/index.js';
3
+ import { JsonlTail, readLineAt } from '../util/jsonl.js';
4
+ /** Ein geladenes Transcript (Hauptsession oder Subagent), inkrementell nachgeführt. */
5
+ export class Transcript {
6
+ path;
7
+ sessionId;
8
+ agentId;
9
+ entries = [];
10
+ /** Byte-Offset je Zeilennummer – für die Rohansicht. */
11
+ lineOffsets = new Map();
12
+ tail;
13
+ lastAccess = Date.now();
14
+ constructor(path, sessionId, agentId) {
15
+ this.path = path;
16
+ this.sessionId = sessionId;
17
+ this.agentId = agentId;
18
+ this.tail = new JsonlTail(path);
19
+ }
20
+ get bytes() {
21
+ return this.tail.offset;
22
+ }
23
+ /** Liest neue Zeilen. Gibt die Anzahl neuer Einträge zurück (-1 = komplett neu geladen). */
24
+ async refresh() {
25
+ const { lines, reset } = await this.tail.readNew();
26
+ if (reset) {
27
+ this.entries = [];
28
+ this.lineOffsets.clear();
29
+ }
30
+ const before = this.entries.length;
31
+ for (const l of lines) {
32
+ this.lineOffsets.set(l.line, l.offset);
33
+ let decoded;
34
+ let decoder;
35
+ if (l.error !== undefined) {
36
+ decoded = [{ kind: 'unknown', recordType: '(corrupt line)', raw: l.error }];
37
+ decoder = 'jsonl.parse-error';
38
+ }
39
+ else {
40
+ ({ out: decoded, decoder } = registry.decode('transcript', l.value, this.path, l.line));
41
+ }
42
+ for (const d of decoded) {
43
+ this.entries.push({ ...d, seq: this.entries.length, line: l.line, decoder });
44
+ }
45
+ }
46
+ return reset ? -1 : this.entries.length - before;
47
+ }
48
+ async rawLine(line) {
49
+ const off = this.lineOffsets.get(line);
50
+ if (off === undefined)
51
+ return undefined;
52
+ return readLineAt(this.path, off, LIMITS.maxRawLine);
53
+ }
54
+ }
55
+ /**
56
+ * Hält geladene Transcripts im Speicher (LRU mit Byte-Budget).
57
+ * Nichts wird auf Platte gespeichert.
58
+ */
59
+ export class TranscriptStore {
60
+ loaded = new Map();
61
+ loading = new Map();
62
+ isLoaded(path) {
63
+ return this.loaded.has(path);
64
+ }
65
+ peek(path) {
66
+ return this.loaded.get(path);
67
+ }
68
+ async get(path, sessionId, agentId) {
69
+ const t = this.loaded.get(path);
70
+ if (t) {
71
+ t.lastAccess = Date.now();
72
+ return t;
73
+ }
74
+ let p = this.loading.get(path);
75
+ if (!p) {
76
+ p = (async () => {
77
+ const nt = new Transcript(path, sessionId, agentId);
78
+ await nt.refresh();
79
+ this.loaded.set(path, nt);
80
+ this.evict();
81
+ return nt;
82
+ })().finally(() => this.loading.delete(path));
83
+ this.loading.set(path, p);
84
+ }
85
+ return p;
86
+ }
87
+ /** Nach einer Dateiänderung: nur nachladen, wenn das Transcript bereits im Speicher ist. */
88
+ async refreshIfLoaded(path) {
89
+ const t = this.loaded.get(path);
90
+ if (!t)
91
+ return undefined;
92
+ try {
93
+ const added = await t.refresh();
94
+ return added === 0 ? undefined : { transcript: t, added };
95
+ }
96
+ catch {
97
+ // Datei gelöscht (Claude Code räumt auf) → aus dem Speicher nehmen
98
+ this.loaded.delete(path);
99
+ return undefined;
100
+ }
101
+ }
102
+ all() {
103
+ return [...this.loaded.values()];
104
+ }
105
+ evict() {
106
+ let total = 0;
107
+ for (const t of this.loaded.values())
108
+ total += t.bytes;
109
+ if (total <= LIMITS.transcriptCacheBytes)
110
+ return;
111
+ const byAge = [...this.loaded.values()].sort((a, b) => a.lastAccess - b.lastAccess);
112
+ for (const t of byAge) {
113
+ if (total <= LIMITS.transcriptCacheBytes * 0.8)
114
+ break;
115
+ if (Date.now() - t.lastAccess < 60_000)
116
+ continue; // gerade benutzt
117
+ this.loaded.delete(t.path);
118
+ total -= t.bytes;
119
+ }
120
+ }
121
+ }
@@ -0,0 +1,124 @@
1
+ import { open, stat } from 'node:fs/promises';
2
+ function parseLine(text, line, offset) {
3
+ try {
4
+ return { line, offset, value: JSON.parse(text) };
5
+ }
6
+ catch (e) {
7
+ return { line, offset, error: e.message };
8
+ }
9
+ }
10
+ /**
11
+ * Liest eine wachsende JSONL-Datei inkrementell: Jeder Aufruf von readNew()
12
+ * liefert nur die seitdem vollständig geschriebenen Zeilen.
13
+ */
14
+ export class JsonlTail {
15
+ path;
16
+ offset = 0;
17
+ lineNo = 0;
18
+ leftover = Buffer.alloc(0);
19
+ constructor(path) {
20
+ this.path = path;
21
+ }
22
+ /**
23
+ * @param prefilter Optional: nur Zeilen parsen, deren Rohtext passt (spart bei
24
+ * großen Dateien den Großteil der JSON.parse-Arbeit).
25
+ */
26
+ async readNew(prefilter) {
27
+ const st = await stat(this.path);
28
+ let reset = false;
29
+ if (st.size < this.offset) {
30
+ // Datei wurde gekürzt oder ersetzt → von vorn.
31
+ this.offset = 0;
32
+ this.lineNo = 0;
33
+ this.leftover = Buffer.alloc(0);
34
+ reset = true;
35
+ }
36
+ if (st.size === this.offset)
37
+ return { lines: [], reset };
38
+ const fh = await open(this.path, 'r');
39
+ try {
40
+ const len = st.size - this.offset;
41
+ const buf = Buffer.alloc(len);
42
+ const { bytesRead } = await fh.read(buf, 0, len, this.offset);
43
+ const chunk = Buffer.concat([this.leftover, buf.subarray(0, bytesRead)]);
44
+ const chunkStart = this.offset - this.leftover.length;
45
+ this.offset += bytesRead;
46
+ const lines = [];
47
+ let start = 0;
48
+ for (let i = chunk.indexOf(10); i !== -1; i = chunk.indexOf(10, start)) {
49
+ const text = chunk.toString('utf8', start, i);
50
+ if (text.trim() && (!prefilter || prefilter(text)))
51
+ lines.push(parseLine(text, this.lineNo, chunkStart + start));
52
+ this.lineNo++;
53
+ start = i + 1;
54
+ }
55
+ this.leftover = Buffer.from(chunk.subarray(start));
56
+ return { lines, reset };
57
+ }
58
+ finally {
59
+ await fh.close();
60
+ }
61
+ }
62
+ }
63
+ /** Liest nur Anfang und Ende einer Datei – für schnelle Zusammenfassungen. */
64
+ export async function readHeadTail(path, headBytes, tailBytes) {
65
+ const st = await stat(path);
66
+ const fh = await open(path, 'r');
67
+ try {
68
+ const parse = (buf, dropFirst, dropLast) => {
69
+ const parts = buf.toString('utf8').split('\n');
70
+ if (dropFirst)
71
+ parts.shift();
72
+ if (dropLast)
73
+ parts.pop();
74
+ const out = [];
75
+ for (const p of parts) {
76
+ if (!p.trim())
77
+ continue;
78
+ try {
79
+ out.push(JSON.parse(p));
80
+ }
81
+ catch {
82
+ /* angeschnittene Zeile */
83
+ }
84
+ }
85
+ return out;
86
+ };
87
+ if (st.size <= headBytes + tailBytes) {
88
+ const buf = Buffer.alloc(st.size);
89
+ await fh.read(buf, 0, st.size, 0);
90
+ const all = parse(buf, false, false);
91
+ return { head: all, tail: all, size: st.size };
92
+ }
93
+ const hb = Buffer.alloc(headBytes);
94
+ await fh.read(hb, 0, headBytes, 0);
95
+ const tb = Buffer.alloc(tailBytes);
96
+ await fh.read(tb, 0, tailBytes, st.size - tailBytes);
97
+ return { head: parse(hb, false, true), tail: parse(tb, true, false), size: st.size };
98
+ }
99
+ finally {
100
+ await fh.close();
101
+ }
102
+ }
103
+ /** Liest eine einzelne Zeile ab einem bekannten Byte-Offset. */
104
+ export async function readLineAt(path, offset, maxBytes) {
105
+ const fh = await open(path, 'r');
106
+ try {
107
+ const buf = Buffer.alloc(maxBytes);
108
+ const { bytesRead } = await fh.read(buf, 0, maxBytes, offset);
109
+ const end = buf.subarray(0, bytesRead).indexOf(10);
110
+ return buf.toString('utf8', 0, end === -1 ? bytesRead : end);
111
+ }
112
+ finally {
113
+ await fh.close();
114
+ }
115
+ }
116
+ export async function readJson(path) {
117
+ const fh = await open(path, 'r');
118
+ try {
119
+ return JSON.parse(await fh.readFile('utf8'));
120
+ }
121
+ finally {
122
+ await fh.close();
123
+ }
124
+ }
@@ -0,0 +1,42 @@
1
+ import { LIMITS } from '../config.js';
2
+ const SECRET_KEY = /(token|secret|password|passwd|api[_-]?key|authorization|cookie|credential)/i;
3
+ function isSecretKey(key) {
4
+ // Token-*Zähler* (input_tokens, totalTokens …) sind Zahlen und bleiben sichtbar.
5
+ return SECRET_KEY.test(key);
6
+ }
7
+ /**
8
+ * Tiefe Kopie, die
9
+ * - String-Werte unter geheimnisverdächtigen Schlüsseln maskiert,
10
+ * - Base64-Bilddaten durch einen Platzhalter ersetzt,
11
+ * - zu lange Strings kürzt.
12
+ */
13
+ export function sanitize(value, opts = {}, key = '') {
14
+ const max = opts.maxString ?? LIMITS.maxString;
15
+ if (typeof value === 'string') {
16
+ if (key && isSecretKey(key))
17
+ return value ? '••••••' : value;
18
+ if ((key === 'base64' || key === 'data') && value.length > 256 && /^[A-Za-z0-9+/=\s]+$/.test(value.slice(0, 256))) {
19
+ return `[base64, ${value.length} characters omitted]`;
20
+ }
21
+ if (value.length > max)
22
+ return value.slice(0, max) + `\n… [${value.length - max} characters truncated]`;
23
+ return value;
24
+ }
25
+ if (Array.isArray(value))
26
+ return value.map((v) => sanitize(v, opts));
27
+ if (value && typeof value === 'object') {
28
+ const out = {};
29
+ for (const [k, v] of Object.entries(value)) {
30
+ if (opts.dropKeys?.has(k)) {
31
+ out[k] = typeof v === 'string' ? `[${v.length} characters omitted]` : v === null ? null : '[omitted]';
32
+ continue;
33
+ }
34
+ out[k] = sanitize(v, opts, k);
35
+ }
36
+ return out;
37
+ }
38
+ return value;
39
+ }
40
+ export function clip(s, max = LIMITS.maxString) {
41
+ return s.length > max ? { text: s.slice(0, max), truncated: s.length - max } : { text: s };
42
+ }
@@ -0,0 +1,33 @@
1
+ /** Minimaler Versionsvergleich für "2.1.283"-artige Strings. */
2
+ export function compareVersions(a, b) {
3
+ const pa = a.split(/[.-]/).map((x) => parseInt(x, 10));
4
+ const pb = b.split(/[.-]/).map((x) => parseInt(x, 10));
5
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
6
+ const x = Number.isNaN(pa[i]) ? 0 : (pa[i] ?? 0);
7
+ const y = Number.isNaN(pb[i]) ? 0 : (pb[i] ?? 0);
8
+ if (x !== y)
9
+ return x < y ? -1 : 1;
10
+ }
11
+ return 0;
12
+ }
13
+ /**
14
+ * Prüft Bereiche wie ">=2.1.0", "<2.2.0", ">=2.1.0 <2.2.0" oder "*".
15
+ * Ein fehlendes `version` passt auf jeden Bereich (ältere Records hatten nicht immer eins).
16
+ */
17
+ export function inRange(version, range) {
18
+ if (!range || range === '*' || !version)
19
+ return true;
20
+ return range.split(/\s+/).every((part) => {
21
+ const m = /^(>=|<=|>|<|=)?(.+)$/.exec(part);
22
+ if (!m)
23
+ return true;
24
+ const c = compareVersions(version, m[2]);
25
+ switch (m[1]) {
26
+ case '>=': return c >= 0;
27
+ case '<=': return c <= 0;
28
+ case '>': return c > 0;
29
+ case '<': return c < 0;
30
+ default: return c === 0;
31
+ }
32
+ });
33
+ }
@@ -0,0 +1,57 @@
1
+ const PRICES = [
2
+ // Reihenfolge: spezifischere Präfixe zuerst
3
+ ['claude-fable-5-1', { input: 10, output: 50, cacheRead: 0.25 }],
4
+ ['claude-mythos-5-1', { input: 10, output: 50, cacheRead: 0.25 }],
5
+ ['claude-fable-5', { input: 10, output: 50 }],
6
+ ['claude-mythos-5', { input: 10, output: 50 }],
7
+ ['claude-opus-5-5', { input: 4, output: 20, cacheRead: 0.2 }],
8
+ ['claude-opus-5', { input: 5, output: 25 }],
9
+ ['claude-opus-4-8', { input: 5, output: 25 }],
10
+ ['claude-opus-4-7', { input: 5, output: 25 }],
11
+ ['claude-opus-4-6', { input: 5, output: 25 }],
12
+ ['claude-opus-4-5', { input: 5, output: 25 }],
13
+ ['claude-sonnet-5', { input: 2, output: 10 }],
14
+ ['claude-sonnet-4', { input: 3, output: 15 }],
15
+ ['claude-haiku-4-5', { input: 1, output: 5 }],
16
+ ];
17
+ export function priceFor(model) {
18
+ if (!model)
19
+ return undefined;
20
+ return PRICES.find(([p]) => model === p || model.startsWith(p + '-') || model.startsWith(p + '['))?.[1];
21
+ }
22
+ export const emptyTotals = () => ({ input: 0, output: 0, cacheRead: 0, cacheWrite5m: 0, cacheWrite1h: 0, requests: 0 });
23
+ export function addUsage(t, u) {
24
+ t.input += u.input_tokens ?? 0;
25
+ t.output += u.output_tokens ?? 0;
26
+ t.cacheRead += u.cache_read_input_tokens ?? 0;
27
+ const cc = u.cache_creation;
28
+ if (cc && (cc.ephemeral_1h_input_tokens !== undefined || cc.ephemeral_5m_input_tokens !== undefined)) {
29
+ t.cacheWrite5m += cc.ephemeral_5m_input_tokens ?? 0;
30
+ t.cacheWrite1h += cc.ephemeral_1h_input_tokens ?? 0;
31
+ }
32
+ else {
33
+ t.cacheWrite5m += u.cache_creation_input_tokens ?? 0;
34
+ }
35
+ t.requests++;
36
+ }
37
+ export function mergeTotals(a, b) {
38
+ a.input += b.input;
39
+ a.output += b.output;
40
+ a.cacheRead += b.cacheRead;
41
+ a.cacheWrite5m += b.cacheWrite5m;
42
+ a.cacheWrite1h += b.cacheWrite1h;
43
+ a.requests += b.requests;
44
+ }
45
+ /** Geschätzte Kosten in USD, oder undefined bei unbekanntem Modell. */
46
+ export function estimateCost(model, t) {
47
+ const p = priceFor(model);
48
+ if (!p)
49
+ return undefined;
50
+ const M = 1_000_000;
51
+ return ((t.input * p.input +
52
+ t.output * p.output +
53
+ t.cacheRead * (p.cacheRead ?? p.input * 0.1) +
54
+ t.cacheWrite5m * p.input * 1.25 +
55
+ t.cacheWrite1h * p.input * 2) /
56
+ M);
57
+ }
@@ -0,0 +1,62 @@
1
+ // Kurzbeschreibung eines Tool-Aufrufs für Listen und Statuszeilen.
2
+ function str(v) {
3
+ return typeof v === 'string' && v.length > 0 ? v : undefined;
4
+ }
5
+ export function basename(p) {
6
+ const i = p.replace(/\/+$/, '').lastIndexOf('/');
7
+ return i >= 0 ? p.slice(i + 1) : p;
8
+ }
9
+ function firstLine(s, max = 120) {
10
+ const line = s.split('\n', 1)[0] ?? '';
11
+ return line.length > max ? line.slice(0, max - 1) + '…' : line;
12
+ }
13
+ /** "mcp__claude-in-chrome__navigate" → { server: "claude-in-chrome", tool: "navigate" } */
14
+ export function splitMcpName(name) {
15
+ const m = /^mcp_{1,2}(.+?)__(.+)$/.exec(name);
16
+ return m ? { server: m[1], tool: m[2] } : { tool: name };
17
+ }
18
+ export function toolSummary(name, input) {
19
+ const i = input ?? {};
20
+ switch (name) {
21
+ case 'Bash':
22
+ return str(i.description) ?? firstLine(str(i.command) ?? '');
23
+ case 'Read':
24
+ case 'Write':
25
+ case 'Edit':
26
+ case 'MultiEdit':
27
+ case 'NotebookEdit':
28
+ return basename(str(i.file_path) ?? str(i.notebook_path) ?? '');
29
+ case 'Grep':
30
+ case 'Glob':
31
+ return (str(i.pattern) ?? '') + (str(i.path) ? ` in ${basename(str(i.path))}` : '');
32
+ case 'Agent':
33
+ case 'Task':
34
+ return [str(i.subagent_type), str(i.description)].filter(Boolean).join(': ');
35
+ case 'WebFetch':
36
+ return str(i.url) ?? '';
37
+ case 'WebSearch':
38
+ return str(i.query) ?? '';
39
+ case 'Skill':
40
+ return str(i.skill) ?? '';
41
+ case 'SendMessage':
42
+ return `→ ${str(i.to) ?? '?'}`;
43
+ case 'ToolSearch':
44
+ return str(i.query) ?? '';
45
+ case 'ScheduleWakeup':
46
+ return str(i.reason) ?? '';
47
+ case 'TaskCreate':
48
+ case 'TaskUpdate':
49
+ return str(i.subject) ?? str(i.taskId) ?? '';
50
+ case 'AskUserQuestion': {
51
+ const q = Array.isArray(i.questions) ? i.questions[0] : undefined;
52
+ return firstLine(str(q?.question) ?? '');
53
+ }
54
+ }
55
+ for (const k of ['description', 'command', 'url', 'query', 'path', 'file_path', 'prompt', 'action', 'name']) {
56
+ const v = str(i[k]);
57
+ if (v)
58
+ return firstLine(v);
59
+ }
60
+ const keys = Object.keys(i);
61
+ return keys.length ? keys.slice(0, 3).join(', ') : '';
62
+ }
@@ -0,0 +1,3 @@
1
+ // Normiertes Domain-Model. Wird von Server und Frontend gemeinsam genutzt.
2
+ // Die Decoder in src/server/formats übersetzen die Rohformate von Claude Code in diese Typen.
3
+ export {};