claude-inspect 0.1.0 → 0.2.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/README.md +2 -2
- package/dist/app/server/cli/demo.js +4 -0
- package/dist/app/server/cli/service.js +3 -1
- package/dist/app/server/history/search.js +2 -1
- package/dist/app/server/inspector.js +2 -2
- package/dist/app/server/run.js +10 -4
- package/dist/app/server/sources/processes.js +70 -11
- package/dist/app/server/sources/projects.js +9 -4
- package/dist/app/server/sources/spawns.js +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](LICENSE)
|
|
4
4
|
[](https://www.npmjs.com/package/claude-inspect)
|
|
5
|
-

|
|
6
6
|

|
|
7
7
|

|
|
8
8
|

|
|
@@ -69,7 +69,7 @@ becomes hard to tell *who is doing what*. claude-inspect answers:
|
|
|
69
69
|
|
|
70
70
|
## Quick start
|
|
71
71
|
|
|
72
|
-
Requirements: Node.js ≥
|
|
72
|
+
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).
|
|
73
73
|
|
|
74
74
|
```bash
|
|
75
75
|
npx claude-inspect --open # try it once
|
|
@@ -292,6 +292,10 @@ export async function runDemo(opts) {
|
|
|
292
292
|
console.log(`Demo data written to ${ROOT} (no processes started)`);
|
|
293
293
|
return;
|
|
294
294
|
}
|
|
295
|
+
if (process.platform === 'win32') {
|
|
296
|
+
console.error('The live demo starts Unix processes (sh, sleep, pgrep) and needs macOS or Linux. Use `demo --out <dir>` to only generate the data.');
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
295
299
|
// orchestrator = shell that forks the implementer (process tree: implementer → orchestrator)
|
|
296
300
|
const children = [
|
|
297
301
|
spawn('sh', ['-c', 'sleep 7200 & wait'], { stdio: 'ignore', detached: true }),
|
|
@@ -114,13 +114,15 @@ function macStatus() {
|
|
|
114
114
|
// ---------------------------------------------------------------------------
|
|
115
115
|
// Linux (systemd --user)
|
|
116
116
|
const unitPath = join(homedir(), '.config', 'systemd', 'user', 'claude-inspect.service');
|
|
117
|
+
/** systemd-Quoting für ExecStart (Pfade mit Leerzeichen, Anführungszeichen, Backslashes, %). */
|
|
118
|
+
const q = (s) => `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/%/g, '%%')}"`;
|
|
117
119
|
function linuxInstall() {
|
|
118
120
|
mkdirSync(dirname(unitPath), { recursive: true });
|
|
119
121
|
writeFileSync(unitPath, `[Unit]
|
|
120
122
|
Description=claude-inspect – live dashboard for Claude Code agents
|
|
121
123
|
|
|
122
124
|
[Service]
|
|
123
|
-
ExecStart=${nodePath()} ${ENTRY} --port ${port} --no-auth
|
|
125
|
+
ExecStart=${q(nodePath())} ${q(ENTRY)} --port ${port} --no-auth
|
|
124
126
|
WorkingDirectory=${ROOT}
|
|
125
127
|
Restart=on-failure
|
|
126
128
|
RestartSec=10
|
|
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { toolSummary } from '../../shared/tools.js';
|
|
3
3
|
import { paths } from '../config.js';
|
|
4
4
|
import { registry } from '../formats/index.js';
|
|
5
|
+
import { pathTail } from '../sources/projects.js';
|
|
5
6
|
function snippet(text, needle, radius = 90) {
|
|
6
7
|
const i = text.toLowerCase().indexOf(needle);
|
|
7
8
|
if (i < 0)
|
|
@@ -117,7 +118,7 @@ export async function search(query, files, knownSessions, limit = 200, budgetMs
|
|
|
117
118
|
continue;
|
|
118
119
|
hits.push({
|
|
119
120
|
sessionId: p.sessionId ?? '',
|
|
120
|
-
project: p.project
|
|
121
|
+
project: p.project ? pathTail(p.project) : '?',
|
|
121
122
|
title: 'transcript no longer exists',
|
|
122
123
|
line: i,
|
|
123
124
|
kind: 'history-prompt',
|
|
@@ -1,7 +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 { join } from 'node:path';
|
|
4
|
+
import { basename, join } from 'node:path';
|
|
5
5
|
import { CLAUDE_DIR, relToClaude } from './config.js';
|
|
6
6
|
import { registry } from './formats/index.js';
|
|
7
7
|
import { HistoryIndex } from './history/aggregator.js';
|
|
@@ -260,7 +260,7 @@ export class Inspector extends EventEmitter {
|
|
|
260
260
|
* Status und aktuelle Aktivität zu bestimmen (für Dashboard und Detailansicht).
|
|
261
261
|
*/
|
|
262
262
|
async resolveSubagents(sessionPath, files, load) {
|
|
263
|
-
const sessionId = sessionPath
|
|
263
|
+
const sessionId = basename(sessionPath, '.jsonl');
|
|
264
264
|
const out = [];
|
|
265
265
|
for (const sf of files) {
|
|
266
266
|
const info = {
|
package/dist/app/server/run.js
CHANGED
|
@@ -11,6 +11,14 @@ function findWebRoot() {
|
|
|
11
11
|
const candidates = [join(here, '..', '..', 'dist', 'web'), join(here, '..', '..', 'web')];
|
|
12
12
|
return candidates.find((d) => existsSync(join(d, 'index.html'))) ?? candidates[0];
|
|
13
13
|
}
|
|
14
|
+
function openBrowser(url) {
|
|
15
|
+
const [bin, args] = process.platform === 'darwin'
|
|
16
|
+
? ['open', [url]]
|
|
17
|
+
: process.platform === 'win32'
|
|
18
|
+
? ['rundll32', ['url.dll,FileProtocolHandler', url]] // kein cmd/start: "&" in der URL wäre dort ein Befehlstrenner
|
|
19
|
+
: ['xdg-open', [url]];
|
|
20
|
+
execFile(bin, args, () => undefined).on('error', () => undefined);
|
|
21
|
+
}
|
|
14
22
|
export async function runServer(opts) {
|
|
15
23
|
const inspector = new Inspector();
|
|
16
24
|
const t0 = Date.now();
|
|
@@ -24,10 +32,8 @@ export async function runServer(opts) {
|
|
|
24
32
|
const url = noAuth ? `http://localhost:${opts.port}/` : `http://localhost:${opts.port}/?t=${token}`;
|
|
25
33
|
console.log(`claude-inspect is reading ${CLAUDE_DIR} (started in ${Date.now() - t0} ms)`);
|
|
26
34
|
console.log(opts.dev ? `API (dev, no token): ${url} – UI: npm run dev:web` : `Dashboard: ${url}`);
|
|
27
|
-
if (opts.open
|
|
28
|
-
|
|
29
|
-
else if (opts.open && process.platform === 'linux')
|
|
30
|
-
execFile('xdg-open', [url]);
|
|
35
|
+
if (opts.open)
|
|
36
|
+
openBrowser(url);
|
|
31
37
|
const shutdown = () => {
|
|
32
38
|
inspector.stop();
|
|
33
39
|
process.exit(0);
|
|
@@ -4,13 +4,14 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { paths } from '../config.js';
|
|
5
5
|
import { registry } from '../formats/index.js';
|
|
6
6
|
import { readJson } from '../util/jsonl.js';
|
|
7
|
+
const norm = (s) => s.replace(/\s+/g, ' ').trim();
|
|
7
8
|
/**
|
|
8
9
|
* Startzeiten laufender Prozesse per `ps` (rein lesend).
|
|
9
10
|
* TZ=UTC, weil Claude Code `procStart` in UTC schreibt.
|
|
10
11
|
*/
|
|
11
12
|
function procStarts(pids) {
|
|
12
13
|
return new Promise((resolve) => {
|
|
13
|
-
if (pids.length === 0
|
|
14
|
+
if (pids.length === 0)
|
|
14
15
|
return resolve(new Map());
|
|
15
16
|
execFile('ps', ['-o', 'pid=,lstart=', '-p', pids.join(',')], { env: { ...process.env, TZ: 'UTC', LC_ALL: 'C' }, timeout: 5000 }, (_err, stdout) => {
|
|
16
17
|
// ps endet mit Code 1, wenn einzelne PIDs fehlen – die Ausgabe ist trotzdem gültig.
|
|
@@ -25,10 +26,8 @@ function procStarts(pids) {
|
|
|
25
26
|
});
|
|
26
27
|
}
|
|
27
28
|
/** Eltern-PID und Programmname aller Prozesse (`ps -A`), für die Zuordnung per Prozessbaum. */
|
|
28
|
-
function
|
|
29
|
+
function unixTable() {
|
|
29
30
|
return new Promise((resolve) => {
|
|
30
|
-
if (process.platform === 'win32')
|
|
31
|
-
return resolve(new Map());
|
|
32
31
|
execFile('ps', ['-A', '-o', 'pid=,ppid=,comm='], { timeout: 5000, maxBuffer: 8 * 1024 * 1024 }, (_err, stdout) => {
|
|
33
32
|
const out = new Map();
|
|
34
33
|
for (const line of String(stdout ?? '').split('\n')) {
|
|
@@ -40,13 +39,66 @@ function processTable() {
|
|
|
40
39
|
});
|
|
41
40
|
});
|
|
42
41
|
}
|
|
42
|
+
/** Windows: Prozesstabelle inkl. Startzeit per CIM (rein lesend). */
|
|
43
|
+
const WIN_PS = "Get-CimInstance Win32_Process | ForEach-Object { '{0} {1} {2} {3}' -f $_.ProcessId, $_.ParentProcessId, " +
|
|
44
|
+
"$(if ($_.CreationDate) { $_.CreationDate.ToUniversalTime().ToString('o') } else { '-' }), $_.Name }";
|
|
45
|
+
function windowsTable() {
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
execFile('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', WIN_PS], { timeout: 10_000, maxBuffer: 8 * 1024 * 1024, windowsHide: true }, (_err, stdout) => {
|
|
48
|
+
const out = new Map();
|
|
49
|
+
for (const line of String(stdout ?? '').split(/\r?\n/)) {
|
|
50
|
+
const m = /^(\d+) (\d+) (\S+) (.*)$/.exec(line.trim());
|
|
51
|
+
if (m)
|
|
52
|
+
out.set(Number(m[1]), { ppid: Number(m[2]), start: m[3] === '-' ? undefined : m[3], comm: m[4].toLowerCase().replace(/\.exe$/, '') });
|
|
53
|
+
}
|
|
54
|
+
resolve(out);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
// PowerShell zu starten kostet spürbar CPU – die Tabelle wird unter Windows kurz zwischengespeichert.
|
|
59
|
+
let winCache;
|
|
60
|
+
function cachedWindowsTable() {
|
|
61
|
+
if (!winCache || Date.now() - winCache.at > 5000)
|
|
62
|
+
winCache = { at: Date.now(), table: windowsTable() };
|
|
63
|
+
return winCache.table;
|
|
64
|
+
}
|
|
65
|
+
/** Startzeit als Epoch-ms. `ps lstart` ("Mon Sep 28 19:15:43 2026") hat keine Zone und ist UTC. */
|
|
66
|
+
function parseStart(s) {
|
|
67
|
+
if (s === undefined || s === '')
|
|
68
|
+
return undefined;
|
|
69
|
+
if (typeof s === 'number' || /^\d+$/.test(s))
|
|
70
|
+
return Number(s);
|
|
71
|
+
const zoned = /(?:Z|[+-]\d{2}:?\d{2}|UTC|GMT)$/.test(s.trim());
|
|
72
|
+
const t = Date.parse(zoned ? s : `${s} UTC`);
|
|
73
|
+
return Number.isNaN(t) ? undefined : t;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Läuft der Prozess noch, und ist es derselbe (PIDs werden wiederverwendet)?
|
|
77
|
+
* Exakter Textvergleich, wo das Format bekannt ist; sonst Vergleich als Zeitpunkt.
|
|
78
|
+
* Ist `procStart` unbekannt oder unlesbar, reicht die Existenz der PID.
|
|
79
|
+
*/
|
|
80
|
+
export function sameProcess(actual, expected) {
|
|
81
|
+
if (actual === undefined)
|
|
82
|
+
return false;
|
|
83
|
+
if (!expected || norm(actual) === norm(expected))
|
|
84
|
+
return true;
|
|
85
|
+
const a = parseStart(actual);
|
|
86
|
+
const e = parseStart(expected);
|
|
87
|
+
if (a !== undefined && e !== undefined)
|
|
88
|
+
return Math.abs(a - e) <= 2000;
|
|
89
|
+
// Unbekanntes Format: unter Windows (kein ps-Format) zählt die PID, sonst lieber "nicht aktiv".
|
|
90
|
+
return process.platform === 'win32';
|
|
91
|
+
}
|
|
43
92
|
/**
|
|
44
93
|
* Programme, die zwischen aufrufendem Agenten und gestartetem claude liegen dürfen
|
|
45
94
|
* (Bash-Tool → Shell → evtl. Wrapper → claude). Alles andere – etwa der Daemon,
|
|
46
95
|
* der Hintergrund-Jobs forkt – ist keine Aufrufbeziehung.
|
|
47
96
|
*/
|
|
48
|
-
const LAUNCH_INTERMEDIATES = new Set([
|
|
49
|
-
|
|
97
|
+
const LAUNCH_INTERMEDIATES = new Set([
|
|
98
|
+
'sh', 'zsh', 'bash', 'dash', 'fish', '-zsh', '-bash', 'env', 'caffeinate', 'timeout', 'gtimeout', 'nohup', 'time', 'script', 'xargs', 'stdbuf', 'nice', 'sudo',
|
|
99
|
+
// Windows (Namen ohne .exe)
|
|
100
|
+
'cmd', 'powershell', 'pwsh',
|
|
101
|
+
]);
|
|
50
102
|
export async function readProcesses() {
|
|
51
103
|
let names = [];
|
|
52
104
|
try {
|
|
@@ -72,12 +124,19 @@ export async function readProcesses() {
|
|
|
72
124
|
/* halb geschrieben oder gelöscht */
|
|
73
125
|
}
|
|
74
126
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
127
|
+
if (decoded.length === 0)
|
|
128
|
+
return decoded;
|
|
129
|
+
let running;
|
|
130
|
+
let table;
|
|
131
|
+
if (process.platform === 'win32') {
|
|
132
|
+
table = await cachedWindowsTable();
|
|
133
|
+
running = new Map([...table].map(([pid, r]) => [pid, r.start ?? '']));
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
[running, table] = await Promise.all([procStarts(decoded.map((p) => p.pid).filter((p) => p > 0)), unixTable()]);
|
|
80
137
|
}
|
|
138
|
+
for (const p of decoded)
|
|
139
|
+
p.alive = sameProcess(running.get(p.pid), procStart.get(p.pid));
|
|
81
140
|
// Prozessbaum hochlaufen (claude → zsh → claude …) bis zum nächsten bekannten Claude-Prozess.
|
|
82
141
|
const alivePids = new Set(decoded.filter((p) => p.alive).map((p) => p.pid));
|
|
83
142
|
for (const p of decoded) {
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { paths } from '../config.js';
|
|
3
|
+
import { paths, relToClaude } from '../config.js';
|
|
4
4
|
import { registry } from '../formats/index.js';
|
|
5
5
|
import { isObj, num, str } from '../formats/util.js';
|
|
6
6
|
import { readHeadTail } from '../util/jsonl.js';
|
|
7
7
|
import { compareVersions } from '../util/semver.js';
|
|
8
|
+
/** Letztes Segment eines Pfads – egal ob mit "/" (macOS/Linux) oder "\\" (Windows). */
|
|
9
|
+
export function pathTail(p) {
|
|
10
|
+
return p.replace(/[\\/]+$/, '').split(/[\\/]/).pop() || p;
|
|
11
|
+
}
|
|
8
12
|
export function projectName(cwd, projectDir) {
|
|
9
13
|
if (cwd)
|
|
10
|
-
return cwd
|
|
11
|
-
|
|
14
|
+
return pathTail(cwd);
|
|
15
|
+
// kodiertes Home-Verzeichnis: -Users-<name>- (macOS), -home-<name>- (Linux), C--Users-<name>- (Windows)
|
|
16
|
+
return projectDir.replace(/^(?:[A-Za-z]-)?-(?:Users|home)-[^-]+-/, '');
|
|
12
17
|
}
|
|
13
18
|
/**
|
|
14
19
|
* Index über alle Session-Transcripts unter ~/.claude/projects.
|
|
@@ -54,7 +59,7 @@ export class ProjectIndex {
|
|
|
54
59
|
}
|
|
55
60
|
/** Aktualisiert einen einzelnen Eintrag nach einem Watch-Event. */
|
|
56
61
|
async touch(path) {
|
|
57
|
-
const m =
|
|
62
|
+
const m = /^projects\/([^/]+)\/([^/]+)\.jsonl$/.exec(relToClaude(path));
|
|
58
63
|
if (!m)
|
|
59
64
|
return;
|
|
60
65
|
try {
|
|
@@ -120,7 +120,7 @@ export function scoreSpawn(child, call) {
|
|
|
120
120
|
evidence.push('time (rough)');
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
|
-
if (child.cwd && call.cwd && child.cwd.replace(
|
|
123
|
+
if (child.cwd && call.cwd && child.cwd.replace(/[\\/]+$/, '') === call.cwd.replace(/[\\/]+$/, '')) {
|
|
124
124
|
score += 5;
|
|
125
125
|
evidence.push('directory');
|
|
126
126
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-inspect",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Local live dashboard for Claude Code agents: see running sessions, subagents, spawned claude processes, tool calls, timelines, token usage and full history – read passively from ~/.claude, no hooks.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -49,13 +49,14 @@
|
|
|
49
49
|
"service:status": "tsx src/server/cli.ts service status",
|
|
50
50
|
"check": "tsc --noEmit && svelte-check --tsconfig ./tsconfig.web.json",
|
|
51
51
|
"test": "tsx --test \"src/**/*.test.ts\" \"tests/**/*.test.ts\"",
|
|
52
|
-
"test:update": "UPDATE_SNAPSHOTS=1
|
|
52
|
+
"test:update": "node -e \"process.env.UPDATE_SNAPSHOTS='1'; require('node:child_process').execSync('npm test', { stdio: 'inherit' })\"",
|
|
53
|
+
"smoke": "tsx scripts/smoke.ts",
|
|
53
54
|
"snapshot": "tsx src/server/cli-snapshot.ts",
|
|
54
55
|
"fixtures": "tsx scripts/make-fixtures.ts",
|
|
55
56
|
"prepublishOnly": "npm run check && npm test && npm run build"
|
|
56
57
|
},
|
|
57
58
|
"engines": {
|
|
58
|
-
"node": ">=
|
|
59
|
+
"node": ">=22"
|
|
59
60
|
},
|
|
60
61
|
"devDependencies": {
|
|
61
62
|
"@sveltejs/vite-plugin-svelte": "^5.1.1",
|