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.
- package/LICENSE +21 -0
- package/README.de.md +84 -0
- package/README.md +167 -0
- package/dist/app/server/cli/demo.js +321 -0
- package/dist/app/server/cli/service.js +170 -0
- package/dist/app/server/cli-snapshot.js +34 -0
- package/dist/app/server/cli.js +84 -0
- package/dist/app/server/config.js +41 -0
- package/dist/app/server/formats/history/prompt.js +17 -0
- package/dist/app/server/formats/index.js +35 -0
- package/dist/app/server/formats/job/job.js +132 -0
- package/dist/app/server/formats/process/session-file.js +77 -0
- package/dist/app/server/formats/registry.js +87 -0
- package/dist/app/server/formats/subagent/meta.js +28 -0
- package/dist/app/server/formats/task/task.js +33 -0
- package/dist/app/server/formats/transcript/common.js +33 -0
- package/dist/app/server/formats/transcript/message.js +206 -0
- package/dist/app/server/formats/transcript/other.js +130 -0
- package/dist/app/server/formats/types.js +10 -0
- package/dist/app/server/formats/util.js +22 -0
- package/dist/app/server/history/aggregator.js +318 -0
- package/dist/app/server/history/search.js +138 -0
- package/dist/app/server/http.js +169 -0
- package/dist/app/server/inspector.js +468 -0
- package/dist/app/server/main.js +2 -0
- package/dist/app/server/model/analyze.js +112 -0
- package/dist/app/server/model/flow.js +47 -0
- package/dist/app/server/run.js +37 -0
- package/dist/app/server/sources/jobs.js +59 -0
- package/dist/app/server/sources/processes.js +99 -0
- package/dist/app/server/sources/projects.js +165 -0
- package/dist/app/server/sources/spawns.js +297 -0
- package/dist/app/server/sources/subagents.js +62 -0
- package/dist/app/server/sources/tasks.js +30 -0
- package/dist/app/server/sources/transcripts.js +121 -0
- package/dist/app/server/util/jsonl.js +124 -0
- package/dist/app/server/util/sanitize.js +42 -0
- package/dist/app/server/util/semver.js +33 -0
- package/dist/app/shared/pricing.js +57 -0
- package/dist/app/shared/tools.js +62 -0
- package/dist/app/shared/types.js +3 -0
- package/dist/web/assets/index-DdxYQpDc.js +14 -0
- package/dist/web/assets/index-VdEOo0XQ.css +1 -0
- package/dist/web/index.html +22 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 brokoskokoli
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.de.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# claude-inspect
|
|
2
|
+
|
|
3
|
+
> English: [README.md](README.md) · Screenshots: [docs/screenshots](docs/screenshots)
|
|
4
|
+
>
|
|
5
|
+
> Die Oberfläche ist englisch; hell, dunkel oder nach Systemeinstellung (Umschalter oben rechts).
|
|
6
|
+
|
|
7
|
+
Lokales Live-Dashboard für Claude-Code-Agenten. Liest **passiv** die Dateien unter
|
|
8
|
+
`~/.claude`, ohne Hooks, ohne Konfigurationsänderung und ohne eigene Datenablage.
|
|
9
|
+
|
|
10
|
+
## Start
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npx claude-inspect --open # einmal ausprobieren
|
|
14
|
+
npm install -g claude-inspect # dauerhaft installieren
|
|
15
|
+
claude-inspect service install # Autostart → http://localhost:47717 (Lesezeichen)
|
|
16
|
+
claude-inspect demo --open # ohne eigene Daten ausprobieren
|
|
17
|
+
claude-inspect --help # alle Optionen
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Der Autostart ist unter macOS ein LaunchAgent, unter Linux ein `systemd --user`-Dienst. Er startet
|
|
21
|
+
beim Login und nach Abstürzen neu und läuft ohne Token (nur von localhost erreichbar).
|
|
22
|
+
`claude-inspect service status` zeigt Zustand und Log, `claude-inspect service uninstall` entfernt ihn.
|
|
23
|
+
|
|
24
|
+
Manuell gestartet gibt `claude-inspect` eine URL mit Zufallstoken (`?t=…`) aus; der Browser merkt
|
|
25
|
+
es sich als Cookie. `--token` für einen festen Token, `--no-auth` für keinen, `--port` für einen
|
|
26
|
+
anderen Port, `CLAUDE_CONFIG_DIR=/pfad` für ein anderes Datenverzeichnis.
|
|
27
|
+
|
|
28
|
+
Aus den Quellen: `git clone …`, `npm install`, `npm run build`, `npm start`
|
|
29
|
+
(`npm run dev` + `npm run dev:web` für die Entwicklung mit Hot-Reload).
|
|
30
|
+
|
|
31
|
+
## Was angezeigt wird
|
|
32
|
+
|
|
33
|
+
- **Live**: laufende Prozesse (`sessions/*.json`, Lebendprüfung per `ps`), jeweils mit
|
|
34
|
+
aktueller Aktivität, laufenden Tools, Hintergrund-Job samt laufender Shells, aktiven
|
|
35
|
+
Subagenten, Modell, Kontextgröße, Tokens und Permission-Mode
|
|
36
|
+
- **Sessions**: alle Transcripts mit Suche
|
|
37
|
+
- **Session-Detail**, mit Tabs:
|
|
38
|
+
- *Transcript*: Agentenbaum (Hauptagent → Subagenten, auch verschachtelt),
|
|
39
|
+
kompletter Verlauf mit Tool-Renderern (Bash, Read, Edit/Write als Diff, Agent mit Link
|
|
40
|
+
zum Subagenten, Grep/Glob, generisch für alle anderen), Filter, Suche und Rohansicht
|
|
41
|
+
jeder Zeile. Bei laufenden Sessions werden Änderungen live nachgeladen.
|
|
42
|
+
- *Timeline*: eine Spur je Agent (inkl. gestarteter Sitzungen), Tool-Aufrufe als
|
|
43
|
+
Balken, Aufruf-Verbindungen, Pausen über 10 min zusammengeschoben, Zoom
|
|
44
|
+
- *Call graph*: wer wen aufgerufen hat, inkl. Fork/Fortsetzung
|
|
45
|
+
- *Tasks*: Task-Liste der Session als Board
|
|
46
|
+
- **Statistics**: API-Gegenwert (Schätzung nach Listenpreisen), Tokens je Modell/Tag/Projekt,
|
|
47
|
+
Cache-Anteil, Aktivitäts-Heatmap, Tool-Statistik mit Fehlern und Dauer (p95)
|
|
48
|
+
- **Tools**: alle Tool-Aufrufe aller Sessions, filterbar; Klick springt an die Stelle im Verlauf
|
|
49
|
+
- **Files**: welche Dateien Agenten gelesen/geändert haben
|
|
50
|
+
- **Search**: Volltext über alle Verläufe, Subagenten und die Eingabe-Historie
|
|
51
|
+
- **Claude-Aufrufe**: Sitzungen, die ein Agent per Bash startet (`claude -p …`,
|
|
52
|
+
`claude --agent …`), werden dem aufrufenden Tool-Aufruf zugeordnet und erscheinen im
|
|
53
|
+
Agentenbaum, auf der Dashboard-Karte und am Bash-Aufruf. Konfidenz:
|
|
54
|
+
- *exakt*: Prozessbaum (laufendes Kind → Shell → Eltern-Prozess) oder `--session-id` im Befehl
|
|
55
|
+
- *sicher*: erster Prompt des Kindes steht im Befehl, dazu Zeit, `--agent` und Verzeichnis
|
|
56
|
+
- *wahrscheinlich*: schwächere Indizien, z. B. nur kurzer Prompt und Zeitfenster
|
|
57
|
+
(siehe `src/server/sources/spawns.ts`)
|
|
58
|
+
- **Formats**: registrierte Decoder mit Trefferzahlen und alle Felder, die noch kein
|
|
59
|
+
Decoder kennt (Schema-Drift)
|
|
60
|
+
|
|
61
|
+
## Architektur
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
src/shared/ Domain-Typen, Tool-Kurzbeschreibungen, Preistabelle (Server + Frontend)
|
|
65
|
+
src/server/
|
|
66
|
+
formats/ Decoder je Quelle + Registry (Format-Erkennung pro Record)
|
|
67
|
+
sources/ Dateizugriff: Projekte, Transcripts (inkrementell), Prozesse, Jobs, Subagenten
|
|
68
|
+
model/ Aktivität/offene Tools (analyze.ts), Zeitleisten-Daten (flow.ts)
|
|
69
|
+
history/ Aggregator (Tokens, Kosten, Tools, Dateien) und Volltextsuche
|
|
70
|
+
inspector.ts Zustand, Dateiüberwachung, Dashboard-Aufbau
|
|
71
|
+
http.ts REST + Server-Sent Events
|
|
72
|
+
web/ Svelte-5-Frontend
|
|
73
|
+
tests/ Fixtures je Claude-Code-Version, Snapshots, Tests
|
|
74
|
+
scripts/ Fixture-Generator
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Neue Dateiformate: siehe [docs/FORMATS.md](docs/FORMATS.md). Gesamtkonzept: [KONZEPT.md](KONZEPT.md).
|
|
78
|
+
|
|
79
|
+
## Speicher & Leistung
|
|
80
|
+
|
|
81
|
+
Nichts wird auf Platte geschrieben. Beim Start liest der Server nur Prozess-, Job- und
|
|
82
|
+
Kopfdaten (unter 1 s); die komplette History (hier ~600 MB) wird im Hintergrund in
|
|
83
|
+
2–3 s aggregiert und danach nur noch inkrementell nachgeführt. Vollständige Verläufe
|
|
84
|
+
werden erst beim Öffnen geladen und mit einer Obergrenze im Speicher gehalten.
|
package/README.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# claude-inspect – live dashboard & history viewer for Claude Code agents
|
|
2
|
+
|
|
3
|
+
[](LICENSE)
|
|
4
|
+
[](https://www.npmjs.com/package/claude-inspect)
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
**See what your [Claude Code](https://claude.com/claude-code) agents are doing – right now and in the past.**
|
|
11
|
+
claude-inspect is a local web UI that reads the files Claude Code already writes to `~/.claude`
|
|
12
|
+
and shows running sessions, subagents, `claude -p` worker sessions started by other agents,
|
|
13
|
+
every tool call with its result, timelines, a call graph, token usage and a searchable history.
|
|
14
|
+
|
|
15
|
+
It is **read-only and passive**: no hooks, no settings changes, no proxy, no data leaves your machine,
|
|
16
|
+
and the tool itself stores nothing.
|
|
17
|
+
|
|
18
|
+
> Deutsch: [README.de.md](README.de.md) · Design notes (German): [KONZEPT.md](KONZEPT.md)
|
|
19
|
+
|
|
20
|
+

|
|
21
|
+
|
|
22
|
+
## Why
|
|
23
|
+
|
|
24
|
+
If you run several Claude Code sessions, background jobs and subagents in parallel – or let an
|
|
25
|
+
orchestrating agent start dedicated `claude -p` / `claude --agent …` worker sessions – it quickly
|
|
26
|
+
becomes hard to tell *who is doing what*. claude-inspect answers:
|
|
27
|
+
|
|
28
|
+
- Which agents are running, which are idle, which crashed?
|
|
29
|
+
- What is each agent doing at this moment (tool, command, since when)?
|
|
30
|
+
- **Who started whom?** Subagents *and* separate `claude` processes spawned via Bash are linked to the exact tool call that started them.
|
|
31
|
+
- What did a tool call receive and return? (Bash output, file diffs, subagent results …)
|
|
32
|
+
- How much did a session cost (API list-price equivalent), which tools fail, which files are touched most?
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
| | |
|
|
37
|
+
|---|---|
|
|
38
|
+
| **Live dashboard** | Running Claude Code processes with status (busy/idle), current activity, pending tools, background jobs with their running shells, active subagents and spawned worker sessions, model, context size, tokens, permission mode, Remote Control. Updates via Server-Sent Events. |
|
|
39
|
+
| **Transcript viewer** | Full conversation with per-tool renderers (Bash output, Edit/Write as diff, Read with line numbers, Agent with link to the subagent), filters, search, and the raw JSON of every line (secrets masked). Follows live sessions. |
|
|
40
|
+
| **Agent tree & call graph** | Main agent → subagents (nested) → `claude` sessions started via Bash, including fork/resume relations. Finished agents are greyed out. |
|
|
41
|
+
| **Timeline** | One lane per agent, tool calls as bars, spawn links between lanes, idle gaps > 10 min collapsed, zoom. Click a bar to jump to the call. |
|
|
42
|
+
| **Spawn linking** | Links sessions started by `claude -p …` to the calling tool use via the process tree (exact), `--session-id`, prompt text, `--agent`, time window and working directory – with a confidence label. |
|
|
43
|
+
| **Statistics** | API list-price estimate, tokens per model/day/project, cache share, activity heatmap, tool statistics with error rate and p95 duration. |
|
|
44
|
+
| **Tool explorer & files** | Every tool call across all sessions, filterable; which files agents read, edited or wrote. |
|
|
45
|
+
| **Full-text search** | Across all transcripts, subagents and the prompt history (including prompts of sessions Claude Code already cleaned up). |
|
|
46
|
+
| **Task boards** | The TaskCreate/TaskUpdate list of a session with dependencies. |
|
|
47
|
+
| **Format detection & schema drift** | Claude Code's file formats change between versions. Each record is decoded individually by versioned decoders; unknown records and fields are never dropped and are listed on the *Formats* page. |
|
|
48
|
+
| **Dark mode** | Light, dark or system theme, remembered per browser. |
|
|
49
|
+
|
|
50
|
+
<table>
|
|
51
|
+
<tr>
|
|
52
|
+
<td><img src="docs/screenshots/timeline.jpg" alt="Timeline of an orchestrator with subagents and a spawned worker session"></td>
|
|
53
|
+
<td><img src="docs/screenshots/call-graph.jpg" alt="Call graph: main agent, subagents and a claude -p worker session"></td>
|
|
54
|
+
</tr>
|
|
55
|
+
<tr>
|
|
56
|
+
<td><img src="docs/screenshots/transcript.jpg" alt="Transcript viewer with an expanded Edit tool call shown as diff"></td>
|
|
57
|
+
<td><img src="docs/screenshots/statistics.jpg" alt="Statistics: API cost estimate per day and model, activity heatmap"></td>
|
|
58
|
+
</tr>
|
|
59
|
+
</table>
|
|
60
|
+
|
|
61
|
+
<table>
|
|
62
|
+
<tr>
|
|
63
|
+
<td><img src="docs/screenshots/tool-explorer.jpg" alt="Tool explorer listing tool calls across all sessions"></td>
|
|
64
|
+
<td><img src="docs/screenshots/dashboard-dark.jpg" alt="Live dashboard in dark mode"></td>
|
|
65
|
+
</tr>
|
|
66
|
+
</table>
|
|
67
|
+
|
|
68
|
+
*All screenshots show the built-in demo data set (`claude-inspect demo`), not real sessions.*
|
|
69
|
+
|
|
70
|
+
## Quick start
|
|
71
|
+
|
|
72
|
+
Requirements: Node.js ≥ 20, macOS or Linux, Claude Code writing to `~/.claude`.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npx claude-inspect --open # try it once
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
or install it permanently:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npm install -g claude-inspect
|
|
82
|
+
claude-inspect service install # starts at login → http://localhost:47717 (bookmark it)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The autostart service is a LaunchAgent on macOS and a `systemd --user` unit on Linux. It restarts
|
|
86
|
+
after crashes and serves the dashboard without a token (still only reachable from localhost).
|
|
87
|
+
`claude-inspect service status` shows state and log file, `claude-inspect service uninstall` removes it.
|
|
88
|
+
After `npm install -g claude-inspect@latest`, run `claude-inspect service install` once more to restart it.
|
|
89
|
+
|
|
90
|
+
Started manually, `claude-inspect` prints a URL with a random access token (`?t=…`) that the browser
|
|
91
|
+
keeps as a cookie. Use `--token <t>` for a fixed token or `--no-auth` for none, `--port` for another port,
|
|
92
|
+
`CLAUDE_CONFIG_DIR=/path/to/.claude` for another data directory. `claude-inspect --help` lists everything.
|
|
93
|
+
|
|
94
|
+
### Try it without your own data
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
npx claude-inspect demo --open # fictional ~/.claude with running agents + 2 weeks of history
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Light, dark or follow the OS – switch with the button in the header (the choice is remembered).
|
|
101
|
+
|
|
102
|
+
### Status indicators
|
|
103
|
+
|
|
104
|
+
| | |
|
|
105
|
+
|---|---|
|
|
106
|
+
| green, pulsing dot | working right now |
|
|
107
|
+
| amber dot | process alive, waiting for input |
|
|
108
|
+
| grey check mark | finished normally |
|
|
109
|
+
| red cross | ended with an API error (single failing tool calls don't count – the agent carries on) |
|
|
110
|
+
| grey ring | no activity for 10 min and never finished, e.g. interrupted |
|
|
111
|
+
| grey dot | process has ended |
|
|
112
|
+
|
|
113
|
+
## How it works
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
~/.claude/sessions/<pid>.json running processes (+ ps liveness check, process tree)
|
|
117
|
+
~/.claude/projects/*/<session>.jsonl transcripts (read incrementally, parsed per record)
|
|
118
|
+
~/.claude/projects/*/<session>/subagents/agent-*.jsonl + .meta.json
|
|
119
|
+
~/.claude/jobs/*/state.json, timeline.jsonl, daemon/roster.json background jobs, forks
|
|
120
|
+
~/.claude/tasks/<session>/*.json task lists
|
|
121
|
+
~/.claude/history.jsonl prompt history
|
|
122
|
+
│
|
|
123
|
+
▼
|
|
124
|
+
FormatRegistry → versioned decoders → normalized model → REST + SSE → Svelte UI
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
- **Per-record format detection:** every decoder scores a record (`type`, version range, structure); the best one wins, a fallback keeps unknown records. Adding support for a new Claude Code version = adding a decoder file. See [docs/FORMATS.md](docs/FORMATS.md).
|
|
128
|
+
- **In memory only:** startup reads process/job data and file heads (< 1 s); the full history (hundreds of MB) is aggregated in the background in a few seconds and then followed incrementally.
|
|
129
|
+
- **Privacy & security:** listens on `127.0.0.1` only (never reachable from the network), rejects requests whose `Host` header isn't `localhost`/`127.0.0.1` (DNS rebinding), sends no CORS headers (other websites in your browser can't read the API), is read-only, never reads credential files (`*.key`, `daemon/auth`, `ide/*.lock`, …) and masks token/secret fields in the raw view. The access token is an extra layer against *other local processes or users* on the same machine; on a single-user machine `--no-auth` (used by the autostart service) is a reasonable trade-off. Cost figures are estimates from public API list prices – with a subscription you do not pay these amounts directly.
|
|
130
|
+
|
|
131
|
+
## Development
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
git clone https://github.com/brokoskokoli/claude_inspect.git && cd claude_inspect
|
|
135
|
+
npm install
|
|
136
|
+
npm run build # web UI (dist/web) + compiled server (dist/app)
|
|
137
|
+
npm start # run the build
|
|
138
|
+
npm run demo # demo data from the sources
|
|
139
|
+
npm run service:install # autostart pointing at this checkout's build
|
|
140
|
+
npm run dev # API without token on :7717 (tsx watch, no build needed)
|
|
141
|
+
npm run dev:web # Vite dev server on :5173 with proxy to the API
|
|
142
|
+
npm run check # type checks (server + Svelte)
|
|
143
|
+
npm test # decoder snapshots per Claude Code version, spawn linker, history aggregator
|
|
144
|
+
npm run snapshot # terminal output: running agents + schema drift
|
|
145
|
+
npm run fixtures # regenerate anonymized test fixtures from your local ~/.claude
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
src/shared/ domain types, tool summaries, price table (server + UI)
|
|
150
|
+
src/server/ formats/ (decoders + registry) · sources/ (files, processes, jobs, spawn linker)
|
|
151
|
+
model/ (activity, timeline data) · history/ (aggregator, search) · http.ts, inspector.ts
|
|
152
|
+
web/ Svelte 5 UI
|
|
153
|
+
tests/ anonymized fixtures per Claude Code version, snapshots, tests
|
|
154
|
+
scripts/ fixture generator (demo data: src/server/cli/demo.ts)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Contributions welcome – especially decoders for new Claude Code versions (run `npm run fixtures` and
|
|
158
|
+
`npm test`, then check the *Formats* page for unknown fields).
|
|
159
|
+
|
|
160
|
+
## Disclaimer
|
|
161
|
+
|
|
162
|
+
Not affiliated with or endorsed by Anthropic. Claude Code's local file formats are undocumented and
|
|
163
|
+
may change at any time; claude-inspect is designed to degrade gracefully (unknown data is shown raw).
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Demo mode: builds a completely fictional ~/.claude directory (running agents,
|
|
3
|
+
* subagents, a `claude -p` spawned worker, tasks and two weeks of history) and
|
|
4
|
+
* starts claude-inspect on it. Used for screenshots and for trying the tool
|
|
5
|
+
* without real data.
|
|
6
|
+
*
|
|
7
|
+
* claude-inspect demo → generate into a temp dir and start the server
|
|
8
|
+
* claude-inspect demo --out DIR → only generate into DIR (no processes, no server)
|
|
9
|
+
*/
|
|
10
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
11
|
+
import { existsSync, mkdirSync, rmSync, utimesSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
let ROOT = '';
|
|
15
|
+
const NOW = Date.now();
|
|
16
|
+
const VERSION = '2.1.283';
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// deterministic pseudo-random numbers
|
|
19
|
+
// mulberry32 – kleiner, deterministischer 32-Bit-Generator
|
|
20
|
+
let seed = 42;
|
|
21
|
+
const rnd = () => {
|
|
22
|
+
seed = (seed + 0x6d2b79f5) | 0;
|
|
23
|
+
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
|
|
24
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
25
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
26
|
+
};
|
|
27
|
+
const pick = (xs) => xs[Math.floor(rnd() * xs.length)];
|
|
28
|
+
let idc = 0;
|
|
29
|
+
const uuid = () => {
|
|
30
|
+
const h = (n) => Array.from({ length: n }, () => Math.floor(rnd() * 16).toString(16)).join('');
|
|
31
|
+
return `${h(8)}-${h(4)}-4${h(3)}-a${h(3)}-${h(12)}`;
|
|
32
|
+
};
|
|
33
|
+
const nextId = (p) => `${p}_demo${(++idc).toString(36).padStart(6, '0')}`;
|
|
34
|
+
const slug = (cwd) => cwd.replace(/[/_.]/g, '-');
|
|
35
|
+
const write = (path, data) => {
|
|
36
|
+
mkdirSync(join(path, '..'), { recursive: true });
|
|
37
|
+
writeFileSync(path, data);
|
|
38
|
+
};
|
|
39
|
+
class Transcript {
|
|
40
|
+
o;
|
|
41
|
+
lines = [];
|
|
42
|
+
t;
|
|
43
|
+
parent = null;
|
|
44
|
+
constructor(o) {
|
|
45
|
+
this.o = o;
|
|
46
|
+
this.t = o.start;
|
|
47
|
+
}
|
|
48
|
+
env(extra = {}) {
|
|
49
|
+
const uuidV = uuid();
|
|
50
|
+
const rec = {
|
|
51
|
+
parentUuid: this.parent,
|
|
52
|
+
isSidechain: !!this.o.agentId,
|
|
53
|
+
userType: 'external',
|
|
54
|
+
cwd: this.o.cwd,
|
|
55
|
+
sessionId: this.o.sessionId,
|
|
56
|
+
version: VERSION,
|
|
57
|
+
gitBranch: 'main',
|
|
58
|
+
entrypoint: this.o.entrypoint ?? 'cli',
|
|
59
|
+
uuid: uuidV,
|
|
60
|
+
timestamp: new Date(this.t).toISOString(),
|
|
61
|
+
...(this.o.agentId ? { agentId: this.o.agentId } : {}),
|
|
62
|
+
...extra,
|
|
63
|
+
};
|
|
64
|
+
this.parent = uuidV;
|
|
65
|
+
return rec;
|
|
66
|
+
}
|
|
67
|
+
wait(s) {
|
|
68
|
+
this.t += s * 1000;
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
meta(type, data) {
|
|
72
|
+
this.lines.push({ type, ...data, sessionId: this.o.sessionId });
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
prompt(text) {
|
|
76
|
+
this.lines.push({ ...this.env({ promptSource: this.o.promptSource, permissionMode: 'auto' }), type: 'user', message: { role: 'user', content: text } });
|
|
77
|
+
this.meta('last-prompt', { lastPrompt: text.slice(0, 200), leafUuid: this.parent });
|
|
78
|
+
return this.wait(3);
|
|
79
|
+
}
|
|
80
|
+
usage(out) {
|
|
81
|
+
const ctx = 20_000 + Math.floor(rnd() * 90_000);
|
|
82
|
+
return {
|
|
83
|
+
input_tokens: 3,
|
|
84
|
+
output_tokens: out,
|
|
85
|
+
cache_read_input_tokens: ctx,
|
|
86
|
+
cache_creation_input_tokens: Math.floor(ctx * 0.04),
|
|
87
|
+
cache_creation: { ephemeral_5m_input_tokens: 0, ephemeral_1h_input_tokens: Math.floor(ctx * 0.04) },
|
|
88
|
+
service_tier: 'standard',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
assistant(blocks, stop, out, model = this.o.model) {
|
|
92
|
+
this.lines.push({
|
|
93
|
+
...this.env({ requestId: nextId('req'), effort: 'medium' }),
|
|
94
|
+
type: 'assistant',
|
|
95
|
+
message: { model, id: nextId('msg'), type: 'message', role: 'assistant', content: blocks, stop_reason: stop, stop_sequence: null, usage: this.usage(out) },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
say(text, final = false) {
|
|
99
|
+
this.assistant([{ type: 'text', text }], final ? 'end_turn' : 'tool_use', 60 + Math.floor(text.length / 3));
|
|
100
|
+
return this.wait(2);
|
|
101
|
+
}
|
|
102
|
+
/** Tool call; `result` undefined = still running. Returns the tool_use id. */
|
|
103
|
+
tool(name, input, result) {
|
|
104
|
+
const id = nextId('toolu');
|
|
105
|
+
this.assistant([{ type: 'thinking', thinking: '', signature: 'x' }], 'tool_use', 20);
|
|
106
|
+
this.assistant([{ type: 'tool_use', id, name, input }], 'tool_use', 80 + Math.floor(rnd() * 300));
|
|
107
|
+
if (!result)
|
|
108
|
+
return id;
|
|
109
|
+
this.wait(result.seconds ?? 1 + rnd() * 3);
|
|
110
|
+
this.lines.push({
|
|
111
|
+
...this.env({ sourceToolAssistantUUID: this.parent }),
|
|
112
|
+
type: 'user',
|
|
113
|
+
message: { role: 'user', content: [{ type: 'tool_result', tool_use_id: id, content: result.text, is_error: !!result.isError }] },
|
|
114
|
+
toolUseResult: result.structured ?? { stdout: result.text, stderr: '', interrupted: false, isImage: false },
|
|
115
|
+
});
|
|
116
|
+
this.wait(2 + rnd() * 4);
|
|
117
|
+
return id;
|
|
118
|
+
}
|
|
119
|
+
system(subtype, data) {
|
|
120
|
+
this.lines.push({ ...this.env(), type: 'system', subtype, level: 'info', ...data });
|
|
121
|
+
return this;
|
|
122
|
+
}
|
|
123
|
+
save(path) {
|
|
124
|
+
write(path, this.lines.map((l) => JSON.stringify(l)).join('\n') + '\n');
|
|
125
|
+
// mtime = letzte Aktivität, damit "Letzte Sessions" realistisch sortiert
|
|
126
|
+
const last = new Date(Math.min(this.t, Date.now()));
|
|
127
|
+
utimesSync(path, last, last);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const read = (file, content) => ({
|
|
131
|
+
text: content,
|
|
132
|
+
structured: { type: 'text', file: { filePath: file, content, numLines: content.split('\n').length, startLine: 1, totalLines: content.split('\n').length + 40 } },
|
|
133
|
+
});
|
|
134
|
+
const edit = (file, oldStart, lines) => ({
|
|
135
|
+
text: `The file ${file} has been updated successfully.`,
|
|
136
|
+
structured: {
|
|
137
|
+
filePath: file,
|
|
138
|
+
oldString: '',
|
|
139
|
+
newString: '',
|
|
140
|
+
originalFile: null,
|
|
141
|
+
structuredPatch: [{ oldStart, oldLines: lines.filter((l) => !l.startsWith('+')).length, newStart: oldStart, newLines: lines.filter((l) => !l.startsWith('-')).length, lines }],
|
|
142
|
+
userModified: false,
|
|
143
|
+
replaceAll: false,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
const bash = (stdout, isError = false, seconds) => ({ text: stdout, isError, seconds, structured: { stdout, stderr: '', interrupted: false, isImage: false } });
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
/** Writes all demo files below ROOT and returns what the process setup needs. */
|
|
149
|
+
function buildScenario() {
|
|
150
|
+
// live scenario: orchestrator (interactive) → subagents + spawned implementer
|
|
151
|
+
const SHOP = '/Users/demo/code/acme-shop';
|
|
152
|
+
const DOCS = '/Users/demo/code/docs-site';
|
|
153
|
+
const shopDir = join(ROOT, 'projects', slug(SHOP));
|
|
154
|
+
const docsDir = join(ROOT, 'projects', slug(DOCS));
|
|
155
|
+
const orchId = uuid();
|
|
156
|
+
const implId = uuid();
|
|
157
|
+
const orch = new Transcript({ sessionId: orchId, cwd: SHOP, start: NOW - 38 * 60_000, model: 'claude-opus-5-5' });
|
|
158
|
+
orch.meta('custom-title', { customTitle: 'orchestrator' }).meta('agent-name', { agentName: 'orchestrator' }).meta('ai-title', { aiTitle: 'Add coupon codes to checkout' });
|
|
159
|
+
orch.prompt('Add coupon codes to the checkout: spec first, then implement it with a separate worker and review the result.');
|
|
160
|
+
orch.say("I'll look at how the checkout computes totals, then write a short spec.");
|
|
161
|
+
orch.tool('Grep', { pattern: 'calculateTotal', path: 'src' }, { text: 'src/checkout/totals.ts\nsrc/checkout/totals.test.ts\nsrc/api/orders.ts', structured: { mode: 'files_with_matches', filenames: ['src/checkout/totals.ts'], numFiles: 3 } });
|
|
162
|
+
orch.tool('Read', { file_path: `${SHOP}/src/checkout/totals.ts` }, read(`${SHOP}/src/checkout/totals.ts`, "import { Cart } from './cart';\n\nexport function calculateTotal(cart: Cart): number {\n const subtotal = cart.items.reduce((s, i) => s + i.price * i.qty, 0);\n return subtotal + shippingFor(cart);\n}"));
|
|
163
|
+
const exploreStart = orch.t;
|
|
164
|
+
const exploreTool = orch.tool('Agent', { subagent_type: 'Explore', description: 'Find discount handling', prompt: 'Find every place where prices, discounts or promotions are applied. Report file:line with a one-line summary each.' }, { text: 'Discounts are applied in two places:\n- src/checkout/totals.ts:12 – shipping threshold\n- src/api/orders.ts:88 – legacy promo field (unused)', seconds: 95, structured: { status: 'completed', agentId: 'a1f0c2e9d4b7a6c51', totalDurationMs: 95_000, totalTokens: 41_200, totalToolUseCount: 9, resolvedModel: 'claude-sonnet-5' } });
|
|
165
|
+
orch.tool('Write', { file_path: `${SHOP}/docs/specs/0012-coupons.md`, content: '# Spec 0012 – Coupon codes\n\n## Goal\nCustomers can enter one coupon code at checkout.\n\n## Rules\n- percentage or fixed amount\n- never below zero\n- one code per order\n\n## Tests\nT1–T6, see §5' }, { text: 'File created successfully', structured: { type: 'create', filePath: `${SHOP}/docs/specs/0012-coupons.md`, content: '', structuredPatch: [] } });
|
|
166
|
+
orch.tool('TaskCreate', { subject: 'Implement coupon codes (spec 0012)', description: 'Worker implements T1–T6' }, bash('Task #1 created'));
|
|
167
|
+
orch.tool('TaskCreate', { subject: 'Review implementation', description: 'Independent review against the spec' }, bash('Task #2 created'));
|
|
168
|
+
orch.say('Spec is written. Starting a dedicated implementer session for it.');
|
|
169
|
+
const spawnPrompt = 'Implement docs/specs/0012-coupons.md in acme-shop. Write tests T1-T6 first, keep changes inside src/checkout, and summarize in .agent/summary.md.';
|
|
170
|
+
orch.tool('Bash', {
|
|
171
|
+
command: `cd ${SHOP} && claude --agent implementer --model sonnet -p "${spawnPrompt}" --output-format json > .agent/run-1.json`,
|
|
172
|
+
description: 'Start implementer for spec 0012',
|
|
173
|
+
run_in_background: true,
|
|
174
|
+
}, { text: '', structured: { stdout: '', stderr: '', interrupted: false, isImage: false, backgroundTaskId: 'b7k2m9' }, seconds: 1 });
|
|
175
|
+
const implStart = orch.t + 2000;
|
|
176
|
+
const reviewTool = orch.tool('Agent', { subagent_type: 'spec-reviewer', description: 'Review spec 0012', prompt: 'Review docs/specs/0012-coupons.md for gaps and contradictions.', run_in_background: true }, { text: 'Async agent launched successfully.', seconds: 1, structured: { isAsync: true, status: 'async_launched', agentId: 'b82d4e1f09c3a7d65', description: 'Review spec 0012', resolvedModel: 'claude-opus-5-5' } });
|
|
177
|
+
orch.tool('Bash', { command: 'npm run lint -- src/checkout', description: 'Lint checkout module' }, bash('✔ 0 problems', false, 6));
|
|
178
|
+
orch.say('Implementer and spec review are running. I will check back when the worker reports.');
|
|
179
|
+
orch.t = NOW - 75_000;
|
|
180
|
+
orch.tool('Bash', { command: 'sleep 240; cat .agent/status.json', description: 'Wait for implementer status' });
|
|
181
|
+
orch.save(join(shopDir, `${orchId}.jsonl`));
|
|
182
|
+
// subagent 1 (done)
|
|
183
|
+
const sub1 = new Transcript({ sessionId: orchId, cwd: SHOP, start: exploreStart + 6000, model: 'claude-sonnet-5', agentId: 'a1f0c2e9d4b7a6c51' });
|
|
184
|
+
sub1.prompt('Find every place where prices, discounts or promotions are applied. Report file:line with a one-line summary each.');
|
|
185
|
+
for (const f of ['totals.ts', 'cart.ts', 'orders.ts', 'promo.ts'])
|
|
186
|
+
sub1.tool('Grep', { pattern: 'discount|promo', path: `src/${f}` }, bash(`src/${f}:12`, false, 6 + rnd() * 8));
|
|
187
|
+
sub1.wait(10).tool('Read', { file_path: `${SHOP}/src/api/orders.ts` }, read(`${SHOP}/src/api/orders.ts`, '// legacy promo field\nconst promo = body.promo ?? null;'));
|
|
188
|
+
sub1.say('Discounts are applied in two places …', true);
|
|
189
|
+
sub1.save(join(shopDir, orchId, 'subagents', 'agent-a1f0c2e9d4b7a6c51.jsonl'));
|
|
190
|
+
write(join(shopDir, orchId, 'subagents', 'agent-a1f0c2e9d4b7a6c51.meta.json'), JSON.stringify({ agentType: 'Explore', description: 'Find discount handling', toolUseId: exploreTool, spawnDepth: 1, requestShape: 'foreground' }));
|
|
191
|
+
// subagent 2 (background, running)
|
|
192
|
+
const sub2 = new Transcript({ sessionId: orchId, cwd: SHOP, start: implStart + 3000, model: 'claude-opus-5-5', agentId: 'b82d4e1f09c3a7d65' });
|
|
193
|
+
sub2.prompt('Review docs/specs/0012-coupons.md for gaps and contradictions.');
|
|
194
|
+
sub2.tool('Read', { file_path: `${SHOP}/docs/specs/0012-coupons.md` }, read(`${SHOP}/docs/specs/0012-coupons.md`, '# Spec 0012 – Coupon codes'));
|
|
195
|
+
sub2.tool('Grep', { pattern: 'currency', path: 'src/checkout' }, bash('src/checkout/money.ts:3'));
|
|
196
|
+
sub2.t = NOW - 25_000;
|
|
197
|
+
sub2.tool('Read', { file_path: `${SHOP}/src/checkout/money.ts` });
|
|
198
|
+
sub2.save(join(shopDir, orchId, 'subagents', 'agent-b82d4e1f09c3a7d65.jsonl'));
|
|
199
|
+
write(join(shopDir, orchId, 'subagents', 'agent-b82d4e1f09c3a7d65.meta.json'), JSON.stringify({ agentType: 'spec-reviewer', description: 'Review spec 0012', toolUseId: reviewTool, spawnDepth: 1, requestShape: 'background' }));
|
|
200
|
+
// spawned implementer session (own process, started via `claude -p`)
|
|
201
|
+
const impl = new Transcript({ sessionId: implId, cwd: SHOP, start: implStart, model: 'claude-sonnet-5', entrypoint: 'sdk-cli', promptSource: 'sdk' });
|
|
202
|
+
impl.meta('agent-setting', { agentSetting: 'implementer' }).meta('ai-title', { aiTitle: 'Implement coupon codes (spec 0012)' });
|
|
203
|
+
impl.lines.push({ type: 'queue-operation', operation: 'enqueue', timestamp: new Date(implStart).toISOString(), sessionId: implId, content: spawnPrompt });
|
|
204
|
+
impl.prompt(spawnPrompt);
|
|
205
|
+
impl.tool('Read', { file_path: `${SHOP}/docs/specs/0012-coupons.md` }, read(`${SHOP}/docs/specs/0012-coupons.md`, '# Spec 0012 – Coupon codes'));
|
|
206
|
+
impl.tool('Write', { file_path: `${SHOP}/src/checkout/coupon.test.ts`, content: "describe('coupons', () => {\n it('T1 applies percentage', () => {});\n it('T2 applies fixed amount', () => {});\n});" }, { text: 'File created successfully', structured: { type: 'create', filePath: `${SHOP}/src/checkout/coupon.test.ts`, content: '', structuredPatch: [] } });
|
|
207
|
+
impl.tool('Bash', { command: 'npm test -- coupon', description: 'Run coupon tests (expect red)' }, bash('FAIL src/checkout/coupon.test.ts\n ✕ T1 applies percentage\n ✕ T2 applies fixed amount', true, 14));
|
|
208
|
+
impl.tool('Edit', { file_path: `${SHOP}/src/checkout/totals.ts`, old_string: ' return subtotal + shippingFor(cart);', new_string: ' const discount = applyCoupon(subtotal, cart.coupon);\n return Math.max(0, subtotal - discount) + shippingFor(cart);' }, edit(`${SHOP}/src/checkout/totals.ts`, 3, [
|
|
209
|
+
' export function calculateTotal(cart: Cart): number {',
|
|
210
|
+
' const subtotal = cart.items.reduce((s, i) => s + i.price * i.qty, 0);',
|
|
211
|
+
'- return subtotal + shippingFor(cart);',
|
|
212
|
+
'+ const discount = applyCoupon(subtotal, cart.coupon);',
|
|
213
|
+
'+ return Math.max(0, subtotal - discount) + shippingFor(cart);',
|
|
214
|
+
' }',
|
|
215
|
+
]));
|
|
216
|
+
impl.tool('Write', { file_path: `${SHOP}/src/checkout/coupon.ts`, content: 'export function applyCoupon(subtotal: number, code?: Coupon): number {\n if (!code) return 0;\n return code.kind === "percent" ? subtotal * code.value / 100 : code.value;\n}' }, { text: 'File created successfully', structured: { type: 'create', filePath: `${SHOP}/src/checkout/coupon.ts`, content: '', structuredPatch: [] } });
|
|
217
|
+
impl.tool('Bash', { command: 'npm test -- coupon', description: 'Run coupon tests' }, bash('PASS src/checkout/coupon.test.ts\n ✓ T1 applies percentage (3 ms)\n ✓ T2 applies fixed amount (1 ms)', false, 12));
|
|
218
|
+
impl.t = NOW - 70_000;
|
|
219
|
+
impl.tool('Bash', { command: 'npm test', description: 'Run full test suite' });
|
|
220
|
+
impl.save(join(shopDir, `${implId}.jsonl`));
|
|
221
|
+
// docs-site session (interactive, idle)
|
|
222
|
+
const docsId = uuid();
|
|
223
|
+
const docs = new Transcript({ sessionId: docsId, cwd: DOCS, start: NOW - 55 * 60_000, model: 'claude-sonnet-5' });
|
|
224
|
+
docs.meta('ai-title', { aiTitle: 'Fix broken links in the docs' });
|
|
225
|
+
docs.prompt('Find and fix broken internal links in the docs.');
|
|
226
|
+
docs.tool('Bash', { command: 'npx linkinator ./build --recurse', description: 'Check links' }, bash('✖ 3 broken links\n /guide/install → 404\n /api/v1 → 404\n /faq#billing → 404', true, 22));
|
|
227
|
+
docs.tool('Grep', { pattern: '/guide/install', path: 'content' }, bash('content/index.md:14\ncontent/start.md:3'));
|
|
228
|
+
for (const f of ['index.md', 'start.md']) {
|
|
229
|
+
docs.tool('Edit', { file_path: `${DOCS}/content/${f}`, old_string: '/guide/install', new_string: '/guide/installation' }, edit(`${DOCS}/content/${f}`, 14, ['-See the [install guide](/guide/install).', '+See the [install guide](/guide/installation).']));
|
|
230
|
+
}
|
|
231
|
+
docs.tool('Bash', { command: 'npx linkinator ./build --recurse', description: 'Re-check links' }, bash('✔ 0 broken links', false, 20));
|
|
232
|
+
docs.say('All internal links resolve again. I fixed 3 links in 2 files.', true);
|
|
233
|
+
docs.t = NOW - 31 * 60_000;
|
|
234
|
+
docs.save(join(docsDir, `${docsId}.jsonl`));
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// history: two weeks of finished sessions for statistics
|
|
237
|
+
const PROJECTS = [SHOP, DOCS, '/Users/demo/code/billing-service', '/Users/demo/code/mobile-app'];
|
|
238
|
+
const MODELS = ['claude-sonnet-5', 'claude-sonnet-5', 'claude-sonnet-5', 'claude-opus-5-5', 'claude-opus-5', 'claude-haiku-4-5'];
|
|
239
|
+
const TOPICS = ['Refactor pricing module', 'Add retry to webhook sender', 'Upgrade to Node 22', 'Fix flaky login test', 'Write migration for orders table', 'Improve search ranking', 'Add dark mode toggle', 'Document the public API', 'Investigate memory leak', 'Split monolith config'];
|
|
240
|
+
const FILES = ['src/index.ts', 'src/api/routes.ts', 'src/db/schema.ts', 'README.md', 'src/ui/Button.tsx', 'package.json', 'src/jobs/webhook.ts', 'tests/e2e/login.spec.ts'];
|
|
241
|
+
for (let d = 14; d >= 1; d--) {
|
|
242
|
+
const n = 1 + Math.floor(rnd() * 3);
|
|
243
|
+
for (let k = 0; k < n; k++) {
|
|
244
|
+
const cwd = pick(PROJECTS);
|
|
245
|
+
const sid = uuid();
|
|
246
|
+
const start = NOW - d * 86_400_000 + (8 + Math.floor(rnd() * 11)) * 3_600_000 + Math.floor(rnd() * 3_600_000);
|
|
247
|
+
const tr = new Transcript({ sessionId: sid, cwd, start, model: pick(MODELS) });
|
|
248
|
+
const topic = pick(TOPICS);
|
|
249
|
+
tr.meta('ai-title', { aiTitle: topic });
|
|
250
|
+
tr.prompt(`${topic}.`);
|
|
251
|
+
const calls = 8 + Math.floor(rnd() * 40);
|
|
252
|
+
for (let c = 0; c < calls; c++) {
|
|
253
|
+
const f = `${cwd}/${pick(FILES)}`;
|
|
254
|
+
const r = rnd();
|
|
255
|
+
if (r < 0.4)
|
|
256
|
+
tr.tool('Bash', { command: pick(['npm test', 'npm run build', 'git status', 'git diff --stat', 'npm run lint']), description: pick(['Run tests', 'Build', 'Check status', 'Show diff', 'Lint']) }, bash('ok', rnd() < 0.06, 2 + rnd() * 30));
|
|
257
|
+
else if (r < 0.65)
|
|
258
|
+
tr.tool('Read', { file_path: f }, read(f, '// …'));
|
|
259
|
+
else if (r < 0.85)
|
|
260
|
+
tr.tool('Edit', { file_path: f, old_string: 'a', new_string: 'b' }, edit(f, 10, ['-a', '+b']));
|
|
261
|
+
else if (r < 0.95)
|
|
262
|
+
tr.tool('Grep', { pattern: pick(['TODO', 'retry', 'price', 'auth']) }, bash('3 matches'));
|
|
263
|
+
else
|
|
264
|
+
tr.tool('Write', { file_path: f, content: '…' }, { text: 'File created successfully', structured: { type: 'create', filePath: f, content: '', structuredPatch: [] } });
|
|
265
|
+
}
|
|
266
|
+
tr.say(`Done: ${topic.toLowerCase()}.`, true);
|
|
267
|
+
tr.save(join(ROOT, 'projects', slug(cwd), `${sid}.jsonl`));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// tasks of the orchestrator
|
|
271
|
+
write(join(ROOT, 'tasks', orchId, '1.json'), JSON.stringify({ id: '1', subject: 'Implement coupon codes (spec 0012)', description: 'Worker implements T1–T6', activeForm: 'Implementing coupon codes', status: 'in_progress', blocks: ['2'], blockedBy: [] }));
|
|
272
|
+
write(join(ROOT, 'tasks', orchId, '2.json'), JSON.stringify({ id: '2', subject: 'Review implementation', description: 'Independent review against the spec', status: 'pending', blocks: [], blockedBy: ['1'] }));
|
|
273
|
+
write(join(ROOT, 'tasks', orchId, '3.json'), JSON.stringify({ id: '3', subject: 'Write spec 0012', status: 'completed', blocks: [], blockedBy: [] }));
|
|
274
|
+
// prompt history
|
|
275
|
+
write(join(ROOT, 'history.jsonl'), [orch, docs].map((t) => JSON.stringify({ display: t.lines.find((l) => l.type === 'user').message.content, timestamp: t.o.start, project: t.o.cwd, sessionId: t.o.sessionId, pastedContents: {} })).join('\n') + '\n');
|
|
276
|
+
return { orch, docs, orchId, implId, docsId, implStart, SHOP, DOCS };
|
|
277
|
+
}
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
// processes: real (sleeping) processes so the liveness check sees them running
|
|
280
|
+
function procStart(pid) {
|
|
281
|
+
return execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], { env: { ...process.env, TZ: 'UTC', LC_ALL: 'C' } }).toString().trim();
|
|
282
|
+
}
|
|
283
|
+
function sessionFile(pid, data) {
|
|
284
|
+
write(join(ROOT, 'sessions', `${pid}.json`), JSON.stringify({ pid, procStart: procStart(pid), version: VERSION, peerProtocol: 1, pidDomain: process.platform, updatedAt: NOW, statusUpdatedAt: NOW - 20_000, ...data }));
|
|
285
|
+
}
|
|
286
|
+
export async function runDemo(opts) {
|
|
287
|
+
ROOT = opts.out ?? join(existsSync('/tmp') ? '/tmp' : tmpdir(), 'claude-inspect-demo');
|
|
288
|
+
if (!opts.out)
|
|
289
|
+
rmSync(ROOT, { recursive: true, force: true }); // always fresh demo data
|
|
290
|
+
const s = buildScenario();
|
|
291
|
+
if (opts.out) {
|
|
292
|
+
console.log(`Demo data written to ${ROOT} (no processes started)`);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
// orchestrator = shell that forks the implementer (process tree: implementer → orchestrator)
|
|
296
|
+
const children = [
|
|
297
|
+
spawn('sh', ['-c', 'sleep 7200 & wait'], { stdio: 'ignore', detached: true }),
|
|
298
|
+
spawn('sleep', ['7200'], { stdio: 'ignore', detached: true }),
|
|
299
|
+
];
|
|
300
|
+
const [orchProc, docsProc] = children;
|
|
301
|
+
process.on('exit', () => {
|
|
302
|
+
for (const c of children) {
|
|
303
|
+
try {
|
|
304
|
+
process.kill(-c.pid); // whole process group, including the child sleep
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
c.kill();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
312
|
+
const implPid = Number(execFileSync('pgrep', ['-P', String(orchProc.pid)]).toString().trim().split('\n')[0]);
|
|
313
|
+
sessionFile(orchProc.pid, { sessionId: s.orchId, cwd: s.SHOP, startedAt: s.orch.o.start, kind: 'interactive', entrypoint: 'cli', name: 'orchestrator', nameSource: 'user', status: 'busy', bridgeSessionId: 'session_demo' });
|
|
314
|
+
sessionFile(implPid, { sessionId: s.implId, cwd: s.SHOP, startedAt: s.implStart, kind: 'interactive', entrypoint: 'sdk-cli', name: 'acme-shop-impl', nameSource: 'derived', status: 'busy', agent: 'implementer' });
|
|
315
|
+
sessionFile(docsProc.pid, { sessionId: s.docsId, cwd: s.DOCS, startedAt: s.docs.o.start, kind: 'interactive', entrypoint: 'cli', name: 'docs', nameSource: 'user', status: 'idle' });
|
|
316
|
+
console.log(`Demo data in ${ROOT}`);
|
|
317
|
+
// config.ts reads CLAUDE_CONFIG_DIR on import – set it before loading the server
|
|
318
|
+
process.env.CLAUDE_CONFIG_DIR = ROOT;
|
|
319
|
+
const { runServer } = await import('../run.js');
|
|
320
|
+
await runServer({ port: opts.port, open: opts.open, token: process.env.CLAUDE_INSPECT_TOKEN ?? 'demo' });
|
|
321
|
+
}
|