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,170 @@
1
+ /**
2
+ * Autostart for claude-inspect (runs the compiled build with plain node).
3
+ *
4
+ * claude-inspect service install [--port 47717] install + start (login autostart, restart on crash)
5
+ * claude-inspect service uninstall stop + remove
6
+ * claude-inspect service status show state and URL
7
+ *
8
+ * macOS: LaunchAgent in ~/Library/LaunchAgents, log in ~/Library/Logs/claude-inspect.log
9
+ * Linux: systemd user unit in ~/.config/systemd/user, log via `journalctl --user -u claude-inspect`
10
+ *
11
+ * The service runs without access token (--no-auth): it only listens on 127.0.0.1 and
12
+ * rejects requests with a foreign Host header, so a plain bookmark to http://localhost:<port> works.
13
+ */
14
+ import { execFileSync } from 'node:child_process';
15
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
16
+ import { homedir, userInfo } from 'node:os';
17
+ import { dirname, join, resolve, sep } from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+ const HERE = dirname(fileURLToPath(import.meta.url));
20
+ /**
21
+ * The service always runs the compiled cli.js: next to this file in a build/npm install
22
+ * (…/dist/app/server/cli), or the project's dist when started from the sources via tsx.
23
+ */
24
+ const FROM_SOURCES = HERE.split(sep).slice(-3).join('/') === 'src/server/cli';
25
+ const ROOT = FROM_SOURCES ? resolve(HERE, '..', '..', '..') : resolve(HERE, '..', '..', '..', '..');
26
+ const ENTRY = FROM_SOURCES ? join(ROOT, 'dist', 'app', 'server', 'cli.js') : resolve(HERE, '..', 'cli.js');
27
+ const WEB = FROM_SOURCES ? join(ROOT, 'dist', 'web') : resolve(HERE, '..', '..', '..', 'web');
28
+ const LABEL = 'com.github.claude-inspect';
29
+ let port = 47717;
30
+ let URL = `http://localhost:${port}/`;
31
+ function run(bin, args, quiet = false) {
32
+ try {
33
+ return execFileSync(bin, args, { stdio: ['ignore', 'pipe', quiet ? 'ignore' : 'inherit'] }).toString();
34
+ }
35
+ catch (e) {
36
+ if (quiet)
37
+ return '';
38
+ throw e;
39
+ }
40
+ }
41
+ /** Stable node path (e.g. /opt/homebrew/bin/node instead of a versioned Cellar path). */
42
+ function nodePath() {
43
+ const found = run('sh', ['-c', 'command -v node'], true).trim();
44
+ return found || process.execPath;
45
+ }
46
+ function requireBuild() {
47
+ if (!existsSync(ENTRY) || !existsSync(join(WEB, 'index.html'))) {
48
+ console.error(`No build found at ${ENTRY} – run \`npm run build\` first.`);
49
+ process.exit(1);
50
+ }
51
+ }
52
+ // ---------------------------------------------------------------------------
53
+ // macOS (launchd)
54
+ const plistPath = join(homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`);
55
+ const logPath = join(homedir(), 'Library', 'Logs', 'claude-inspect.log');
56
+ const domain = `gui/${userInfo().uid}`;
57
+ function macInstall() {
58
+ const esc = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;');
59
+ const args = [nodePath(), ENTRY, '--port', String(port), '--no-auth'];
60
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
61
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
62
+ <plist version="1.0">
63
+ <dict>
64
+ <key>Label</key><string>${LABEL}</string>
65
+ <key>ProgramArguments</key>
66
+ <array>
67
+ ${args.map((a) => ` <string>${esc(a)}</string>`).join('\n')}
68
+ </array>
69
+ <key>WorkingDirectory</key><string>${esc(ROOT)}</string>
70
+ <key>EnvironmentVariables</key>
71
+ <dict>
72
+ <key>PATH</key><string>/usr/bin:/bin:/usr/sbin:/sbin</string>
73
+ </dict>
74
+ <key>RunAtLoad</key><true/>
75
+ <key>KeepAlive</key><true/>
76
+ <key>ThrottleInterval</key><integer>10</integer>
77
+ <key>ProcessType</key><string>Background</string>
78
+ <key>StandardOutPath</key><string>${esc(logPath)}</string>
79
+ <key>StandardErrorPath</key><string>${esc(logPath)}</string>
80
+ </dict>
81
+ </plist>
82
+ `;
83
+ mkdirSync(dirname(plistPath), { recursive: true });
84
+ mkdirSync(dirname(logPath), { recursive: true });
85
+ run('launchctl', ['bootout', `${domain}/${LABEL}`], true); // replace an older install
86
+ writeFileSync(plistPath, plist);
87
+ // bootout finishes asynchronously; bootstrap fails with "Input/output error" until it has.
88
+ for (let attempt = 1;; attempt++) {
89
+ try {
90
+ execFileSync('launchctl', ['bootstrap', domain, plistPath], { stdio: 'pipe' });
91
+ break;
92
+ }
93
+ catch (e) {
94
+ if (attempt >= 10)
95
+ throw e;
96
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 500);
97
+ }
98
+ }
99
+ console.log(`Installed LaunchAgent ${plistPath}\nLog: ${logPath}`);
100
+ }
101
+ function macUninstall() {
102
+ run('launchctl', ['bootout', `${domain}/${LABEL}`], true);
103
+ rmSync(plistPath, { force: true });
104
+ console.log('LaunchAgent removed.');
105
+ }
106
+ function macStatus() {
107
+ const out = run('launchctl', ['print', `${domain}/${LABEL}`], true);
108
+ if (!out)
109
+ return console.log('Not installed.');
110
+ const state = /state = (\w+)/.exec(out)?.[1] ?? '?';
111
+ const pid = /pid = (\d+)/.exec(out)?.[1];
112
+ console.log(`State: ${state}${pid ? ` (pid ${pid})` : ''}\nPlist: ${plistPath}\nLog: ${logPath}`);
113
+ }
114
+ // ---------------------------------------------------------------------------
115
+ // Linux (systemd --user)
116
+ const unitPath = join(homedir(), '.config', 'systemd', 'user', 'claude-inspect.service');
117
+ function linuxInstall() {
118
+ mkdirSync(dirname(unitPath), { recursive: true });
119
+ writeFileSync(unitPath, `[Unit]
120
+ Description=claude-inspect – live dashboard for Claude Code agents
121
+
122
+ [Service]
123
+ ExecStart=${nodePath()} ${ENTRY} --port ${port} --no-auth
124
+ WorkingDirectory=${ROOT}
125
+ Restart=on-failure
126
+ RestartSec=10
127
+
128
+ [Install]
129
+ WantedBy=default.target
130
+ `);
131
+ run('systemctl', ['--user', 'daemon-reload']);
132
+ run('systemctl', ['--user', 'enable', '--now', 'claude-inspect.service']);
133
+ console.log(`Installed ${unitPath}\nLog: journalctl --user -u claude-inspect`);
134
+ }
135
+ function linuxUninstall() {
136
+ run('systemctl', ['--user', 'disable', '--now', 'claude-inspect.service'], true);
137
+ rmSync(unitPath, { force: true });
138
+ run('systemctl', ['--user', 'daemon-reload'], true);
139
+ console.log('Service removed.');
140
+ }
141
+ function linuxStatus() {
142
+ console.log(run('systemctl', ['--user', 'status', 'claude-inspect.service', '--no-pager'], true) || 'Not installed.');
143
+ }
144
+ // ---------------------------------------------------------------------------
145
+ export function service(cmd, servicePort) {
146
+ port = servicePort;
147
+ URL = `http://localhost:${port}/`;
148
+ const mac = process.platform === 'darwin';
149
+ if (!mac && process.platform !== 'linux') {
150
+ console.error('Autostart is only supported on macOS and Linux.');
151
+ process.exit(1);
152
+ }
153
+ switch (cmd) {
154
+ case 'install':
155
+ requireBuild();
156
+ (mac ? macInstall : linuxInstall)();
157
+ console.log(`\nDashboard: ${URL} (bookmark it – no token needed)`);
158
+ break;
159
+ case 'uninstall':
160
+ (mac ? macUninstall : linuxUninstall)();
161
+ break;
162
+ case 'status':
163
+ (mac ? macStatus : linuxStatus)();
164
+ console.log(`URL: ${URL}`);
165
+ break;
166
+ default:
167
+ console.error('Usage: claude-inspect service install|uninstall|status [--port 47717]');
168
+ process.exit(1);
169
+ }
170
+ }
@@ -0,0 +1,34 @@
1
+ // Terminal-Ausgabe "laufende Agenten + letzte Aktion" – nützlich zum Testen der Parser.
2
+ import { Inspector } from './inspector.js';
3
+ const inspector = new Inspector();
4
+ await inspector.start();
5
+ // Komplettscan nach claude-Aufrufen abwarten
6
+ await inspector.spawns.refresh(inspector.index.list(), (await inspector.getDashboard()).processes.map((p) => p.process), true);
7
+ const d = await inspector.getDashboard();
8
+ inspector.stop();
9
+ const ago = (iso) => (iso ? `${Math.round((Date.now() - Date.parse(iso)) / 1000)}s` : '?');
10
+ for (const dp of d.processes) {
11
+ const { process: p, job, session: s } = dp;
12
+ const mark = p.alive ? (p.status === 'idle' ? '○' : '●') : '✕';
13
+ console.log(`${mark} ${p.name ?? s?.title ?? p.sessionId.slice(0, 8)} [${p.kind ?? '?'}/${p.status ?? '?'}] pid ${p.pid} ${p.cwd ?? ''} v${p.version ?? '?'}`);
14
+ if (dp.spawnedBy)
15
+ console.log(` ⇠ started by ${dp.spawnedBy.parentTitle ?? dp.spawnedBy.parentSessionId.slice(0, 8)} [${dp.spawnedBy.confidence}: ${dp.spawnedBy.evidence.join(', ')}]`);
16
+ if (job)
17
+ console.log(` job ${job.short}: ${job.state} – ${job.detail ?? ''}`);
18
+ if (s) {
19
+ console.log(` ${s.model ?? '?'} · ctx ${s.contextTokens ?? '?'} · ${s.toolCalls} tools · ${s.current?.kind}: ${s.current?.label ?? ''} (${ago(s.current?.since)} ago)`);
20
+ for (const t of s.pendingTools)
21
+ console.log(` ⏳ ${t.name}: ${t.summary}`);
22
+ for (const c of s.spawned)
23
+ console.log(` ⇢ claude ${c.agent ?? ''} ${c.childSessionId.slice(0, 8)} ${c.status} [${c.confidence}: ${c.evidence.join(', ')}]`);
24
+ for (const a of s.subagents)
25
+ console.log(` ↳ ${a.agentType ?? 'agent'} "${a.description ?? ''}" ${a.status}${a.current ? ' – ' + a.current.label : ''}`);
26
+ }
27
+ }
28
+ if (d.orphanJobs.length)
29
+ console.log(`\n${d.orphanJobs.length} jobs without a process`);
30
+ const { decoders, drift } = inspector.formats();
31
+ console.log(`\nDecoder hits: ${decoders.filter((x) => x.hits).map((x) => `${x.id}=${x.hits}`).join(', ')}`);
32
+ console.log(`Drift entries: ${drift.length}`);
33
+ for (const x of drift.slice(0, 15))
34
+ console.log(` ${x.source}/${x.recordType}.${x.field} ×${x.count} (${x.firstVersion ?? '?'}–${x.lastVersion ?? '?'})`);
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * claude-inspect command line.
4
+ *
5
+ * claude-inspect [--port 7717] [--open] [--token T | --no-auth] start the dashboard
6
+ * claude-inspect service install|uninstall|status [--port 47717] autostart (macOS/Linux)
7
+ * claude-inspect demo [--port 7718] [--out DIR] run on fictional demo data
8
+ *
9
+ * Everything is imported lazily: the demo must set CLAUDE_CONFIG_DIR before config.ts is loaded.
10
+ */
11
+ import { readFileSync } from 'node:fs';
12
+ import { dirname, join } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { parseArgs } from 'node:util';
15
+ const env = process.env;
16
+ function version() {
17
+ const here = dirname(fileURLToPath(import.meta.url));
18
+ for (const p of [join(here, '..', '..', 'package.json'), join(here, '..', '..', '..', 'package.json')]) {
19
+ try {
20
+ const pkg = JSON.parse(readFileSync(p, 'utf8'));
21
+ if (pkg.name === 'claude-inspect')
22
+ return pkg.version;
23
+ }
24
+ catch {
25
+ /* next candidate */
26
+ }
27
+ }
28
+ return '?';
29
+ }
30
+ const HELP = `claude-inspect ${version()} – live dashboard & history viewer for Claude Code agents
31
+
32
+ Usage:
33
+ claude-inspect [options] start the dashboard (reads ~/.claude)
34
+ claude-inspect service install start automatically at login (port 47717, no token)
35
+ claude-inspect service uninstall|status
36
+ claude-inspect demo try it on fictional demo data
37
+
38
+ Options:
39
+ --port <n> port (default 7717; service 47717; demo 7718) env: CLAUDE_INSPECT_PORT
40
+ --open open the browser
41
+ --token <t> fixed access token instead of a random one env: CLAUDE_INSPECT_TOKEN
42
+ --no-auth no token (still only reachable from localhost) env: CLAUDE_INSPECT_NO_AUTH=1
43
+ --out <dir> demo: only write the demo data to <dir>
44
+ -v, --version print the version
45
+ -h, --help show this help
46
+
47
+ Data directory: $CLAUDE_CONFIG_DIR or ~/.claude`;
48
+ const { values, positionals } = parseArgs({
49
+ allowPositionals: true,
50
+ options: {
51
+ port: { type: 'string' },
52
+ open: { type: 'boolean', default: false },
53
+ dev: { type: 'boolean', default: false },
54
+ token: { type: 'string', default: env.CLAUDE_INSPECT_TOKEN },
55
+ 'no-auth': { type: 'boolean', default: env.CLAUDE_INSPECT_NO_AUTH === '1' },
56
+ out: { type: 'string' },
57
+ help: { type: 'boolean', short: 'h', default: false },
58
+ version: { type: 'boolean', short: 'v', default: false },
59
+ },
60
+ });
61
+ const [cmd, sub] = positionals;
62
+ const port = (fallback) => Number(values.port ?? env.CLAUDE_INSPECT_PORT ?? fallback);
63
+ if (values.help) {
64
+ console.log(HELP);
65
+ }
66
+ else if (values.version) {
67
+ console.log(version());
68
+ }
69
+ else if (cmd === 'service') {
70
+ const { service } = await import('./cli/service.js');
71
+ service(sub ?? 'status', Number(values.port ?? 47717));
72
+ }
73
+ else if (cmd === 'demo') {
74
+ const { runDemo } = await import('./cli/demo.js');
75
+ await runDemo({ out: values.out, port: port(7718), open: values.open });
76
+ }
77
+ else if (cmd === undefined) {
78
+ const { runServer } = await import('./run.js');
79
+ await runServer({ port: port(7717), open: values.open, dev: values.dev, noAuth: values['no-auth'], token: values.token });
80
+ }
81
+ else {
82
+ console.error(`Unknown command "${cmd}".\n\n${HELP}`);
83
+ process.exit(1);
84
+ }
@@ -0,0 +1,41 @@
1
+ import { homedir } from 'node:os';
2
+ import { join, relative, sep } from 'node:path';
3
+ export const CLAUDE_DIR = process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude');
4
+ export const paths = {
5
+ projects: join(CLAUDE_DIR, 'projects'),
6
+ sessions: join(CLAUDE_DIR, 'sessions'),
7
+ jobs: join(CLAUDE_DIR, 'jobs'),
8
+ daemonRoster: join(CLAUDE_DIR, 'daemon', 'roster.json'),
9
+ tasks: join(CLAUDE_DIR, 'tasks'),
10
+ history: join(CLAUDE_DIR, 'history.jsonl'),
11
+ };
12
+ /**
13
+ * Dateien mit Zugangsdaten. Werden nie gelesen – auch nicht über die Rohansicht.
14
+ * Pfade relativ zu CLAUDE_DIR, mit "/" als Trenner.
15
+ */
16
+ const DENY = [
17
+ /\.key$/,
18
+ /^daemon\/(control\.key|auth\/)/,
19
+ /^ide\//,
20
+ /credentials/i,
21
+ /^session-env\//,
22
+ /^shell-snapshots\//,
23
+ ];
24
+ export function relToClaude(abs) {
25
+ return relative(CLAUDE_DIR, abs).split(sep).join('/');
26
+ }
27
+ export function isDenied(abs) {
28
+ const rel = relToClaude(abs);
29
+ if (rel.startsWith('..'))
30
+ return true;
31
+ return DENY.some((r) => r.test(rel));
32
+ }
33
+ /** Limits für die Datenmenge, die ans Frontend geht. */
34
+ export const LIMITS = {
35
+ /** Maximale Länge eines Strings in Einträgen, bevor gekürzt wird. */
36
+ maxString: 24_000,
37
+ /** Arbeitsspeicher-Budget für geladene Transcripts (Bytes der Quelldateien). */
38
+ transcriptCacheBytes: 400 * 1024 * 1024,
39
+ /** Rohansicht einer Zeile. */
40
+ maxRawLine: 4 * 1024 * 1024,
41
+ };
@@ -0,0 +1,17 @@
1
+ import { num, str } from '../util.js';
2
+ /** ~/.claude/history.jsonl – jede Nutzereingabe über alle Projekte. */
3
+ export const historyPromptDecoder = {
4
+ id: 'history.prompt@1',
5
+ source: 'history',
6
+ description: 'prompt history (prompt, project, session, time)',
7
+ match: (raw) => (typeof raw.display === 'string' ? 10 : 0),
8
+ decode: (raw) => ({ display: str(raw.display) ?? '', timestamp: num(raw.timestamp), project: str(raw.project), sessionId: str(raw.sessionId) }),
9
+ knownFields: () => ['display', 'timestamp', 'project', 'sessionId', 'pastedContents'],
10
+ };
11
+ export const historyFallback = {
12
+ id: 'history.fallback',
13
+ source: 'history',
14
+ description: 'unknown history entry',
15
+ match: () => 1,
16
+ decode: (raw) => ({ display: JSON.stringify(raw).slice(0, 200) }),
17
+ };
@@ -0,0 +1,35 @@
1
+ import { jobStateDecoder, jobStateFallback, jobTimelineDecoder, jobTimelineFallback, rosterDecoder, rosterFallback } from './job/job.js';
2
+ import { processDecoderV1, processDecoderV2, processFallback } from './process/session-file.js';
3
+ import { FormatRegistry } from './registry.js';
4
+ import { historyFallback, historyPromptDecoder } from './history/prompt.js';
5
+ import { subagentMetaDecoder, subagentMetaFallback } from './subagent/meta.js';
6
+ import { taskDecoder, taskFallback } from './task/task.js';
7
+ import { messageDecoder } from './transcript/message.js';
8
+ import { attachmentDecoder, metaDecoder, systemDecoder, transcriptFallback } from './transcript/other.js';
9
+ /** Zentrale Registry. Neue Decoder hier eintragen. */
10
+ export const registry = new FormatRegistry()
11
+ // Transcripts
12
+ .register(messageDecoder)
13
+ .register(systemDecoder)
14
+ .register(attachmentDecoder)
15
+ .register(metaDecoder)
16
+ .register(transcriptFallback)
17
+ // Prozesse
18
+ .register(processDecoderV2)
19
+ .register(processDecoderV1)
20
+ .register(processFallback)
21
+ // Jobs & Daemon
22
+ .register(jobStateDecoder)
23
+ .register(jobStateFallback)
24
+ .register(jobTimelineDecoder)
25
+ .register(jobTimelineFallback)
26
+ .register(rosterDecoder)
27
+ .register(rosterFallback)
28
+ // Subagenten
29
+ .register(subagentMetaDecoder)
30
+ .register(subagentMetaFallback)
31
+ // Tasks & Eingabe-Historie
32
+ .register(taskDecoder)
33
+ .register(taskFallback)
34
+ .register(historyPromptDecoder)
35
+ .register(historyFallback);
@@ -0,0 +1,132 @@
1
+ import { sanitize } from '../../util/sanitize.js';
2
+ import { arr, extraFields, isObj, num, obj, str } from '../util.js';
3
+ const STATE_FIELDS = [
4
+ 'state',
5
+ 'detail',
6
+ 'tempo',
7
+ 'inFlight',
8
+ 'fan',
9
+ 'tokens',
10
+ 'output',
11
+ 'children',
12
+ 'linkScanOffset',
13
+ 'linkScanPath',
14
+ 'template',
15
+ 'respawnFlags',
16
+ 'intent',
17
+ 'name',
18
+ 'nameSource',
19
+ 'sessionId',
20
+ 'resumeSessionId',
21
+ 'daemonShort',
22
+ 'cliVersion',
23
+ 'cwd',
24
+ 'bridgeSessionId',
25
+ 'bridgeOutboundOnly',
26
+ 'bridgeOwnerAccountUuid',
27
+ 'bridgeOwnerOrganizationUuid',
28
+ 'bgIsolation',
29
+ 'providerEnv',
30
+ 'interactiveLineage',
31
+ 'backend',
32
+ 'createdAt',
33
+ 'updatedAt',
34
+ 'firstTerminalAt',
35
+ 'lastTerminalAt',
36
+ 'deadEpochReapedAt',
37
+ 'bridgeSessionSeq',
38
+ 'forkSourceAlive',
39
+ 'forkBoundaryAt',
40
+ 'forkSessionId',
41
+ 'forkParentSessionId',
42
+ ];
43
+ export const jobStateDecoder = {
44
+ id: 'job-state@1',
45
+ source: 'job-state',
46
+ description: 'background job: state, current step, running shells/monitors (fan)',
47
+ versions: '>=2.1.200',
48
+ match: (raw) => (typeof raw.state === 'string' && 'sessionId' in raw ? 10 : 0),
49
+ decode(raw) {
50
+ const fan = arr(raw.fan)
51
+ .filter(isObj)
52
+ .map((f) => ({ id: str(f.id) ?? '', kind: str(f.kind) ?? '?', label: str(f.label) ?? '', startedAt: num(f.startedAt) }));
53
+ const extra = extraFields(raw, STATE_FIELDS);
54
+ return {
55
+ state: str(raw.state),
56
+ detail: str(raw.detail),
57
+ tempo: str(raw.tempo),
58
+ fan,
59
+ tokens: num(raw.tokens),
60
+ intent: str(raw.intent),
61
+ name: str(raw.name),
62
+ sessionId: str(raw.sessionId),
63
+ resumeSessionId: str(raw.resumeSessionId),
64
+ cwd: str(raw.cwd),
65
+ cliVersion: str(raw.cliVersion),
66
+ createdAt: str(raw.createdAt),
67
+ updatedAt: str(raw.updatedAt),
68
+ inFlight: obj(raw.inFlight),
69
+ fork: raw.forkParentSessionId || raw.forkSessionId
70
+ ? {
71
+ parentSessionId: str(raw.forkParentSessionId),
72
+ sessionId: str(raw.forkSessionId),
73
+ boundaryAt: str(raw.forkBoundaryAt),
74
+ sourceAlive: typeof raw.forkSourceAlive === 'boolean' ? raw.forkSourceAlive : undefined,
75
+ }
76
+ : undefined,
77
+ extra: extra ? sanitize(extra) : undefined,
78
+ };
79
+ },
80
+ knownFields: () => STATE_FIELDS,
81
+ };
82
+ export const jobStateFallback = {
83
+ id: 'job-state.fallback',
84
+ source: 'job-state',
85
+ description: 'unknown job state',
86
+ match: () => 1,
87
+ decode: (raw) => ({ fan: [], extra: sanitize(raw) }),
88
+ };
89
+ export const jobTimelineDecoder = {
90
+ id: 'job-timeline@1',
91
+ source: 'job-timeline',
92
+ description: 'job status changes',
93
+ match: (raw) => (typeof raw.at === 'string' ? 10 : 0),
94
+ decode: (raw) => ({ at: str(raw.at), state: str(raw.state), detail: str(raw.detail), text: str(raw.text) }),
95
+ knownFields: () => ['at', 'state', 'detail', 'text'],
96
+ };
97
+ export const jobTimelineFallback = {
98
+ id: 'job-timeline.fallback',
99
+ source: 'job-timeline',
100
+ description: 'unknown timeline entry',
101
+ match: () => 1,
102
+ decode: (raw) => ({ at: str(raw.at) ?? '', text: JSON.stringify(sanitize(raw)) }),
103
+ };
104
+ export const rosterDecoder = {
105
+ id: 'daemon-roster@1',
106
+ source: 'daemon-roster',
107
+ description: 'daemon workers with launch info (resume/fork, source session)',
108
+ match: (raw) => (isObj(raw.workers) ? 10 : 0),
109
+ decode(raw) {
110
+ const out = {};
111
+ for (const [short, w] of Object.entries(obj(raw.workers) ?? {})) {
112
+ const dispatch = obj(obj(w)?.dispatch);
113
+ const launch = obj(dispatch?.launch);
114
+ const from = str(launch?.sessionId);
115
+ out[short] = {
116
+ mode: str(launch?.mode),
117
+ fork: launch?.fork === true,
118
+ source: str(dispatch?.source),
119
+ // sessionId ist hier teils ein Pfad zur .jsonl-Datei
120
+ fromSessionId: from?.replace(/^.*\//, '').replace(/\.jsonl$/, ''),
121
+ };
122
+ }
123
+ return out;
124
+ },
125
+ };
126
+ export const rosterFallback = {
127
+ id: 'daemon-roster.fallback',
128
+ source: 'daemon-roster',
129
+ description: 'unknown roster format',
130
+ match: () => 1,
131
+ decode: () => ({}),
132
+ };
@@ -0,0 +1,77 @@
1
+ import { sanitize } from '../../util/sanitize.js';
2
+ import { arr, extraFields, num, str } from '../util.js';
3
+ const FIELDS = [
4
+ 'pid',
5
+ 'sessionId',
6
+ 'cwd',
7
+ 'startedAt',
8
+ 'procStart',
9
+ 'version',
10
+ 'kind',
11
+ 'entrypoint',
12
+ 'name',
13
+ 'nameSource',
14
+ 'nameSince',
15
+ 'status',
16
+ 'statusUpdatedAt',
17
+ 'updatedAt',
18
+ 'bridgeSessionId',
19
+ 'jobId',
20
+ 'parkedJobId',
21
+ 'peerProtocol',
22
+ 'peerFeatures',
23
+ 'pidDomain',
24
+ 'messagingSocketPath',
25
+ 'agent',
26
+ ];
27
+ function decode(raw) {
28
+ const extra = extraFields(raw, FIELDS);
29
+ return {
30
+ pid: num(raw.pid) ?? -1,
31
+ sessionId: str(raw.sessionId) ?? '',
32
+ cwd: str(raw.cwd),
33
+ name: str(raw.name),
34
+ nameSource: str(raw.nameSource),
35
+ kind: str(raw.kind),
36
+ entrypoint: str(raw.entrypoint),
37
+ version: str(raw.version),
38
+ status: str(raw.status),
39
+ statusUpdatedAt: num(raw.statusUpdatedAt),
40
+ startedAt: num(raw.startedAt),
41
+ updatedAt: num(raw.updatedAt),
42
+ bridgeSessionId: raw.bridgeSessionId === null ? null : str(raw.bridgeSessionId),
43
+ jobId: str(raw.jobId),
44
+ parkedJobId: str(raw.parkedJobId),
45
+ peerFeatures: arr(raw.peerFeatures).filter((x) => typeof x === 'string'),
46
+ agent: str(raw.agent),
47
+ procStart: str(raw.procStart),
48
+ extra: extra ? sanitize(extra) : undefined,
49
+ };
50
+ }
51
+ /** sessions/<pid>.json ab ca. 2.1.2xx: mit Status, Namen und Peer-Messaging. */
52
+ export const processDecoderV2 = {
53
+ id: 'process.session-file@2',
54
+ source: 'process',
55
+ description: 'running process with status (busy/idle), name, job and bridge link',
56
+ versions: '>=2.1.200',
57
+ match: (raw) => (typeof raw.pid === 'number' && typeof raw.sessionId === 'string' && 'status' in raw ? 15 : 0),
58
+ decode,
59
+ knownFields: () => FIELDS,
60
+ };
61
+ /** Ältere, schlanke Variante (z. B. 2.1.114): nur pid, sessionId, cwd, startedAt, version, kind. */
62
+ export const processDecoderV1 = {
63
+ id: 'process.session-file@1',
64
+ source: 'process',
65
+ description: 'running process, older variant without status',
66
+ versions: '<2.1.200',
67
+ match: (raw) => (typeof raw.pid === 'number' && typeof raw.sessionId === 'string' ? 10 : 0),
68
+ decode,
69
+ knownFields: () => FIELDS,
70
+ };
71
+ export const processFallback = {
72
+ id: 'process.fallback',
73
+ source: 'process',
74
+ description: 'unknown process file',
75
+ match: () => 1,
76
+ decode: (raw) => ({ pid: num(raw.pid) ?? -1, sessionId: str(raw.sessionId) ?? '', extra: sanitize(raw) }),
77
+ };