Chat panel with a local Claude Code bridge: app-state, YAML, run and playback tools
12 changed files+2118−25
README.mdmodified+14−2View file
@@ -17,7 +17,18 @@ The sidebar holds the preset selector, YAML import and download, the run control
1717
1818 Start builds the set-up and begins the run. The main area shows the cymbal as a lit height field (drag to orbit, wheel to zoom, the exaggeration slider scales the displacement), the spectrogram of the listener signal growing as chunks of audio arrive, and a playback bar. Play plays the part of the signal that exists so far from the slider position and stops where the signal ends; press again to hear newer material. The signal selector switches between the unbaffled estimate (front minus diffracted back, the default) and the baffled Rayleigh integral; the WAV download writes the selected one at 16 bits, normalized to 0.9 full scale or at 1 Pa = 0.05 full scale. Under the views, the energy table (kinetic, membrane, bending, exciters, and the nonlinearity meter max|w|/h) and the hit table (peak force, contact time, exciter velocity) update about once a second, and the log reports the set-up and the run.
1919
20-URL parameters: `?preset=<id>` selects a preset (`crash_mallet`, `crash_stick`, `hit_train`, `quick_cpu`), `backend=cpu` forces the CPU loop, `autostart=1` starts on load.
20+URL parameters: `?preset=<id>` selects a preset (`crash_mallet`, `crash_stick`, `hit_train`, `quick_cpu`), `backend=cpu` forces the CPU loop, `autostart=1` starts on load, `chat=1` opens the chat panel, `bridge=ws://host:port` points it at a bridge other than `ws://localhost:8765`.
21+
22+## Chat with Claude Code
23+
24+The Chat button opens a panel in which Claude Code talks about the app, reads its state, and changes it: it can write a configuration from a description ("a thin 20 inch ride hit with a stick near the edge, then a mallet stroke a second later"), load a preset, start, pause and stop runs, play the sound, and interpret a finished run (contact force, nonlinearity, band onsets and centroid, that is, whether the upward cascade happened). It has read access to this repository, so it can answer questions about the method from the source and from THEORY.md.
25+
26+A web page cannot run Claude Code, so the panel talks to a small bridge process on your machine, which uses your own Claude Code login (subscription or API key, whatever `claude` is logged in with):
27+
28+ git clone https://vault1.magland.org/proof-of-concept/cymsimweb
29+ cd cymsimweb && npm install && npm run bridge
30+
31+The bridge listens on `ws://localhost:8765` (127.0.0.1 only) and accepts connections from the deployed site and from localhost pages; it also serves the built app at `http://localhost:8765` after `npm run build`. Open the site, press Chat, and the panel connects. `node bridge/server.mjs --port 9000 --model claude-sonnet-5 --origin https://other.site` changes the port, the model (default: your Claude Code default) and the allowed page origins. Each browser connection is one Claude Code session that persists across turns; New starts a fresh one, Stop interrupts the current turn. The bridge gives Claude the read-only tools (Read, Glob, Grep, WebFetch) plus the app tools `get_app_state`, `set_yaml`, `select_preset`, `start_run`, `pause_run`, `stop_run`, `play_audio` and `set_signal`, which execute in the page; it cannot edit files or run commands. `node tests/bridge.smoke.mjs` runs one real turn against a scripted fake page (it uses your quota, so it is not part of `npm test`).
2132
2233 ## Speed
2334
@@ -36,5 +47,6 @@ At the default resolution (6 mm elements, about 4700 nodes, 54 sub-steps per aud
3647 | `src/cpu/` | float64 reference time loop, also the fallback without WebGPU |
3748 | `src/gpu/` | WGSL kernels and the WebGPU driver |
3849 | `src/analysis/`, `src/audio/` | incremental STFT; Web Audio playback and WAV export |
39-| `src/ui/`, `src/run.ts`, `src/main.ts` | editor, form, plate and spectrogram views, readouts, the run loop, wiring |
50+| `src/ui/`, `src/run.ts`, `src/main.ts` | editor, form, plate and spectrogram views, readouts, chat panel, the run loop, wiring |
51+| `src/chat/`, `bridge/` | WebSocket client of the bridge and the tool executor; the local Claude Code bridge (Agent SDK) |
4052 | `tests/` | node:test files, the Dawn check, and the Python fixture |
bridge/server.mjsadded+221−0View file
@@ -0,0 +1,221 @@
1+#!/usr/bin/env node
2+// Local bridge between the cymsimweb page and a Claude Code session.
3+//
4+// The page runs in a browser and cannot start Claude Code itself, so this process runs on the
5+// user's machine: it drives the Claude Agent SDK (which uses the local `claude` login, i.e. the
6+// user's Claude Code subscription), and relays the tools Claude calls to the page over a
7+// WebSocket, where they read the app's state, replace the YAML, start and stop runs, and play
8+// the audio. It also serves the built site from dist/ so the page can be used from the same
9+// origin. Listens on 127.0.0.1 only and accepts WebSocket connections only from known origins.
10+//
11+// npm run bridge # ws://localhost:8765, site at http://localhost:8765
12+// node bridge/server.mjs --port 9000 --origin https://my.site --model claude-sonnet-5
13+import fs from "node:fs";
14+import http from "node:http";
15+import path from "node:path";
16+import { fileURLToPath } from "node:url";
17+import { WebSocketServer } from "ws";
18+import { z } from "zod";
19+import { createSdkMcpServer, query, tool } from "@anthropic-ai/claude-agent-sdk";
20+import { defaultConfig } from "../src/config/schema.ts";
21+import { PRESETS } from "../src/config/presets.ts";
22+import { configToYaml } from "../src/config/yaml.ts";
23+
24+const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
25+
26+// ---------------------------------------------------------------- arguments
27+const argv = process.argv.slice(2);
28+const opt = (name, def) => { const i = argv.indexOf(name); return i >= 0 && argv[i + 1] !== undefined ? argv[i + 1] : def; };
29+const PORT = Number(opt("--port", process.env.CYMSIMWEB_BRIDGE_PORT ?? 8765));
30+const MODEL = opt("--model", process.env.CYMSIMWEB_BRIDGE_MODEL ?? undefined);
31+const EXTRA_ORIGINS = argv.flatMap((a, i) => (a === "--origin" && argv[i + 1] ? [argv[i + 1]] : []));
32+const ALLOWED_ORIGINS = new Set([
33+ "https://cymsimweb--proof-of-concept.vault1-sites.magland.org",
34+ "https://vault1.magland.org",
35+ ...EXTRA_ORIGINS,
36+]);
37+const isAllowedOrigin = (origin) => {
38+ if (!origin) return true; // non-browser clients (tests); browsers always send Origin
39+ if (ALLOWED_ORIGINS.has(origin)) return true;
40+ return /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin);
41+};
42+
43+// ---------------------------------------------------------------- what Claude is told about the app
44+const read = (f) => { try { return fs.readFileSync(path.join(root, f), "utf8"); } catch { return ""; } };
45+const SYSTEM_APPEND = `
46+# cymsimweb assistant
47+
48+You are embedded in cymsimweb, a browser application that simulates a struck cymbal with a nonlinear
49+finite-element shell model on WebGPU (the companion of the cymsim Python package). The user talks to
50+you in a chat panel beside the app. You can see the app's state and change it through the
51+mcp__cymsimweb__* tools; the tools run inside the page. Your working directory is the app's source
52+repository: read README.md, THEORY.md, src/config/schema.ts (the configuration fields and defaults),
53+src/model/*.ts (geometry, mesh, elements, shell, exciters, radiation) and src/gpu/shaders.ts when a
54+question needs the details. The equations are in the Python package's docs/model.md, which you may
55+fetch at https://vault1.magland.org/proof-of-concept/cymsim/blob/main/docs/model.md . Do not modify
56+files: you have no write access, and the app is changed only through the tools.
57+
58+## Tools
59+- get_app_state: the current YAML, its validation issues, the run state and progress, the set-up
60+ summary of the last run, the latest energies and hit results, and a spectrogram summary (per-octave
61+ band onsets and levels, spectral centroid over time). Call it before answering questions about
62+ what is going on, and after a run to interpret the result.
63+- set_yaml: replace the configuration in the editor with a complete YAML document. Omitted fields take
64+ the defaults, so a short document is fine, but write every field the user asked about explicitly.
65+ The tool returns validation issues; fix them and call again if there are any. Units are SI.
66+- select_preset: load one of the built-in configurations.
67+- start_run / pause_run / stop_run: control the simulation. A run takes tens of seconds to minutes
68+ depending on the GPU; start_run returns immediately, so check progress with get_app_state.
69+- play_audio: play the generated sound from a time; set_signal chooses the unbaffled or the baffled
70+ (Rayleigh) listener signal for the spectrogram and playback.
71+
72+## Guidance
73+When the user describes a cymbal or a stroke in words, translate it into the configuration: size sets
74+profile.radius (18 inch crash: 0.2286 m; 20 inch ride: 0.254 m), weight sets the thicknesses (thin
75+crash h_rim 0.8-1.0 mm, medium 1.0-1.2 mm, ride 1.6-2.0 mm at the rim, thicker toward the bell),
76+bell size sets bell_height and bell_shoulder_radius, a stick shoulder hit is a "stick" exciter near
77+the rim (r about 0.85 R) and a soft mallet stroke a "mallet" exciter on the bow (r about 0.6 R).
78+Loudness is the exciter velocity (stick 4-9 m/s, mallet 3-8 m/s). Losses: sigma0 sets the decay of the
79+low modes (T60 about 6.9/sigma0 s), sigma1 the loss growing with frequency. Numerics: element_size
80+0.006 m is the default resolution and needs a discrete GPU; 0.01-0.02 m for quick previews; duration
81+is the simulated seconds. Several hits go in the hits list, each with its own time, position, velocity
82+and exciter. After setting a configuration, say briefly what you set and offer to start the run
83+(or start it if the user asked for that). When a run finishes, get_app_state and describe the
84+result: peak contact force, contact time, how nonlinear (max|w|/h), and whether the band onsets show
85+the upward cascade (high bands rising later than low ones).
86+
87+Keep replies short and concrete. Do not paste the whole YAML into the chat unless asked; the editor
88+shows it.
89+
90+## The YAML schema (every field with its default)
91+\`\`\`yaml
92+${configToYaml(defaultConfig())}\`\`\`
93+
94+## Presets
95+${PRESETS.map((p) => `- ${p.id}: ${p.name}`).join("\n")}
96+
97+## README.md
98+${read("README.md")}
99+`;
100+
101+// ---------------------------------------------------------------- one chat session per socket
102+class Session {
103+ constructor(ws) {
104+ this.ws = ws; this.sessionId = null; this.active = null; this.queue = []; this.pending = new Map(); this.nextId = 1;
105+ const relay = (name, description, schema, extras) => tool(name, description, schema, async (args) => this.callBrowser(name, args), { alwaysLoad: true, ...extras });
106+ this.server = createSdkMcpServer({
107+ name: "cymsimweb", version: "1.0.0",
108+ tools: [
109+ relay("get_app_state", "Read the app's state: current YAML and validation issues, run state and progress, set-up summary, latest energies and hit results, spectrogram band summary.", {}, { annotations: { readOnlyHint: true } }),
110+ relay("set_yaml", "Replace the configuration YAML in the editor. Returns validation issues (empty when valid).", { yaml: z.string().describe("Complete YAML document; omitted fields take the defaults") }),
111+ relay("select_preset", "Load a built-in preset into the editor.", { id: z.enum(PRESETS.map((p) => p.id)) }),
112+ relay("start_run", "Build the set-up from the current YAML and start the simulation. Returns immediately.", {}),
113+ relay("pause_run", "Pause or continue the running simulation.", {}),
114+ relay("stop_run", "Stop the running simulation, keeping the audio generated so far.", {}),
115+ relay("play_audio", "Play the generated sound in the browser from a time in seconds.", { from_seconds: z.number().min(0).default(0) }),
116+ relay("set_signal", "Choose which listener signal the spectrogram and playback use.", { signal: z.enum(["unbaffled", "rayleigh"]) }),
117+ ],
118+ });
119+ }
120+
121+ send(obj) { if (this.ws.readyState === this.ws.OPEN) this.ws.send(JSON.stringify(obj)); }
122+
123+ /** Ask the page to run a tool and wait for its answer. */
124+ callBrowser(name, args) {
125+ const id = this.nextId++;
126+ this.send({ type: "tool_call", id, name, args });
127+ return new Promise((resolve) => {
128+ const timer = setTimeout(() => { this.pending.delete(id); resolve({ content: [{ type: "text", text: "the page did not answer within 120 s" }], isError: true }); }, 120000);
129+ this.pending.set(id, (result) => { clearTimeout(timer); resolve(result); });
130+ });
131+ }
132+
133+ onMessage(raw) {
134+ let msg;
135+ try { msg = JSON.parse(raw); } catch { return; }
136+ if (msg.type === "tool_result") {
137+ const cb = this.pending.get(msg.id);
138+ if (cb) { this.pending.delete(msg.id); cb({ content: [{ type: "text", text: typeof msg.text === "string" ? msg.text : JSON.stringify(msg.text) }], isError: !!msg.isError }); }
139+ } else if (msg.type === "user" && typeof msg.text === "string" && msg.text.trim()) {
140+ if (this.active) this.queue.push(msg.text); else void this.run(msg.text);
141+ } else if (msg.type === "interrupt") {
142+ this.queue = []; void this.active?.interrupt();
143+ } else if (msg.type === "reset") {
144+ this.queue = []; void this.active?.interrupt(); this.sessionId = null; this.send({ type: "reset" });
145+ }
146+ }
147+
148+ async run(text) {
149+ const q = query({
150+ prompt: text,
151+ options: {
152+ cwd: root,
153+ resume: this.sessionId ?? undefined,
154+ includePartialMessages: true,
155+ systemPrompt: { type: "preset", preset: "claude_code", append: SYSTEM_APPEND },
156+ mcpServers: { cymsimweb: this.server },
157+ tools: ["Read", "Glob", "Grep", "WebFetch"],
158+ allowedTools: ["Read", "Glob", "Grep", "WebFetch", "mcp__cymsimweb__*"],
159+ permissionMode: "dontAsk",
160+ maxTurns: 40,
161+ ...(MODEL ? { model: MODEL } : {}),
162+ },
163+ });
164+ this.active = q;
165+ this.send({ type: "status", state: "working" });
166+ try {
167+ for await (const m of q) {
168+ if (m.type === "system" && m.subtype === "init") { this.sessionId = m.session_id; this.send({ type: "init", session_id: m.session_id, model: m.model }); }
169+ else if (m.type === "stream_event") {
170+ const e = m.event;
171+ if (e.type === "content_block_delta" && e.delta?.type === "text_delta" && m.parent_tool_use_id === null) this.send({ type: "delta", text: e.delta.text });
172+ else if (e.type === "content_block_start" && e.content_block?.type === "text" && m.parent_tool_use_id === null) this.send({ type: "block_start" });
173+ } else if (m.type === "assistant") {
174+ for (const block of m.message.content) {
175+ if (block.type === "tool_use") this.send({ type: "tool_use", name: block.name.replace(/^mcp__cymsimweb__/, ""), input: summarize(block.input) });
176+ }
177+ } else if (m.type === "result") {
178+ this.sessionId = m.session_id;
179+ this.send({ type: "result", ok: m.subtype === "success", error: m.subtype === "success" ? undefined : m.subtype, cost: m.total_cost_usd, turns: m.num_turns });
180+ }
181+ }
182+ } catch (err) {
183+ this.send({ type: "error", message: String(err?.message ?? err) });
184+ } finally {
185+ this.active = null;
186+ this.send({ type: "status", state: "idle" });
187+ const next = this.queue.shift();
188+ if (next) void this.run(next);
189+ }
190+ }
191+}
192+
193+function summarize(input) {
194+ const s = JSON.stringify(input ?? {});
195+ return s.length > 160 ? s.slice(0, 157) + "..." : s;
196+}
197+
198+// ---------------------------------------------------------------- static site + WebSocket
199+const MIME = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".json": "application/json", ".svg": "image/svg+xml", ".png": "image/png", ".ico": "image/x-icon", ".wasm": "application/wasm", ".map": "application/json" };
200+const dist = path.join(root, "dist");
201+const server = http.createServer((req, res) => {
202+ const url = new URL(req.url ?? "/", "http://localhost");
203+ let file = path.normalize(path.join(dist, decodeURIComponent(url.pathname)));
204+ if (!file.startsWith(dist)) { res.writeHead(403); res.end(); return; }
205+ if (url.pathname === "/" || !fs.existsSync(file) || fs.statSync(file).isDirectory()) file = path.join(dist, "index.html");
206+ if (!fs.existsSync(file)) { res.writeHead(404, { "content-type": "text/plain" }); res.end("dist/ not found: run `npm run build` first, or use the deployed site with this bridge"); return; }
207+ res.writeHead(200, { "content-type": MIME[path.extname(file)] ?? "application/octet-stream" });
208+ fs.createReadStream(file).pipe(res);
209+});
210+const wss = new WebSocketServer({ server, verifyClient: ({ origin }) => isAllowedOrigin(origin) });
211+wss.on("connection", (ws, req) => {
212+ const session = new Session(ws);
213+ console.log(`[bridge] page connected from ${req.headers.origin ?? "no origin"}`);
214+ ws.send(JSON.stringify({ type: "hello", cwd: root, model: MODEL ?? "default", version: 1 }));
215+ ws.on("message", (data) => session.onMessage(data.toString()));
216+ ws.on("close", () => { void session.active?.interrupt(); console.log("[bridge] page disconnected"); });
217+});
218+server.listen(PORT, "127.0.0.1", () => {
219+ console.log(`[bridge] Claude Code bridge for cymsimweb on ws://localhost:${PORT} (site: http://localhost:${PORT}, model: ${MODEL ?? "Claude Code default"})`);
220+ console.log(`[bridge] accepting pages from localhost and ${[...ALLOWED_ORIGINS].join(", ")}`);
221+});
index.htmlmodified+3−0View file
@@ -12,6 +12,7 @@
1212 <header>
1313 <h1>cymsimweb</h1>
1414 <span class="subtitle">struck-cymbal simulation on WebGPU</span>
15+ <button id="chat-toggle" class="chat-toggle" title="chat with Claude Code about this app">Chat</button>
1516 </header>
1617 <section class="run">
1718 <div class="row">
@@ -72,6 +73,8 @@
7273 </section>
7374 <section id="readouts"></section>
7475 </main>
76+ <div class="splitter vertical hidden" id="chat-splitter" data-target="chat" data-var="--chat-w" data-invert="1"></div>
77+ <aside id="chat" class="hidden"></aside>
7578 </div>
7679 <script type="module" src="./src/main.ts"></script>
7780 </body>
package-lock.jsonmodified+1505−10View file
@@ -8,13 +8,16 @@
88 "name": "cymsimweb",
99 "version": "0.1.0",
1010 "dependencies": {
11+ "@anthropic-ai/claude-agent-sdk": "^0.3.280",
1112 "@codemirror/commands": "^6.11.0",
1213 "@codemirror/language": "^6.12.4",
1314 "@codemirror/legacy-modes": "^6.5.4",
1415 "@codemirror/state": "^6.7.4",
1516 "@codemirror/view": "^6.43.11",
1617 "@lezer/highlight": "^1.2.3",
17- "js-yaml": "^4.1.0"
18+ "js-yaml": "^4.1.0",
19+ "ws": "^8.21.3",
20+ "zod": "^4.6.5"
1821 },
1922 "devDependencies": {
2023 "@types/js-yaml": "^4.0.9",
@@ -25,6 +28,178 @@
2528 "webgpu": "^0.4.0"
2629 }
2730 },
31+ "node_modules/@anthropic-ai/claude-agent-sdk": {
32+ "version": "0.3.280",
33+ "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.280.tgz",
34+ "integrity": "sha512-aIQSTKcCJcOgi125GAGQaNZUYgxbEBQwV2Ac+Utp0+gGUjIEWjTqz4B8kXr9XjkcCTYiRUMTDsXPAUfIcBDjnw==",
35+ "license": "SEE LICENSE IN README.md",
36+ "engines": {
37+ "node": ">=18.0.0"
38+ },
39+ "optionalDependencies": {
40+ "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.280",
41+ "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.280",
42+ "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.280",
43+ "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.280",
44+ "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.280",
45+ "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.280",
46+ "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.280",
47+ "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.280"
48+ },
49+ "peerDependencies": {
50+ "@anthropic-ai/sdk": ">=0.93.0",
51+ "@modelcontextprotocol/sdk": "^1.29.0",
52+ "zod": "^4.0.0"
53+ }
54+ },
55+ "node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": {
56+ "version": "0.3.280",
57+ "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.280.tgz",
58+ "integrity": "sha512-Yws14X5g5hgDtF565Tld/+dILAhH9QZpundjKMyO3dFasq9miXZlhU0NwXqwTO2xP0jDjJ54zduIiJwkcfcvGw==",
59+ "cpu": [
60+ "arm64"
61+ ],
62+ "license": "SEE LICENSE IN LICENSE.md",
63+ "optional": true,
64+ "os": [
65+ "darwin"
66+ ]
67+ },
68+ "node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": {
69+ "version": "0.3.280",
70+ "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.280.tgz",
71+ "integrity": "sha512-B1eLx/oZ5RL1c3a13nQ4cRUksWyhGcVNS5vbCHnOVu4Pz+dH0moZ+iY8VaE6FvikVAQwFEW6VDU00b6m3tYdsQ==",
72+ "cpu": [
73+ "x64"
74+ ],
75+ "license": "SEE LICENSE IN LICENSE.md",
76+ "optional": true,
77+ "os": [
78+ "darwin"
79+ ]
80+ },
81+ "node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": {
82+ "version": "0.3.280",
83+ "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.280.tgz",
84+ "integrity": "sha512-6s96OIvBHT2817hCFLIZ421JBpQnvrmZucpjlWsyCODKmwGsSxiEhlWqK2h5Q7lwS6F7bzeAhqjGvC7FzDl9Rw==",
85+ "cpu": [
86+ "arm64"
87+ ],
88+ "libc": [
89+ "glibc"
90+ ],
91+ "license": "SEE LICENSE IN LICENSE.md",
92+ "optional": true,
93+ "os": [
94+ "linux"
95+ ]
96+ },
97+ "node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": {
98+ "version": "0.3.280",
99+ "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.280.tgz",
100+ "integrity": "sha512-uDh+Ggjb36l+jcVgaYxSziGHmBSSR3clqvGlQ0Bgc1v5dLF4igwqZ6ZPW/hS9dArG27fislIOetH1waEgIr+GA==",
101+ "cpu": [
102+ "arm64"
103+ ],
104+ "libc": [
105+ "musl"
106+ ],
107+ "license": "SEE LICENSE IN LICENSE.md",
108+ "optional": true,
109+ "os": [
110+ "linux"
111+ ]
112+ },
113+ "node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": {
114+ "version": "0.3.280",
115+ "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.280.tgz",
116+ "integrity": "sha512-vpPyxYLy+zNc7LwQDWsZf0GZtTYa0PJLxzuv2vK9iV2dG+hrbRNTl35NSKKRR9rMj6RpKz0i2NLyVQ+D4mREgw==",
117+ "cpu": [
118+ "x64"
119+ ],
120+ "libc": [
121+ "glibc"
122+ ],
123+ "license": "SEE LICENSE IN LICENSE.md",
124+ "optional": true,
125+ "os": [
126+ "linux"
127+ ]
128+ },
129+ "node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": {
130+ "version": "0.3.280",
131+ "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.280.tgz",
132+ "integrity": "sha512-IVbIgvi5C0sdLg/p9hpiVvAQ/ikvTKwOOEy5C1aT7w8XLqBgfpJbzzVuk06df1Q0HlILUmf63EKPmq48lbxtqA==",
133+ "cpu": [
134+ "x64"
135+ ],
136+ "libc": [
137+ "musl"
138+ ],
139+ "license": "SEE LICENSE IN LICENSE.md",
140+ "optional": true,
141+ "os": [
142+ "linux"
143+ ]
144+ },
145+ "node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": {
146+ "version": "0.3.280",
147+ "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.280.tgz",
148+ "integrity": "sha512-heTflHwciJF+Hrf7v0J53QBzD+9mxN9gt2VVmEJAN0+ejbPcOcqnGGZHvyc9aZUuGh5INN0ECaiL4Fn1X2vXQQ==",
149+ "cpu": [
150+ "arm64"
151+ ],
152+ "license": "SEE LICENSE IN LICENSE.md",
153+ "optional": true,
154+ "os": [
155+ "win32"
156+ ]
157+ },
158+ "node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": {
159+ "version": "0.3.280",
160+ "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.280.tgz",
161+ "integrity": "sha512-N+Y1zJb19nx0oPTvzkl/E3ng9AtWGW+K4hySmSHCgYSsgIwfs1YzDj/F5eHDMrxgklTuzFp76TvL3S75z5ge+g==",
162+ "cpu": [
163+ "x64"
164+ ],
165+ "license": "SEE LICENSE IN LICENSE.md",
166+ "optional": true,
167+ "os": [
168+ "win32"
169+ ]
170+ },
171+ "node_modules/@anthropic-ai/sdk": {
172+ "version": "0.128.0",
173+ "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.128.0.tgz",
174+ "integrity": "sha512-tl5cBFZC1jVFrTuQyvQObA4WmuNgcYVeXfriqOumgtLAHUm4gPUj18YnszBW60v0PrjDB8nBDOC6wd9CmHQFlA==",
175+ "license": "MIT",
176+ "peer": true,
177+ "dependencies": {
178+ "json-schema-to-ts": "^3.1.1",
179+ "standardwebhooks": "^1.0.0"
180+ },
181+ "bin": {
182+ "anthropic-ai-sdk": "bin/cli"
183+ },
184+ "peerDependencies": {
185+ "zod": "^3.25.0 || ^4.0.0"
186+ },
187+ "peerDependenciesMeta": {
188+ "zod": {
189+ "optional": true
190+ }
191+ }
192+ },
193+ "node_modules/@babel/runtime": {
194+ "version": "7.29.7",
195+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
196+ "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
197+ "license": "MIT",
198+ "peer": true,
199+ "engines": {
200+ "node": ">=6.9.0"
201+ }
202+ },
28203 "node_modules/@codemirror/commands": {
29204 "version": "6.11.1",
30205 "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.11.1.tgz",
@@ -523,6 +698,19 @@
523698 "node": ">=18"
524699 }
525700 },
701+ "node_modules/@hono/node-server": {
702+ "version": "2.1.1",
703+ "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz",
704+ "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==",
705+ "license": "MIT",
706+ "peer": true,
707+ "engines": {
708+ "node": ">=20"
709+ },
710+ "peerDependencies": {
711+ "hono": "^4"
712+ }
713+ },
526714 "node_modules/@lezer/common": {
527715 "version": "1.5.2",
528716 "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz",
@@ -553,6 +741,47 @@
553741 "integrity": "sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==",
554742 "license": "MIT"
555743 },
744+ "node_modules/@modelcontextprotocol/sdk": {
745+ "version": "1.30.0",
746+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz",
747+ "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==",
748+ "license": "MIT",
749+ "peer": true,
750+ "dependencies": {
751+ "@hono/node-server": "^1.19.9 || ^2.0.5",
752+ "ajv": "^8.17.1",
753+ "ajv-formats": "^3.0.1",
754+ "content-type": "^1.0.5",
755+ "cors": "^2.8.5",
756+ "cross-spawn": "^7.0.5",
757+ "eventsource": "^3.0.2",
758+ "eventsource-parser": "^3.0.0",
759+ "express": "^5.2.1",
760+ "express-rate-limit": "^8.2.1",
761+ "hono": "^4.11.4",
762+ "jose": "^6.1.3",
763+ "json-schema-typed": "^8.0.2",
764+ "pkce-challenge": "^5.0.0",
765+ "raw-body": "^3.0.0",
766+ "zod": "^3.25 || ^4.0",
767+ "zod-to-json-schema": "^3.25.1"
768+ },
769+ "engines": {
770+ "node": ">=18"
771+ },
772+ "peerDependencies": {
773+ "@cfworker/json-schema": "^4.1.1",
774+ "zod": "^3.25 || ^4.0"
775+ },
776+ "peerDependenciesMeta": {
777+ "@cfworker/json-schema": {
778+ "optional": true
779+ },
780+ "zod": {
781+ "optional": false
782+ }
783+ }
784+ },
556785 "node_modules/@napi-rs/lzma-linux-x64-gnu": {
557786 "version": "1.5.1",
558787 "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
@@ -962,6 +1191,13 @@
9621191 "win32"
9631192 ]
9641193 },
1194+ "node_modules/@stablelib/base64": {
1195+ "version": "1.0.1",
1196+ "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
1197+ "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
1198+ "license": "MIT",
1199+ "peer": true
1200+ },
9651201 "node_modules/@types/estree": {
9661202 "version": "1.0.9",
9671203 "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
@@ -993,23 +1229,228 @@
9931229 "dev": true,
9941230 "license": "BSD-3-Clause"
9951231 },
1232+ "node_modules/accepts": {
1233+ "version": "2.0.0",
1234+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
1235+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
1236+ "license": "MIT",
1237+ "peer": true,
1238+ "dependencies": {
1239+ "mime-types": "^3.0.0",
1240+ "negotiator": "^1.0.0"
1241+ },
1242+ "engines": {
1243+ "node": ">= 0.6"
1244+ }
1245+ },
1246+ "node_modules/ajv": {
1247+ "version": "8.20.0",
1248+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
1249+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
1250+ "license": "MIT",
1251+ "peer": true,
1252+ "dependencies": {
1253+ "fast-deep-equal": "^3.1.3",
1254+ "fast-uri": "^3.0.1",
1255+ "json-schema-traverse": "^1.0.0",
1256+ "require-from-string": "^2.0.2"
1257+ },
1258+ "funding": {
1259+ "type": "github",
1260+ "url": "https://github.com/sponsors/epoberezkin"
1261+ }
1262+ },
1263+ "node_modules/ajv-formats": {
1264+ "version": "3.0.1",
1265+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
1266+ "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
1267+ "license": "MIT",
1268+ "peer": true,
1269+ "dependencies": {
1270+ "ajv": "^8.0.0"
1271+ },
1272+ "peerDependencies": {
1273+ "ajv": "^8.0.0"
1274+ },
1275+ "peerDependenciesMeta": {
1276+ "ajv": {
1277+ "optional": true
1278+ }
1279+ }
1280+ },
9961281 "node_modules/argparse": {
9971282 "version": "2.0.1",
9981283 "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
9991284 "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
10001285 "license": "Python-2.0"
10011286 },
1287+ "node_modules/body-parser": {
1288+ "version": "2.3.0",
1289+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
1290+ "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
1291+ "license": "MIT",
1292+ "peer": true,
1293+ "dependencies": {
1294+ "bytes": "^3.1.2",
1295+ "content-type": "^2.0.0",
1296+ "debug": "^4.4.3",
1297+ "http-errors": "^2.0.1",
1298+ "iconv-lite": "^0.7.2",
1299+ "on-finished": "^2.4.1",
1300+ "qs": "^6.15.2",
1301+ "raw-body": "^3.0.2",
1302+ "type-is": "^2.1.0"
1303+ },
1304+ "engines": {
1305+ "node": ">=18"
1306+ },
1307+ "funding": {
1308+ "type": "opencollective",
1309+ "url": "https://opencollective.com/express"
1310+ }
1311+ },
1312+ "node_modules/body-parser/node_modules/content-type": {
1313+ "version": "2.1.0",
1314+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
1315+ "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
1316+ "license": "MIT",
1317+ "peer": true,
1318+ "engines": {
1319+ "node": ">=18"
1320+ },
1321+ "funding": {
1322+ "type": "opencollective",
1323+ "url": "https://opencollective.com/express"
1324+ }
1325+ },
1326+ "node_modules/bytes": {
1327+ "version": "3.1.2",
1328+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
1329+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
1330+ "license": "MIT",
1331+ "peer": true,
1332+ "engines": {
1333+ "node": ">= 0.8"
1334+ }
1335+ },
1336+ "node_modules/call-bind-apply-helpers": {
1337+ "version": "1.0.2",
1338+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
1339+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
1340+ "license": "MIT",
1341+ "peer": true,
1342+ "dependencies": {
1343+ "es-errors": "^1.3.0",
1344+ "function-bind": "^1.1.2"
1345+ },
1346+ "engines": {
1347+ "node": ">= 0.4"
1348+ }
1349+ },
1350+ "node_modules/call-bound": {
1351+ "version": "1.0.4",
1352+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
1353+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
1354+ "license": "MIT",
1355+ "peer": true,
1356+ "dependencies": {
1357+ "call-bind-apply-helpers": "^1.0.2",
1358+ "get-intrinsic": "^1.3.0"
1359+ },
1360+ "engines": {
1361+ "node": ">= 0.4"
1362+ },
1363+ "funding": {
1364+ "url": "https://github.com/sponsors/ljharb"
1365+ }
1366+ },
1367+ "node_modules/content-disposition": {
1368+ "version": "1.1.0",
1369+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
1370+ "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
1371+ "license": "MIT",
1372+ "peer": true,
1373+ "engines": {
1374+ "node": ">=18"
1375+ },
1376+ "funding": {
1377+ "type": "opencollective",
1378+ "url": "https://opencollective.com/express"
1379+ }
1380+ },
1381+ "node_modules/content-type": {
1382+ "version": "1.0.5",
1383+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
1384+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
1385+ "license": "MIT",
1386+ "peer": true,
1387+ "engines": {
1388+ "node": ">= 0.6"
1389+ }
1390+ },
1391+ "node_modules/cookie": {
1392+ "version": "0.7.2",
1393+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
1394+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
1395+ "license": "MIT",
1396+ "peer": true,
1397+ "engines": {
1398+ "node": ">= 0.6"
1399+ }
1400+ },
1401+ "node_modules/cookie-signature": {
1402+ "version": "1.2.2",
1403+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
1404+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
1405+ "license": "MIT",
1406+ "peer": true,
1407+ "engines": {
1408+ "node": ">=6.6.0"
1409+ }
1410+ },
1411+ "node_modules/cors": {
1412+ "version": "2.8.6",
1413+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
1414+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
1415+ "license": "MIT",
1416+ "peer": true,
1417+ "dependencies": {
1418+ "object-assign": "^4",
1419+ "vary": "^1"
1420+ },
1421+ "engines": {
1422+ "node": ">= 0.10"
1423+ },
1424+ "funding": {
1425+ "type": "opencollective",
1426+ "url": "https://opencollective.com/express"
1427+ }
1428+ },
10021429 "node_modules/crelt": {
10031430 "version": "1.0.7",
10041431 "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz",
10051432 "integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==",
10061433 "license": "MIT"
10071434 },
1435+ "node_modules/cross-spawn": {
1436+ "version": "7.0.6",
1437+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
1438+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
1439+ "license": "MIT",
1440+ "peer": true,
1441+ "dependencies": {
1442+ "path-key": "^3.1.0",
1443+ "shebang-command": "^2.0.0",
1444+ "which": "^2.0.1"
1445+ },
1446+ "engines": {
1447+ "node": ">= 8"
1448+ }
1449+ },
10081450 "node_modules/debug": {
10091451 "version": "4.4.3",
10101452 "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
10111453 "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1012- "dev": true,
10131454 "license": "MIT",
10141455 "dependencies": {
10151456 "ms": "^2.1.3"
@@ -1023,6 +1464,81 @@
10231464 }
10241465 }
10251466 },
1467+ "node_modules/depd": {
1468+ "version": "2.0.0",
1469+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
1470+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
1471+ "license": "MIT",
1472+ "peer": true,
1473+ "engines": {
1474+ "node": ">= 0.8"
1475+ }
1476+ },
1477+ "node_modules/dunder-proto": {
1478+ "version": "1.0.1",
1479+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
1480+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
1481+ "license": "MIT",
1482+ "peer": true,
1483+ "dependencies": {
1484+ "call-bind-apply-helpers": "^1.0.1",
1485+ "es-errors": "^1.3.0",
1486+ "gopd": "^1.2.0"
1487+ },
1488+ "engines": {
1489+ "node": ">= 0.4"
1490+ }
1491+ },
1492+ "node_modules/ee-first": {
1493+ "version": "1.1.1",
1494+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
1495+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
1496+ "license": "MIT",
1497+ "peer": true
1498+ },
1499+ "node_modules/encodeurl": {
1500+ "version": "2.0.0",
1501+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
1502+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
1503+ "license": "MIT",
1504+ "peer": true,
1505+ "engines": {
1506+ "node": ">= 0.8"
1507+ }
1508+ },
1509+ "node_modules/es-define-property": {
1510+ "version": "1.0.1",
1511+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
1512+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
1513+ "license": "MIT",
1514+ "peer": true,
1515+ "engines": {
1516+ "node": ">= 0.4"
1517+ }
1518+ },
1519+ "node_modules/es-errors": {
1520+ "version": "1.3.0",
1521+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
1522+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
1523+ "license": "MIT",
1524+ "peer": true,
1525+ "engines": {
1526+ "node": ">= 0.4"
1527+ }
1528+ },
1529+ "node_modules/es-object-atoms": {
1530+ "version": "1.1.2",
1531+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
1532+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
1533+ "license": "MIT",
1534+ "peer": true,
1535+ "dependencies": {
1536+ "es-errors": "^1.3.0"
1537+ },
1538+ "engines": {
1539+ "node": ">= 0.4"
1540+ }
1541+ },
10261542 "node_modules/esbuild": {
10271543 "version": "0.25.12",
10281544 "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
@@ -1065,16 +1581,151 @@
10651581 "@esbuild/win32-x64": "0.25.12"
10661582 }
10671583 },
1068- "node_modules/fdir": {
1069- "version": "6.5.0",
1070- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
1071- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
1072- "dev": true,
1584+ "node_modules/escape-html": {
1585+ "version": "1.0.3",
1586+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
1587+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
10731588 "license": "MIT",
1589+ "peer": true
1590+ },
1591+ "node_modules/etag": {
1592+ "version": "1.8.1",
1593+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
1594+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
1595+ "license": "MIT",
1596+ "peer": true,
10741597 "engines": {
1075- "node": ">=12.0.0"
1598+ "node": ">= 0.6"
1599+ }
1600+ },
1601+ "node_modules/eventsource": {
1602+ "version": "3.0.7",
1603+ "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
1604+ "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
1605+ "license": "MIT",
1606+ "peer": true,
1607+ "dependencies": {
1608+ "eventsource-parser": "^3.0.1"
10761609 },
1077- "peerDependencies": {
1610+ "engines": {
1611+ "node": ">=18.0.0"
1612+ }
1613+ },
1614+ "node_modules/eventsource-parser": {
1615+ "version": "3.1.1",
1616+ "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz",
1617+ "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==",
1618+ "license": "MIT",
1619+ "peer": true,
1620+ "engines": {
1621+ "node": ">=18.0.0"
1622+ }
1623+ },
1624+ "node_modules/express": {
1625+ "version": "5.2.1",
1626+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
1627+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
1628+ "license": "MIT",
1629+ "peer": true,
1630+ "dependencies": {
1631+ "accepts": "^2.0.0",
1632+ "body-parser": "^2.2.1",
1633+ "content-disposition": "^1.0.0",
1634+ "content-type": "^1.0.5",
1635+ "cookie": "^0.7.1",
1636+ "cookie-signature": "^1.2.1",
1637+ "debug": "^4.4.0",
1638+ "depd": "^2.0.0",
1639+ "encodeurl": "^2.0.0",
1640+ "escape-html": "^1.0.3",
1641+ "etag": "^1.8.1",
1642+ "finalhandler": "^2.1.0",
1643+ "fresh": "^2.0.0",
1644+ "http-errors": "^2.0.0",
1645+ "merge-descriptors": "^2.0.0",
1646+ "mime-types": "^3.0.0",
1647+ "on-finished": "^2.4.1",
1648+ "once": "^1.4.0",
1649+ "parseurl": "^1.3.3",
1650+ "proxy-addr": "^2.0.7",
1651+ "qs": "^6.14.0",
1652+ "range-parser": "^1.2.1",
1653+ "router": "^2.2.0",
1654+ "send": "^1.1.0",
1655+ "serve-static": "^2.2.0",
1656+ "statuses": "^2.0.1",
1657+ "type-is": "^2.0.1",
1658+ "vary": "^1.1.2"
1659+ },
1660+ "engines": {
1661+ "node": ">= 18"
1662+ },
1663+ "funding": {
1664+ "type": "opencollective",
1665+ "url": "https://opencollective.com/express"
1666+ }
1667+ },
1668+ "node_modules/express-rate-limit": {
1669+ "version": "8.7.0",
1670+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.0.tgz",
1671+ "integrity": "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==",
1672+ "license": "MIT",
1673+ "peer": true,
1674+ "dependencies": {
1675+ "debug": "^4.4.3",
1676+ "ip-address": "^10.2.0"
1677+ },
1678+ "engines": {
1679+ "node": ">= 16"
1680+ },
1681+ "funding": {
1682+ "url": "https://github.com/sponsors/express-rate-limit"
1683+ },
1684+ "peerDependencies": {
1685+ "express": ">= 4.11"
1686+ }
1687+ },
1688+ "node_modules/fast-deep-equal": {
1689+ "version": "3.1.3",
1690+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
1691+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
1692+ "license": "MIT",
1693+ "peer": true
1694+ },
1695+ "node_modules/fast-sha256": {
1696+ "version": "1.3.0",
1697+ "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
1698+ "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
1699+ "license": "Unlicense",
1700+ "peer": true
1701+ },
1702+ "node_modules/fast-uri": {
1703+ "version": "3.1.8",
1704+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
1705+ "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
1706+ "funding": [
1707+ {
1708+ "type": "github",
1709+ "url": "https://github.com/sponsors/fastify"
1710+ },
1711+ {
1712+ "type": "opencollective",
1713+ "url": "https://opencollective.com/fastify"
1714+ }
1715+ ],
1716+ "license": "BSD-3-Clause",
1717+ "peer": true
1718+ },
1719+ "node_modules/fdir": {
1720+ "version": "6.5.0",
1721+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
1722+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
1723+ "dev": true,
1724+ "license": "MIT",
1725+ "engines": {
1726+ "node": ">=12.0.0"
1727+ },
1728+ "peerDependencies": {
10781729 "picomatch": "^3 || ^4"
10791730 },
10801731 "peerDependenciesMeta": {
@@ -1083,6 +1734,48 @@
10831734 }
10841735 }
10851736 },
1737+ "node_modules/finalhandler": {
1738+ "version": "2.1.1",
1739+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
1740+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
1741+ "license": "MIT",
1742+ "peer": true,
1743+ "dependencies": {
1744+ "debug": "^4.4.0",
1745+ "encodeurl": "^2.0.0",
1746+ "escape-html": "^1.0.3",
1747+ "on-finished": "^2.4.1",
1748+ "parseurl": "^1.3.3",
1749+ "statuses": "^2.0.1"
1750+ },
1751+ "engines": {
1752+ "node": ">= 18.0.0"
1753+ },
1754+ "funding": {
1755+ "type": "opencollective",
1756+ "url": "https://opencollective.com/express"
1757+ }
1758+ },
1759+ "node_modules/forwarded": {
1760+ "version": "0.2.0",
1761+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
1762+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
1763+ "license": "MIT",
1764+ "peer": true,
1765+ "engines": {
1766+ "node": ">= 0.6"
1767+ }
1768+ },
1769+ "node_modules/fresh": {
1770+ "version": "2.0.0",
1771+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
1772+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
1773+ "license": "MIT",
1774+ "peer": true,
1775+ "engines": {
1776+ "node": ">= 0.8"
1777+ }
1778+ },
10861779 "node_modules/fsevents": {
10871780 "version": "2.3.3",
10881781 "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1098,6 +1791,193 @@
10981791 "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
10991792 }
11001793 },
1794+ "node_modules/function-bind": {
1795+ "version": "1.1.2",
1796+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
1797+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
1798+ "license": "MIT",
1799+ "peer": true,
1800+ "funding": {
1801+ "url": "https://github.com/sponsors/ljharb"
1802+ }
1803+ },
1804+ "node_modules/get-intrinsic": {
1805+ "version": "1.3.0",
1806+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
1807+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
1808+ "license": "MIT",
1809+ "peer": true,
1810+ "dependencies": {
1811+ "call-bind-apply-helpers": "^1.0.2",
1812+ "es-define-property": "^1.0.1",
1813+ "es-errors": "^1.3.0",
1814+ "es-object-atoms": "^1.1.1",
1815+ "function-bind": "^1.1.2",
1816+ "get-proto": "^1.0.1",
1817+ "gopd": "^1.2.0",
1818+ "has-symbols": "^1.1.0",
1819+ "hasown": "^2.0.2",
1820+ "math-intrinsics": "^1.1.0"
1821+ },
1822+ "engines": {
1823+ "node": ">= 0.4"
1824+ },
1825+ "funding": {
1826+ "url": "https://github.com/sponsors/ljharb"
1827+ }
1828+ },
1829+ "node_modules/get-proto": {
1830+ "version": "1.0.1",
1831+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
1832+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
1833+ "license": "MIT",
1834+ "peer": true,
1835+ "dependencies": {
1836+ "dunder-proto": "^1.0.1",
1837+ "es-object-atoms": "^1.0.0"
1838+ },
1839+ "engines": {
1840+ "node": ">= 0.4"
1841+ }
1842+ },
1843+ "node_modules/gopd": {
1844+ "version": "1.2.0",
1845+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
1846+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
1847+ "license": "MIT",
1848+ "peer": true,
1849+ "engines": {
1850+ "node": ">= 0.4"
1851+ },
1852+ "funding": {
1853+ "url": "https://github.com/sponsors/ljharb"
1854+ }
1855+ },
1856+ "node_modules/has-symbols": {
1857+ "version": "1.1.0",
1858+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
1859+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
1860+ "license": "MIT",
1861+ "peer": true,
1862+ "engines": {
1863+ "node": ">= 0.4"
1864+ },
1865+ "funding": {
1866+ "url": "https://github.com/sponsors/ljharb"
1867+ }
1868+ },
1869+ "node_modules/hasown": {
1870+ "version": "2.0.4",
1871+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
1872+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
1873+ "license": "MIT",
1874+ "peer": true,
1875+ "dependencies": {
1876+ "function-bind": "^1.1.2"
1877+ },
1878+ "engines": {
1879+ "node": ">= 0.4"
1880+ }
1881+ },
1882+ "node_modules/hono": {
1883+ "version": "4.13.8",
1884+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.8.tgz",
1885+ "integrity": "sha512-/Gng7NfoykZl2pjukW5Z6+8Yxm3BPRf86GTbQnt0SbySkvax4fyL4H3HhY1cCpBGmiW9XDRFzRV+CXK2W8QudQ==",
1886+ "license": "MIT",
1887+ "peer": true,
1888+ "engines": {
1889+ "node": ">=16.9.0"
1890+ }
1891+ },
1892+ "node_modules/http-errors": {
1893+ "version": "2.0.1",
1894+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
1895+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
1896+ "license": "MIT",
1897+ "peer": true,
1898+ "dependencies": {
1899+ "depd": "~2.0.0",
1900+ "inherits": "~2.0.4",
1901+ "setprototypeof": "~1.2.0",
1902+ "statuses": "~2.0.2",
1903+ "toidentifier": "~1.0.1"
1904+ },
1905+ "engines": {
1906+ "node": ">= 0.8"
1907+ },
1908+ "funding": {
1909+ "type": "opencollective",
1910+ "url": "https://opencollective.com/express"
1911+ }
1912+ },
1913+ "node_modules/iconv-lite": {
1914+ "version": "0.7.3",
1915+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
1916+ "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
1917+ "license": "MIT",
1918+ "peer": true,
1919+ "dependencies": {
1920+ "safer-buffer": ">= 2.1.2 < 3.0.0"
1921+ },
1922+ "engines": {
1923+ "node": ">=0.10.0"
1924+ },
1925+ "funding": {
1926+ "type": "opencollective",
1927+ "url": "https://opencollective.com/express"
1928+ }
1929+ },
1930+ "node_modules/inherits": {
1931+ "version": "2.0.4",
1932+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
1933+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
1934+ "license": "ISC",
1935+ "peer": true
1936+ },
1937+ "node_modules/ip-address": {
1938+ "version": "10.7.2",
1939+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz",
1940+ "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==",
1941+ "license": "MIT",
1942+ "peer": true,
1943+ "engines": {
1944+ "node": ">= 12"
1945+ }
1946+ },
1947+ "node_modules/ipaddr.js": {
1948+ "version": "1.9.1",
1949+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
1950+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
1951+ "license": "MIT",
1952+ "peer": true,
1953+ "engines": {
1954+ "node": ">= 0.10"
1955+ }
1956+ },
1957+ "node_modules/is-promise": {
1958+ "version": "4.0.0",
1959+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
1960+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
1961+ "license": "MIT",
1962+ "peer": true
1963+ },
1964+ "node_modules/isexe": {
1965+ "version": "2.0.0",
1966+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
1967+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
1968+ "license": "ISC",
1969+ "peer": true
1970+ },
1971+ "node_modules/jose": {
1972+ "version": "6.2.12",
1973+ "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz",
1974+ "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==",
1975+ "license": "MIT",
1976+ "peer": true,
1977+ "funding": {
1978+ "url": "https://github.com/sponsors/panva"
1979+ }
1980+ },
11011981 "node_modules/js-yaml": {
11021982 "version": "4.3.2",
11031983 "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
@@ -1120,11 +2000,102 @@
11202000 "js-yaml": "bin/js-yaml.js"
11212001 }
11222002 },
2003+ "node_modules/json-schema-to-ts": {
2004+ "version": "3.1.1",
2005+ "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
2006+ "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
2007+ "license": "MIT",
2008+ "peer": true,
2009+ "dependencies": {
2010+ "@babel/runtime": "^7.18.3",
2011+ "ts-algebra": "^2.0.0"
2012+ },
2013+ "engines": {
2014+ "node": ">=16"
2015+ }
2016+ },
2017+ "node_modules/json-schema-traverse": {
2018+ "version": "1.0.0",
2019+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
2020+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
2021+ "license": "MIT",
2022+ "peer": true
2023+ },
2024+ "node_modules/json-schema-typed": {
2025+ "version": "8.0.2",
2026+ "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
2027+ "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
2028+ "license": "BSD-2-Clause",
2029+ "peer": true
2030+ },
2031+ "node_modules/math-intrinsics": {
2032+ "version": "1.1.0",
2033+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
2034+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
2035+ "license": "MIT",
2036+ "peer": true,
2037+ "engines": {
2038+ "node": ">= 0.4"
2039+ }
2040+ },
2041+ "node_modules/media-typer": {
2042+ "version": "1.1.1",
2043+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz",
2044+ "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==",
2045+ "license": "MIT",
2046+ "peer": true,
2047+ "engines": {
2048+ "node": ">= 0.8"
2049+ },
2050+ "funding": {
2051+ "type": "opencollective",
2052+ "url": "https://opencollective.com/express"
2053+ }
2054+ },
2055+ "node_modules/merge-descriptors": {
2056+ "version": "2.0.0",
2057+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
2058+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
2059+ "license": "MIT",
2060+ "peer": true,
2061+ "engines": {
2062+ "node": ">=18"
2063+ },
2064+ "funding": {
2065+ "url": "https://github.com/sponsors/sindresorhus"
2066+ }
2067+ },
2068+ "node_modules/mime-db": {
2069+ "version": "1.54.0",
2070+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
2071+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
2072+ "license": "MIT",
2073+ "peer": true,
2074+ "engines": {
2075+ "node": ">= 0.6"
2076+ }
2077+ },
2078+ "node_modules/mime-types": {
2079+ "version": "3.0.2",
2080+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
2081+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
2082+ "license": "MIT",
2083+ "peer": true,
2084+ "dependencies": {
2085+ "mime-db": "^1.54.0"
2086+ },
2087+ "engines": {
2088+ "node": ">=18"
2089+ },
2090+ "funding": {
2091+ "type": "opencollective",
2092+ "url": "https://opencollective.com/express"
2093+ }
2094+ },
11232095 "node_modules/ms": {
11242096 "version": "2.1.3",
11252097 "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
11262098 "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1127- "dev": true,
11282099 "license": "MIT"
11292100 },
11302101 "node_modules/nanoid": {
@@ -1146,6 +2117,114 @@
11462117 "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
11472118 }
11482119 },
2120+ "node_modules/negotiator": {
2121+ "version": "1.1.0",
2122+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz",
2123+ "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==",
2124+ "license": "MIT",
2125+ "peer": true,
2126+ "dependencies": {
2127+ "content-type": "^2.1.0"
2128+ },
2129+ "engines": {
2130+ "node": ">=18"
2131+ },
2132+ "funding": {
2133+ "type": "opencollective",
2134+ "url": "https://opencollective.com/express"
2135+ }
2136+ },
2137+ "node_modules/negotiator/node_modules/content-type": {
2138+ "version": "2.1.0",
2139+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
2140+ "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
2141+ "license": "MIT",
2142+ "peer": true,
2143+ "engines": {
2144+ "node": ">=18"
2145+ },
2146+ "funding": {
2147+ "type": "opencollective",
2148+ "url": "https://opencollective.com/express"
2149+ }
2150+ },
2151+ "node_modules/object-assign": {
2152+ "version": "4.1.1",
2153+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
2154+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
2155+ "license": "MIT",
2156+ "peer": true,
2157+ "engines": {
2158+ "node": ">=0.10.0"
2159+ }
2160+ },
2161+ "node_modules/object-inspect": {
2162+ "version": "1.13.4",
2163+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
2164+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
2165+ "license": "MIT",
2166+ "peer": true,
2167+ "engines": {
2168+ "node": ">= 0.4"
2169+ },
2170+ "funding": {
2171+ "url": "https://github.com/sponsors/ljharb"
2172+ }
2173+ },
2174+ "node_modules/on-finished": {
2175+ "version": "2.4.1",
2176+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
2177+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
2178+ "license": "MIT",
2179+ "peer": true,
2180+ "dependencies": {
2181+ "ee-first": "1.1.1"
2182+ },
2183+ "engines": {
2184+ "node": ">= 0.8"
2185+ }
2186+ },
2187+ "node_modules/once": {
2188+ "version": "1.4.0",
2189+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
2190+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
2191+ "license": "ISC",
2192+ "peer": true,
2193+ "dependencies": {
2194+ "wrappy": "1"
2195+ }
2196+ },
2197+ "node_modules/parseurl": {
2198+ "version": "1.3.3",
2199+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
2200+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
2201+ "license": "MIT",
2202+ "peer": true,
2203+ "engines": {
2204+ "node": ">= 0.8"
2205+ }
2206+ },
2207+ "node_modules/path-key": {
2208+ "version": "3.1.1",
2209+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
2210+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
2211+ "license": "MIT",
2212+ "peer": true,
2213+ "engines": {
2214+ "node": ">=8"
2215+ }
2216+ },
2217+ "node_modules/path-to-regexp": {
2218+ "version": "8.4.2",
2219+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
2220+ "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
2221+ "license": "MIT",
2222+ "peer": true,
2223+ "funding": {
2224+ "type": "opencollective",
2225+ "url": "https://opencollective.com/express"
2226+ }
2227+ },
11492228 "node_modules/picocolors": {
11502229 "version": "1.1.1",
11512230 "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -1166,6 +2245,16 @@
11662245 "url": "https://github.com/sponsors/jonschlinkert"
11672246 }
11682247 },
2248+ "node_modules/pkce-challenge": {
2249+ "version": "5.0.1",
2250+ "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
2251+ "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==",
2252+ "license": "MIT",
2253+ "peer": true,
2254+ "engines": {
2255+ "node": ">=16.20.0"
2256+ }
2257+ },
11692258 "node_modules/postcss": {
11702259 "version": "8.5.28",
11712260 "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
@@ -1195,6 +2284,81 @@
11952284 "node": "^10 || ^12 || >=14"
11962285 }
11972286 },
2287+ "node_modules/proxy-addr": {
2288+ "version": "2.0.8",
2289+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz",
2290+ "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==",
2291+ "license": "MIT",
2292+ "peer": true,
2293+ "dependencies": {
2294+ "forwarded": "0.2.0",
2295+ "ipaddr.js": "1.9.1"
2296+ },
2297+ "engines": {
2298+ "node": ">= 0.10"
2299+ },
2300+ "funding": {
2301+ "type": "opencollective",
2302+ "url": "https://opencollective.com/express"
2303+ }
2304+ },
2305+ "node_modules/qs": {
2306+ "version": "6.16.0",
2307+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",
2308+ "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==",
2309+ "license": "BSD-3-Clause",
2310+ "peer": true,
2311+ "dependencies": {
2312+ "es-define-property": "^1.0.1",
2313+ "side-channel": "^1.1.1"
2314+ },
2315+ "engines": {
2316+ "node": ">=0.6"
2317+ },
2318+ "funding": {
2319+ "url": "https://github.com/sponsors/ljharb"
2320+ }
2321+ },
2322+ "node_modules/range-parser": {
2323+ "version": "1.3.0",
2324+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz",
2325+ "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==",
2326+ "license": "MIT",
2327+ "peer": true,
2328+ "engines": {
2329+ "node": ">= 0.6"
2330+ },
2331+ "funding": {
2332+ "type": "opencollective",
2333+ "url": "https://opencollective.com/express"
2334+ }
2335+ },
2336+ "node_modules/raw-body": {
2337+ "version": "3.0.2",
2338+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
2339+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
2340+ "license": "MIT",
2341+ "peer": true,
2342+ "dependencies": {
2343+ "bytes": "~3.1.2",
2344+ "http-errors": "~2.0.1",
2345+ "iconv-lite": "~0.7.0",
2346+ "unpipe": "~1.0.0"
2347+ },
2348+ "engines": {
2349+ "node": ">= 0.10"
2350+ }
2351+ },
2352+ "node_modules/require-from-string": {
2353+ "version": "2.0.2",
2354+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
2355+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
2356+ "license": "MIT",
2357+ "peer": true,
2358+ "engines": {
2359+ "node": ">=0.10.0"
2360+ }
2361+ },
11982362 "node_modules/rollup": {
11992363 "version": "4.63.4",
12002364 "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.4.tgz",
@@ -1241,6 +2405,183 @@
12412405 "fsevents": "~2.3.2"
12422406 }
12432407 },
2408+ "node_modules/router": {
2409+ "version": "2.2.0",
2410+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
2411+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
2412+ "license": "MIT",
2413+ "peer": true,
2414+ "dependencies": {
2415+ "debug": "^4.4.0",
2416+ "depd": "^2.0.0",
2417+ "is-promise": "^4.0.0",
2418+ "parseurl": "^1.3.3",
2419+ "path-to-regexp": "^8.0.0"
2420+ },
2421+ "engines": {
2422+ "node": ">= 18"
2423+ }
2424+ },
2425+ "node_modules/safer-buffer": {
2426+ "version": "2.1.2",
2427+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
2428+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
2429+ "license": "MIT",
2430+ "peer": true
2431+ },
2432+ "node_modules/send": {
2433+ "version": "1.2.1",
2434+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
2435+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
2436+ "license": "MIT",
2437+ "peer": true,
2438+ "dependencies": {
2439+ "debug": "^4.4.3",
2440+ "encodeurl": "^2.0.0",
2441+ "escape-html": "^1.0.3",
2442+ "etag": "^1.8.1",
2443+ "fresh": "^2.0.0",
2444+ "http-errors": "^2.0.1",
2445+ "mime-types": "^3.0.2",
2446+ "ms": "^2.1.3",
2447+ "on-finished": "^2.4.1",
2448+ "range-parser": "^1.2.1",
2449+ "statuses": "^2.0.2"
2450+ },
2451+ "engines": {
2452+ "node": ">= 18"
2453+ },
2454+ "funding": {
2455+ "type": "opencollective",
2456+ "url": "https://opencollective.com/express"
2457+ }
2458+ },
2459+ "node_modules/serve-static": {
2460+ "version": "2.2.1",
2461+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
2462+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
2463+ "license": "MIT",
2464+ "peer": true,
2465+ "dependencies": {
2466+ "encodeurl": "^2.0.0",
2467+ "escape-html": "^1.0.3",
2468+ "parseurl": "^1.3.3",
2469+ "send": "^1.2.0"
2470+ },
2471+ "engines": {
2472+ "node": ">= 18"
2473+ },
2474+ "funding": {
2475+ "type": "opencollective",
2476+ "url": "https://opencollective.com/express"
2477+ }
2478+ },
2479+ "node_modules/setprototypeof": {
2480+ "version": "1.2.0",
2481+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
2482+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
2483+ "license": "ISC",
2484+ "peer": true
2485+ },
2486+ "node_modules/shebang-command": {
2487+ "version": "2.0.0",
2488+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
2489+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
2490+ "license": "MIT",
2491+ "peer": true,
2492+ "dependencies": {
2493+ "shebang-regex": "^3.0.0"
2494+ },
2495+ "engines": {
2496+ "node": ">=8"
2497+ }
2498+ },
2499+ "node_modules/shebang-regex": {
2500+ "version": "3.0.0",
2501+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
2502+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
2503+ "license": "MIT",
2504+ "peer": true,
2505+ "engines": {
2506+ "node": ">=8"
2507+ }
2508+ },
2509+ "node_modules/side-channel": {
2510+ "version": "1.1.1",
2511+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
2512+ "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
2513+ "license": "MIT",
2514+ "peer": true,
2515+ "dependencies": {
2516+ "es-errors": "^1.3.0",
2517+ "object-inspect": "^1.13.4",
2518+ "side-channel-list": "^1.0.1",
2519+ "side-channel-map": "^1.0.1",
2520+ "side-channel-weakmap": "^1.0.2"
2521+ },
2522+ "engines": {
2523+ "node": ">= 0.4"
2524+ },
2525+ "funding": {
2526+ "url": "https://github.com/sponsors/ljharb"
2527+ }
2528+ },
2529+ "node_modules/side-channel-list": {
2530+ "version": "1.0.1",
2531+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
2532+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
2533+ "license": "MIT",
2534+ "peer": true,
2535+ "dependencies": {
2536+ "es-errors": "^1.3.0",
2537+ "object-inspect": "^1.13.4"
2538+ },
2539+ "engines": {
2540+ "node": ">= 0.4"
2541+ },
2542+ "funding": {
2543+ "url": "https://github.com/sponsors/ljharb"
2544+ }
2545+ },
2546+ "node_modules/side-channel-map": {
2547+ "version": "1.0.1",
2548+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
2549+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
2550+ "license": "MIT",
2551+ "peer": true,
2552+ "dependencies": {
2553+ "call-bound": "^1.0.2",
2554+ "es-errors": "^1.3.0",
2555+ "get-intrinsic": "^1.2.5",
2556+ "object-inspect": "^1.13.3"
2557+ },
2558+ "engines": {
2559+ "node": ">= 0.4"
2560+ },
2561+ "funding": {
2562+ "url": "https://github.com/sponsors/ljharb"
2563+ }
2564+ },
2565+ "node_modules/side-channel-weakmap": {
2566+ "version": "1.0.2",
2567+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
2568+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
2569+ "license": "MIT",
2570+ "peer": true,
2571+ "dependencies": {
2572+ "call-bound": "^1.0.2",
2573+ "es-errors": "^1.3.0",
2574+ "get-intrinsic": "^1.2.5",
2575+ "object-inspect": "^1.13.3",
2576+ "side-channel-map": "^1.0.1"
2577+ },
2578+ "engines": {
2579+ "node": ">= 0.4"
2580+ },
2581+ "funding": {
2582+ "url": "https://github.com/sponsors/ljharb"
2583+ }
2584+ },
12442585 "node_modules/source-map-js": {
12452586 "version": "1.2.1",
12462587 "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -1251,6 +2592,27 @@
12512592 "node": ">=0.10.0"
12522593 }
12532594 },
2595+ "node_modules/standardwebhooks": {
2596+ "version": "1.1.1",
2597+ "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.1.1.tgz",
2598+ "integrity": "sha512-bCbX9ZEyFkWPsRz7Bl3NuQUJohmwGSev/yhr7vhaGPlc4AfIrspIRa6cPTBuI1ItmrTDJ4d/S2hCsfe4+vQGnQ==",
2599+ "license": "MIT",
2600+ "peer": true,
2601+ "dependencies": {
2602+ "@stablelib/base64": "^1.0.0",
2603+ "fast-sha256": "^1.3.0"
2604+ }
2605+ },
2606+ "node_modules/statuses": {
2607+ "version": "2.0.2",
2608+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
2609+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
2610+ "license": "MIT",
2611+ "peer": true,
2612+ "engines": {
2613+ "node": ">= 0.8"
2614+ }
2615+ },
12542616 "node_modules/style-mod": {
12552617 "version": "4.1.4",
12562618 "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.4.tgz",
@@ -1274,6 +2636,56 @@
12742636 "url": "https://github.com/sponsors/SuperchupuDev"
12752637 }
12762638 },
2639+ "node_modules/toidentifier": {
2640+ "version": "1.0.1",
2641+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
2642+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
2643+ "license": "MIT",
2644+ "peer": true,
2645+ "engines": {
2646+ "node": ">=0.6"
2647+ }
2648+ },
2649+ "node_modules/ts-algebra": {
2650+ "version": "2.0.0",
2651+ "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
2652+ "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
2653+ "license": "MIT",
2654+ "peer": true
2655+ },
2656+ "node_modules/type-is": {
2657+ "version": "2.1.0",
2658+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
2659+ "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
2660+ "license": "MIT",
2661+ "peer": true,
2662+ "dependencies": {
2663+ "content-type": "^2.0.0",
2664+ "media-typer": "^1.1.0",
2665+ "mime-types": "^3.0.0"
2666+ },
2667+ "engines": {
2668+ "node": ">= 18"
2669+ },
2670+ "funding": {
2671+ "type": "opencollective",
2672+ "url": "https://opencollective.com/express"
2673+ }
2674+ },
2675+ "node_modules/type-is/node_modules/content-type": {
2676+ "version": "2.1.0",
2677+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
2678+ "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
2679+ "license": "MIT",
2680+ "peer": true,
2681+ "engines": {
2682+ "node": ">=18"
2683+ },
2684+ "funding": {
2685+ "type": "opencollective",
2686+ "url": "https://opencollective.com/express"
2687+ }
2688+ },
12772689 "node_modules/typescript": {
12782690 "version": "5.9.3",
12792691 "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -1295,6 +2707,26 @@
12952707 "dev": true,
12962708 "license": "MIT"
12972709 },
2710+ "node_modules/unpipe": {
2711+ "version": "1.0.0",
2712+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
2713+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
2714+ "license": "MIT",
2715+ "peer": true,
2716+ "engines": {
2717+ "node": ">= 0.8"
2718+ }
2719+ },
2720+ "node_modules/vary": {
2721+ "version": "1.1.2",
2722+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
2723+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
2724+ "license": "MIT",
2725+ "peer": true,
2726+ "engines": {
2727+ "node": ">= 0.8"
2728+ }
2729+ },
12982730 "node_modules/vite": {
12992731 "version": "6.4.3",
13002732 "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
@@ -1387,6 +2819,69 @@
13872819 "@webgpu/types": "^0.1.69",
13882820 "debug": "^4.4.0"
13892821 }
2822+ },
2823+ "node_modules/which": {
2824+ "version": "2.0.2",
2825+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
2826+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
2827+ "license": "ISC",
2828+ "peer": true,
2829+ "dependencies": {
2830+ "isexe": "^2.0.0"
2831+ },
2832+ "bin": {
2833+ "node-which": "bin/node-which"
2834+ },
2835+ "engines": {
2836+ "node": ">= 8"
2837+ }
2838+ },
2839+ "node_modules/wrappy": {
2840+ "version": "1.0.2",
2841+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
2842+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
2843+ "license": "ISC",
2844+ "peer": true
2845+ },
2846+ "node_modules/ws": {
2847+ "version": "8.21.3",
2848+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
2849+ "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
2850+ "license": "MIT",
2851+ "engines": {
2852+ "node": ">=10.0.0"
2853+ },
2854+ "peerDependencies": {
2855+ "bufferutil": "^4.0.1",
2856+ "utf-8-validate": ">=5.0.2"
2857+ },
2858+ "peerDependenciesMeta": {
2859+ "bufferutil": {
2860+ "optional": true
2861+ },
2862+ "utf-8-validate": {
2863+ "optional": true
2864+ }
2865+ }
2866+ },
2867+ "node_modules/zod": {
2868+ "version": "4.6.5",
2869+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz",
2870+ "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==",
2871+ "license": "MIT",
2872+ "funding": {
2873+ "url": "https://github.com/sponsors/colinhacks"
2874+ }
2875+ },
2876+ "node_modules/zod-to-json-schema": {
2877+ "version": "3.25.2",
2878+ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz",
2879+ "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==",
2880+ "license": "ISC",
2881+ "peer": true,
2882+ "peerDependencies": {
2883+ "zod": "^3.25.28 || ^4"
2884+ }
13902885 }
13912886 }
13922887 }
package.jsonmodified+5−1View file
@@ -8,6 +8,7 @@
88 "dev": "vite",
99 "build": "tsc --noEmit && vite build",
1010 "preview": "vite preview",
11+ "bridge": "node bridge/server.mjs",
1112 "test": "npm run test:cpu && npm run test:gpu",
1213 "test:cpu": "node --test tests/*.test.ts",
1314 "test:gpu": "node tests/gpu.check.ts"
@@ -21,12 +22,15 @@
2122 "webgpu": "^0.4.0"
2223 },
2324 "dependencies": {
25+ "@anthropic-ai/claude-agent-sdk": "^0.3.280",
2426 "@codemirror/commands": "^6.11.0",
2527 "@codemirror/language": "^6.12.4",
2628 "@codemirror/legacy-modes": "^6.5.4",
2729 "@codemirror/state": "^6.7.4",
2830 "@codemirror/view": "^6.43.11",
2931 "@lezer/highlight": "^1.2.3",
30- "js-yaml": "^4.1.0"
32+ "js-yaml": "^4.1.0",
33+ "ws": "^8.21.3",
34+ "zod": "^4.6.5"
3135 }
3236 }
src/analysis/spectrogram.tsmodified+29−0View file
@@ -66,3 +66,32 @@ export class Stft {
6666 return n;
6767 }
6868 }
69+
70+export const DEFAULT_BANDS = [60, 125, 250, 500, 1000, 2000, 4000, 8000, 16000];
71+
72+export interface BandSummary { lo: number; hi: number; onsetMs: number; peakMs: number; levelDb: number }
73+
74+/** Per-octave band envelopes: onset (first time at -20 dB of the band's own peak), peak time, level vs the loudest band. */
75+export function bandSummary(st: Stft, bands = DEFAULT_BANDS): { bands: BandSummary[]; centroidHz: { ms: number; hz: number }[] } {
76+ const nCol = st.columns.length;
77+ if (!nCol) return { bands: [], centroidHz: [] };
78+ const total = new Float64Array(nCol);
79+ for (let c = 0; c < nCol; c++) for (const p of st.columns[c]) total[c] += p;
80+ const totMax = Math.max(...total) || 1;
81+ const out: BandSummary[] = [];
82+ for (let b = 0; b + 1 < bands.length; b++) {
83+ const lo = bands[b], hi = bands[b + 1];
84+ const b0 = Math.ceil(lo / st.binFrequency(1)), b1 = Math.min(st.nBins, Math.ceil(hi / st.binFrequency(1)));
85+ if (b0 >= b1) continue;
86+ const env = new Float64Array(nCol);
87+ let mx = 0, peak = 0;
88+ for (let c = 0; c < nCol; c++) { let s = 0; for (let k = b0; k < b1; k++) s += st.columns[c][k]; env[c] = s; if (s > mx) { mx = s; peak = c; } }
89+ if (mx <= 0) continue;
90+ let onset = 0;
91+ for (let c = 0; c < nCol; c++) if (env[c] >= mx * 0.01) { onset = c; break; }
92+ out.push({ lo, hi, onsetMs: 1e3 * st.columnTime(onset), peakMs: 1e3 * st.columnTime(peak), levelDb: 10 * Math.log10(mx / totMax) });
93+ }
94+ const centroidHz = [5, 10, 20, 50, 100, 200, 500, 1000, 2000].filter((ms) => ms / 1e3 < st.columnTime(nCol - 1))
95+ .map((ms) => { let c = 0; while (c < nCol - 1 && st.columnTime(c) < ms / 1e3) c++; return { ms, hz: Math.round(st.centroid[c]) }; });
96+ return { bands: out, centroidHz };
97+}
src/chat/client.tsadded+95−0View file
@@ -0,0 +1,95 @@
1+// WebSocket client of the local Claude Code bridge (bridge/server.mjs): sends the user's
2+// messages, receives streamed replies, and executes the tools Claude calls against the app.
3+
4+export interface AppApi {
5+ getState(): Promise<unknown>;
6+ setYaml(yaml: string): { issues: { path: string; message: string }[] };
7+ selectPreset(id: string): string;
8+ start(): Promise<string>;
9+ pause(): string;
10+ stop(): string;
11+ play(fromSeconds: number): string;
12+ setSignal(signal: string): string;
13+}
14+
15+export type ChatEvent =
16+ | { type: "connection"; state: "connecting" | "open" | "closed" }
17+ | { type: "hello"; model: string; cwd: string }
18+ | { type: "init"; session_id: string; model: string }
19+ | { type: "status"; state: "working" | "idle" }
20+ | { type: "block_start" }
21+ | { type: "delta"; text: string }
22+ | { type: "tool_use"; name: string; input: string }
23+ | { type: "tool_call"; name: string }
24+ | { type: "result"; ok: boolean; error?: string; cost: number; turns: number }
25+ | { type: "reset" }
26+ | { type: "error"; message: string };
27+
28+export class ChatClient {
29+ private ws: WebSocket | null = null;
30+ private wanted = false;
31+ private retry = 1000;
32+ readonly url: string; private readonly api: AppApi; private readonly onEvent: (e: ChatEvent) => void;
33+
34+ constructor(url: string, api: AppApi, onEvent: (e: ChatEvent) => void) { this.url = url; this.api = api; this.onEvent = onEvent; }
35+
36+ get connected() { return this.ws?.readyState === WebSocket.OPEN; }
37+
38+ connect() {
39+ this.wanted = true;
40+ if (this.ws && this.ws.readyState <= WebSocket.OPEN) return;
41+ this.onEvent({ type: "connection", state: "connecting" });
42+ const ws = new WebSocket(this.url);
43+ this.ws = ws;
44+ ws.onopen = () => { this.retry = 1000; this.onEvent({ type: "connection", state: "open" }); };
45+ ws.onmessage = (ev) => void this.handle(String(ev.data));
46+ ws.onclose = () => {
47+ this.onEvent({ type: "connection", state: "closed" });
48+ if (this.wanted) { setTimeout(() => this.connect(), this.retry); this.retry = Math.min(this.retry * 2, 15000); }
49+ };
50+ ws.onerror = () => { /* onclose follows */ };
51+ }
52+
53+ disconnect() { this.wanted = false; this.ws?.close(); this.ws = null; }
54+
55+ private send(obj: unknown) { if (this.connected) this.ws!.send(JSON.stringify(obj)); }
56+ sendUser(text: string) { this.send({ type: "user", text }); }
57+ interrupt() { this.send({ type: "interrupt" }); }
58+ reset() { this.send({ type: "reset" }); }
59+
60+ private async handle(raw: string) {
61+ let msg: any;
62+ try { msg = JSON.parse(raw); } catch { return; }
63+ if (msg.type === "tool_call") {
64+ this.onEvent({ type: "tool_call", name: msg.name });
65+ const { text, isError } = await this.execute(msg.name, msg.args ?? {});
66+ this.send({ type: "tool_result", id: msg.id, text, isError });
67+ return;
68+ }
69+ this.onEvent(msg as ChatEvent);
70+ }
71+
72+ /** Run a tool against the app; every result is a string for Claude. */
73+ async execute(name: string, args: Record<string, unknown>): Promise<{ text: string; isError: boolean }> {
74+ try {
75+ switch (name) {
76+ case "get_app_state": return { text: JSON.stringify(await this.api.getState()), isError: false };
77+ case "set_yaml": {
78+ const r = this.api.setYaml(String(args.yaml ?? ""));
79+ return r.issues.length
80+ ? { text: `configuration set, but it has issues:\n${r.issues.map((i) => `- ${i.path || "document"}: ${i.message}`).join("\n")}`, isError: true }
81+ : { text: "configuration set; it is valid", isError: false };
82+ }
83+ case "select_preset": return { text: this.api.selectPreset(String(args.id)), isError: false };
84+ case "start_run": return { text: await this.api.start(), isError: false };
85+ case "pause_run": return { text: this.api.pause(), isError: false };
86+ case "stop_run": return { text: this.api.stop(), isError: false };
87+ case "play_audio": return { text: this.api.play(Number(args.from_seconds ?? 0)), isError: false };
88+ case "set_signal": return { text: this.api.setSignal(String(args.signal)), isError: false };
89+ default: return { text: `unknown tool ${name}`, isError: true };
90+ }
91+ } catch (err) {
92+ return { text: `tool ${name} failed: ${(err as Error).message}`, isError: true };
93+ }
94+ }
95+}
src/main.tsmodified+78−10View file
@@ -1,4 +1,6 @@
1-import { Stft } from "./analysis/spectrogram.ts";
1+import { Stft, bandSummary } from "./analysis/spectrogram.ts";
2+import { ChatClient, type AppApi } from "./chat/client.ts";
3+import { ChatPanel } from "./ui/chatPanel.ts";
24 import { Player, encodeWav, scaleForPlayback } from "./audio/player.ts";
35 import { PRESETS } from "./config/presets.ts";
46 import type { SimulationConfig } from "./config/schema.ts";
@@ -110,6 +112,9 @@ for (const sp of document.querySelectorAll<HTMLElement>(".splitter")) {
110112 // ---------------------------------------------------------------- run state
111113 let setup: Setup | null = null;
112114 let runner: Runner | null = null;
115+let runState: "idle" | "setup" | "running" | "paused" | "done" | "stopped" = "idle";
116+let lastDiagnostics: Diagnostics | null = null;
117+let lastProgress = { done: 0, wall: 0 };
113118 let backend: SimulatorBackend | null = null;
114119 let front = new Float32Array(0), back = new Float32Array(0), frontier = 0, pushed = 0;
115120 let stft: Stft | null = null;
@@ -142,14 +147,14 @@ const progress = $<HTMLProgressElement>("progress");
142147
143148 async function start() {
144149 if (!config || issues.length) { readouts.log("fix the configuration issues first"); return; }
145- startBtn.disabled = true; presetSel.disabled = true;
150+ startBtn.disabled = true; presetSel.disabled = true; runState = "setup";
146151 readouts.clearLog();
147152 const cfg = structuredClone(config);
148153 try {
149154 readouts.log("building the set-up ...");
150155 setup = await buildSetup(cfg, async (phase) => { readouts.log(` ${phase}`); await new Promise((r) => setTimeout(r, 0)); });
151156 } catch (err) {
152- readouts.log(`set-up failed: ${(err as Error).message}`); startBtn.disabled = false; presetSel.disabled = false; return;
157+ readouts.log(`set-up failed: ${(err as Error).message}`); startBtn.disabled = false; presetSel.disabled = false; runState = "idle"; return;
153158 }
154159 $("summary").textContent = formatSummary(setup.summary);
155160 readouts.log(`set-up done in ${setup.summary.setupSeconds.toFixed(1)} s: ${setup.summary.nodes} nodes, ${setup.summary.substeps} sub-steps per sample, ${setup.summary.nSteps} steps`);
@@ -164,6 +169,7 @@ async function start() {
164169 pauseBtn.disabled = false; stopBtn.disabled = false; pauseBtn.textContent = "Pause";
165170 const kinds = setup.patches.map((p) => p.hit.exciter.kind);
166171 let lastPlate = 0;
172+ runState = "running"; lastDiagnostics = null; lastProgress = { done: 0, wall: 0 };
167173 runner = new Runner(backend, setup, {
168174 onChunk(res: ChunkResult) {
169175 const n = res.pressureFront.length;
@@ -180,25 +186,32 @@ async function start() {
180186 }
181187 },
182188 onDiagnostics(d: Diagnostics) {
183- readouts.setDiagnostics(d, kinds);
189+ lastDiagnostics = d; readouts.setDiagnostics(d, kinds);
184190 if (!Number.isFinite(d.kinetic + d.membrane + d.bending)) { readouts.log("energy is not finite: the run has blown up (lower numerics.cfl_safety)"); runner?.stop(); }
185191 },
186192 onProgress(done, wall) {
193+ lastProgress = { done, wall };
187194 if (!setup) return;
188195 progress.value = done / setup.nSamples;
189196 const audio = done / setup.fs;
190197 $("progress-text").textContent = `${audio.toFixed(2)} / ${setup.config.numerics.duration.toFixed(2)} s (${wall.toFixed(0)} s wall, ${(audio / Math.max(wall, 1e-3)).toFixed(3)}x real time)`;
191198 },
192199 onDone(stopped) {
200+ runState = stopped ? "stopped" : "done";
193201 readouts.log(stopped ? "stopped" : `done: ${frontier} samples of pressure (the last ${DECIMATION_SPAN / 2} samples of the decimation window are not produced)`);
194202 startBtn.disabled = false; presetSel.disabled = false; pauseBtn.disabled = true; stopBtn.disabled = true;
195- void backend?.diagnostics().then((d) => readouts.setDiagnostics(d, kinds));
203+ void backend?.diagnostics().then((d) => { lastDiagnostics = d; readouts.setDiagnostics(d, kinds); });
196204 },
197205 });
198206 void runner.run();
199207 }
200208 startBtn.onclick = () => void start();
201-pauseBtn.onclick = () => { if (!runner) return; if (runner.isPaused) { runner.unpause(); pauseBtn.textContent = "Pause"; } else { runner.pause(); pauseBtn.textContent = "Continue"; } };
209+function togglePause(): string {
210+ if (!runner || runState !== "running" && runState !== "paused") return "no run in progress";
211+ if (runner.isPaused) { runner.unpause(); pauseBtn.textContent = "Pause"; runState = "running"; return "continued"; }
212+ runner.pause(); pauseBtn.textContent = "Continue"; runState = "paused"; return "paused";
213+}
214+pauseBtn.onclick = () => { togglePause(); };
202215 stopBtn.onclick = () => runner?.stop();
203216
204217 // ---------------------------------------------------------------- playback of the completed portion
@@ -213,12 +226,14 @@ function tick() {
213226 requestAnimationFrame(tick);
214227 }
215228 requestAnimationFrame(tick);
216-playBtn.onclick = () => {
217- if (!setup || frontier === 0) return;
229+function playFrom(seconds: number): string {
230+ if (!setup || frontier === 0) return "no audio yet";
218231 player.setGainDb(Number(gain.value));
219- player.play(scaleForPlayback(signal(), normalize.checked), setup.fs, Number(position.value) * setup.config.numerics.duration);
232+ player.play(scaleForPlayback(signal(), normalize.checked), setup.fs, seconds);
220233 stopAudioBtn.disabled = false;
221-};
234+ return `playing ${signalSel.value} from ${seconds.toFixed(2)} s (${(frontier / setup.fs).toFixed(2)} s available)`;
235+}
236+playBtn.onclick = () => { playFrom(Number(position.value) * (setup?.config.numerics.duration ?? 0)); };
222237 player.onEnded = () => {
223238 // playback reached the end of the available signal: return the position to the start
224239 stopAudioBtn.disabled = true; position.value = "0"; timeEl.textContent = "0.000 s"; specView.cursorTime = null; specView.draw();
@@ -228,3 +243,56 @@ position.oninput = () => { if (setup) { const t = Number(position.value) * setup
228243 wavBtn.onclick = () => { if (setup) download(encodeWav(scaleForPlayback(signal(), normalize.checked), setup.fs), `${setup.config.output.prefix}_${signalSel.value}.wav`); };
229244
230245 if (params.get("autostart") === "1") void start();
246+
247+// ---------------------------------------------------------------- chat with Claude Code through the local bridge
248+const appApi: AppApi = {
249+ async getState() {
250+ const yamlIssues = parseConfigYaml(editor.text).issues;
251+ let spectrogram: unknown = null;
252+ if (stft && stft.columns.length) spectrogram = bandSummary(stft);
253+ return {
254+ yaml: editor.text, issues: yamlIssues, preset: presetSel.value,
255+ backend: device ? "webgpu" : "cpu",
256+ run: {
257+ state: runState, samplesDone: lastProgress.done, samplesTotal: setup?.nSamples ?? 0,
258+ audioSeconds: setup ? lastProgress.done / setup.fs : 0, wallSeconds: lastProgress.wall,
259+ audioAvailableSeconds: setup ? frontier / setup.fs : 0, signal: signalSel.value,
260+ },
261+ setup: setup ? formatSummary(setup.summary) : null,
262+ diagnostics: lastDiagnostics ? { time: lastDiagnostics.time, kinetic: lastDiagnostics.kinetic, membrane: lastDiagnostics.membrane, bending: lastDiagnostics.bending,
263+ exciters: lastDiagnostics.exciters, maxWoverH: lastDiagnostics.maxWoverH,
264+ hits: lastDiagnostics.hits.map((h, k) => ({ kind: setup?.patches[k].hit.exciter.kind, peakForceN: h.peakForce, contactMs: h.contactTime * 1e3, exciterVelocity: h.velocity })) } : null,
265+ spectrogram,
266+ };
267+ },
268+ setYaml(text) { editor.setText(text); fromYaml(text); return { issues }; },
269+ selectPreset(id) {
270+ const p = PRESETS.find((x) => x.id === id);
271+ if (!p) return `unknown preset ${id}; known: ${PRESETS.map((x) => x.id).join(", ")}`;
272+ presetSel.value = id; editor.setText(p.yaml); fromYaml(p.yaml);
273+ return `loaded preset ${id}`;
274+ },
275+ async start() {
276+ if (runState === "running" || runState === "paused" || runState === "setup") return `a run is already ${runState}`;
277+ if (!config || issues.length) return `cannot start: ${issues.map((i) => `${i.path}: ${i.message}`).join("; ") || "no configuration"}`;
278+ void start();
279+ return "run started; call get_app_state for progress";
280+ },
281+ pause() { return togglePause(); },
282+ stop() { if (!runner || (runState !== "running" && runState !== "paused")) return "no run in progress"; runner.stop(); return "stopping"; },
283+ play(from) { return playFrom(from); },
284+ setSignal(s) { if (s !== "unbaffled" && s !== "rayleigh") return "signal must be 'unbaffled' or 'rayleigh'"; signalSel.value = s; rebuildSpectrogram(); return `signal set to ${s}`; },
285+};
286+const bridgeUrl = params.get("bridge") ?? "ws://localhost:8765";
287+const chatEl = $("chat"), chatSplitter = $("chat-splitter"), appEl = $("app");
288+const chatPanel = new ChatPanel(chatEl, bridgeUrl);
289+const chat = new ChatClient(bridgeUrl, appApi, (e) => chatPanel.handle(e));
290+chatPanel.onSend = (t) => chat.sendUser(t); chatPanel.onInterrupt = () => chat.interrupt(); chatPanel.onReset = () => chat.reset();
291+function showChat(open: boolean) {
292+ chatEl.classList.toggle("hidden", !open); chatSplitter.classList.toggle("hidden", !open); appEl.classList.toggle("with-chat", open);
293+ if (open) { chat.connect(); chatPanel.focus(); } else chat.disconnect();
294+ redraw();
295+}
296+$("chat-toggle").onclick = () => showChat(chatEl.classList.contains("hidden"));
297+chatEl.addEventListener("chat-close", () => showChat(false));
298+if (params.get("chat") === "1") showChat(true);
src/style.cssmodified+29−2View file
@@ -1,13 +1,16 @@
1-:root { --sidebar-w: 420px; --plate-h: 40vh; --readouts-h: 190px; --bg: #14161a; --panel: #1d2026; --border: #2c3038; --text: #e6e6e6; --muted: #9aa0a8; --accent: #e0a23a; --danger: #d9534f; }
1+:root { --sidebar-w: 420px; --chat-w: 380px; --plate-h: 40vh; --readouts-h: 190px; --bg: #14161a; --panel: #1d2026; --border: #2c3038; --text: #e6e6e6; --muted: #9aa0a8; --accent: #e0a23a; --danger: #d9534f; }
22 * { box-sizing: border-box; }
33 html, body { margin: 0; height: 100%; background: var(--bg); color: var(--text); font: 13px/1.4 system-ui, sans-serif; }
44 #app { display: grid; grid-template-columns: var(--sidebar-w) 6px 1fr; height: 100vh; }
5+#app.with-chat { grid-template-columns: var(--sidebar-w) 6px 1fr 6px var(--chat-w); }
6+.splitter.hidden, #chat.hidden { display: none; }
57 .splitter { background: var(--border); flex: none; }
68 .splitter.vertical { width: 6px; cursor: col-resize; }
79 .splitter.horizontal { height: 6px; cursor: row-resize; }
810 .splitter:hover, .splitter.active { background: var(--accent); }
911 #sidebar { background: var(--panel); border-right: 1px solid var(--border); display: flex; flex-direction: column; min-height: 0; }
10-#sidebar header { padding: 10px 12px 4px; }
12+#sidebar header { padding: 10px 12px 4px; display: flex; align-items: baseline; gap: 8px; }
13+.chat-toggle { margin-left: auto; padding: 2px 10px; }
1114 #sidebar h1 { margin: 0; font-size: 18px; display: inline; }
1215 .subtitle { color: var(--muted); margin-left: 8px; }
1316 .run { padding: 6px 12px; border-bottom: 1px solid var(--border); }
@@ -61,3 +64,27 @@ canvas { flex: 1; width: 100%; height: 100%; display: block; min-height: 0; }
6164 #readouts th { color: var(--muted); font-weight: normal; }
6265 #readouts .log { font-family: ui-monospace, Menlo, Consolas, monospace; color: var(--muted); white-space: pre-wrap; margin-top: 4px; }
6366 @media (max-width: 900px) { #app { grid-template-columns: 1fr; grid-template-rows: auto auto 1fr; } #sidebar { max-height: 45vh; } .splitter.vertical { display: none; } }
67+
68+/* chat panel */
69+#chat { background: var(--panel); border-left: 1px solid var(--border); display: flex; flex-direction: column; min-height: 0; min-width: 0; }
70+.chat-head { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-bottom: 1px solid var(--border); }
71+.chat-head .spacer { flex: 1; }
72+.chat-head .dot { width: 9px; height: 9px; border-radius: 50%; background: #666; }
73+.chat-head .dot.on { background: #6fbf73; } .chat-head .dot.busy { background: var(--accent); } .chat-head .dot.off { background: #888; }
74+.chat-help { padding: 8px 10px; color: var(--muted); font-size: 12px; border-bottom: 1px solid var(--border); line-height: 1.5; }
75+.chat-help code { color: var(--text); font-size: 11px; word-break: break-all; }
76+.chat-log { flex: 1; overflow: auto; padding: 8px 10px; display: flex; flex-direction: column; gap: 8px; }
77+.msg { max-width: 100%; border-radius: 8px; padding: 6px 10px; font-size: 13px; line-height: 1.45; white-space: pre-wrap; word-break: break-word; }
78+.msg.user { background: #2c3a55; align-self: flex-end; }
79+.msg.assistant { background: #262a32; white-space: normal; }
80+.msg.assistant p { margin: 0 0 6px; } .msg.assistant p:last-child { margin: 0; }
81+.msg.assistant pre { background: #111318; padding: 6px 8px; border-radius: 4px; overflow: auto; font-size: 11px; margin: 4px 0; }
82+.msg.assistant code { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px; }
83+.msg.assistant ul, .msg.assistant ol { margin: 2px 0 6px 18px; padding: 0; }
84+.msg.assistant h4 { margin: 4px 0; }
85+.msg.tool { color: var(--muted); font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 11px; padding: 2px 6px; }
86+.msg.tool::before { content: "▸ "; }
87+.msg.system { color: var(--muted); font-size: 11px; font-style: italic; padding: 2px 6px; align-self: center; }
88+.chat-input { border-top: 1px solid var(--border); padding: 8px 10px; display: flex; flex-direction: column; gap: 6px; }
89+.chat-input textarea { width: 100%; resize: vertical; background: #111318; color: var(--text); border: 1px solid var(--border); border-radius: 4px; padding: 6px; font: inherit; }
90+.chat-buttons { display: flex; justify-content: flex-end; gap: 6px; }
src/ui/chatPanel.tsadded+99−0View file
@@ -0,0 +1,99 @@
1+// The chat panel: connection status, streamed replies, tool activity, and the input box.
2+import type { ChatEvent } from "../chat/client.ts";
3+
4+function escapeHtml(s: string) { return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); }
5+
6+/** Minimal markdown: fenced code, inline code, bold, bullet lists, paragraphs. */
7+export function renderMarkdown(src: string): string {
8+ const parts = src.split(/```/);
9+ let out = "";
10+ parts.forEach((part, i) => {
11+ if (i % 2 === 1) { const body = part.replace(/^[a-z]*\n/, ""); out += `<pre><code>${escapeHtml(body)}</code></pre>`; return; }
12+ const blocks = part.split(/\n{2,}/);
13+ for (const b of blocks) {
14+ const lines = b.split("\n").filter((l) => l.length);
15+ if (!lines.length) continue;
16+ const inline = (t: string) => escapeHtml(t).replace(/`([^`]+)`/g, "<code>$1</code>").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
17+ if (lines.every((l) => /^\s*[-*] /.test(l))) out += `<ul>${lines.map((l) => `<li>${inline(l.replace(/^\s*[-*] /, ""))}</li>`).join("")}</ul>`;
18+ else if (lines.every((l) => /^\s*\d+\. /.test(l))) out += `<ol>${lines.map((l) => `<li>${inline(l.replace(/^\s*\d+\. /, ""))}</li>`).join("")}</ol>`;
19+ else if (/^#{1,3} /.test(lines[0]) && lines.length === 1) out += `<h4>${inline(lines[0].replace(/^#+ /, ""))}</h4>`;
20+ else out += `<p>${lines.map(inline).join("<br>")}</p>`;
21+ }
22+ });
23+ return out;
24+}
25+
26+export class ChatPanel {
27+ private readonly log: HTMLElement; private readonly status: HTMLElement; private readonly input: HTMLTextAreaElement;
28+ private readonly sendBtn: HTMLButtonElement; private readonly stopBtn: HTMLButtonElement; private readonly help: HTMLElement;
29+ private current: { el: HTMLElement; text: string } | null = null;
30+ private working = false; private connected = false;
31+ onSend: ((text: string) => void) | null = null; onInterrupt: (() => void) | null = null; onReset: (() => void) | null = null;
32+
33+ constructor(root: HTMLElement, bridgeUrl: string) {
34+ root.innerHTML = `
35+ <div class="chat-head"><span class="dot"></span><span class="chat-status">not connected</span>
36+ <span class="spacer"></span><button class="chat-new" title="start a new session">New</button><button class="chat-close" title="hide the panel">×</button></div>
37+ <div class="chat-help"></div>
38+ <div class="chat-log"></div>
39+ <div class="chat-input"><textarea rows="2" placeholder="Ask Claude to set up a cymbal, run it, or explain a result"></textarea>
40+ <div class="chat-buttons"><button class="chat-stop" disabled>Stop</button><button class="chat-send primary">Send</button></div></div>`;
41+ this.log = root.querySelector(".chat-log")!; this.status = root.querySelector(".chat-status")!; this.help = root.querySelector(".chat-help")!;
42+ this.input = root.querySelector("textarea")!; this.sendBtn = root.querySelector(".chat-send")!; this.stopBtn = root.querySelector(".chat-stop")!;
43+ this.help.innerHTML = `Chat with Claude Code about this app. It needs the local bridge running on your machine:<br>
44+ <code>git clone https://vault1.magland.org/proof-of-concept/cymsimweb && cd cymsimweb && npm install && npm run bridge</code><br>
45+ It uses your Claude Code login and connects at <code>${escapeHtml(bridgeUrl)}</code> (change with <code>?bridge=ws://host:port</code>).`;
46+ this.sendBtn.onclick = () => this.submit();
47+ this.stopBtn.onclick = () => this.onInterrupt?.();
48+ root.querySelector<HTMLButtonElement>(".chat-new")!.onclick = () => { this.onReset?.(); this.log.replaceChildren(); this.current = null; this.system("new session"); };
49+ root.querySelector<HTMLButtonElement>(".chat-close")!.onclick = () => root.dispatchEvent(new CustomEvent("chat-close", { bubbles: true }));
50+ this.input.addEventListener("keydown", (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); this.submit(); } });
51+ this.updateButtons();
52+ }
53+
54+ focus() { this.input.focus(); }
55+
56+ private submit() {
57+ const text = this.input.value.trim();
58+ if (!text || !this.connected) return;
59+ this.input.value = "";
60+ this.append("user", text);
61+ this.current = null;
62+ this.onSend?.(text);
63+ }
64+
65+ private append(cls: string, text: string): HTMLElement {
66+ const el = document.createElement("div"); el.className = `msg ${cls}`;
67+ if (cls === "assistant") el.innerHTML = renderMarkdown(text); else el.textContent = text;
68+ this.log.appendChild(el); this.log.scrollTop = this.log.scrollHeight;
69+ return el;
70+ }
71+ private system(text: string) { this.append("system", text); }
72+
73+ private updateButtons() {
74+ this.sendBtn.disabled = !this.connected; this.stopBtn.disabled = !this.working;
75+ this.status.textContent = !this.connected ? "not connected" : this.working ? "Claude is working ..." : "connected";
76+ this.status.parentElement!.querySelector(".dot")!.className = `dot ${!this.connected ? "off" : this.working ? "busy" : "on"}`;
77+ this.help.style.display = this.connected ? "none" : "";
78+ }
79+
80+ handle(e: ChatEvent) {
81+ switch (e.type) {
82+ case "connection": this.connected = e.state === "open"; if (e.state === "closed") this.working = false; break;
83+ case "hello": this.system(`bridge ready (model: ${e.model})`); break;
84+ case "init": break;
85+ case "status": this.working = e.state === "working"; if (!this.working) this.current = null; break;
86+ case "block_start": this.current = null; break;
87+ case "delta":
88+ if (!this.current) this.current = { el: this.append("assistant", ""), text: "" };
89+ this.current.text += e.text; this.current.el.innerHTML = renderMarkdown(this.current.text); this.log.scrollTop = this.log.scrollHeight;
90+ break;
91+ case "tool_use": this.current = null; this.append("tool", `${e.name} ${e.input}`); break;
92+ case "tool_call": break;
93+ case "result": if (!e.ok) this.system(`ended with ${e.error}`); break;
94+ case "reset": this.system("session reset"); break;
95+ case "error": this.system(`error: ${e.message}`); break;
96+ }
97+ this.updateButtons();
98+ }
99+}
tests/analysis.test.tsmodified+11−0View file
@@ -32,3 +32,14 @@ test("WAV encoding and playback scaling", () => {
3232 const blob = encodeWav(s, 44100);
3333 assert.equal(blob.size, 44 + 8);
3434 });
35+
36+test("band summary reports staggered onsets for a chirp", async () => {
37+ const { bandSummary } = await import("../src/analysis/spectrogram.ts");
38+ const fs = 44100, st = new Stft(fs, 1024, 256), sig = new Float32Array(fs);
39+ for (let i = 0; i < fs; i++) { const t = i / fs; sig[i] = Math.sin(2 * Math.PI * (200 * t + 4000 * t * t)); }
40+ st.push(sig);
41+ const s = bandSummary(st);
42+ const b250 = s.bands.find((b) => b.lo === 250)!, b2k = s.bands.find((b) => b.lo === 2000)!;
43+ assert.ok(b250.onsetMs < b2k.onsetMs, `${b250.onsetMs} vs ${b2k.onsetMs}`);
44+ assert.ok(s.centroidHz.length > 3 && s.centroidHz[0].hz < s.centroidHz[s.centroidHz.length - 1].hz);
45+});
tests/bridge.smoke.mjsadded+29−0View file
@@ -0,0 +1,29 @@
1+// Manual smoke test of bridge/server.mjs with a scripted fake page. Starts the bridge on a spare
2+// port, connects, answers tool calls with canned state, sends one short prompt and prints the
3+// events. It runs a real Claude Code turn (uses your login/quota), so it is not part of `npm test`.
4+import { spawn } from "node:child_process";
5+const port = 8799;
6+const bridge = spawn(process.execPath, ["bridge/server.mjs", "--port", String(port)], { stdio: ["ignore", "pipe", "pipe"] });
7+bridge.stdout.on("data", (d) => process.stdout.write(`[bridge stdout] ${d}`));
8+bridge.stderr.on("data", (d) => process.stdout.write(`[bridge stderr] ${d}`));
9+await new Promise((r) => setTimeout(r, 1500));
10+const ws = new WebSocket(`ws://localhost:${port}`);
11+const fakeState = { yaml: "hits:\n- time: 0\n r: 0.137\n phi: 0.3\n velocity: 8\n exciter: {kind: mallet}\n", issues: [], preset: "crash_mallet", backend: "webgpu",
12+ run: { state: "done", samplesDone: 88200, samplesTotal: 88200, audioSeconds: 2, wallSeconds: 41, audioAvailableSeconds: 1.9999, signal: "unbaffled" },
13+ setup: "mesh: 4657 nodes ...", diagnostics: { time: 2, kinetic: 0.1, membrane: 0.02, bending: 0.05, exciters: 5.2, maxWoverH: 3.1, hits: [{ kind: "mallet", peakForceN: 1310, contactMs: 5.8, exciterVelocity: 5.9 }] },
14+ spectrogram: { bands: [{ lo: 125, hi: 250, onsetMs: 3, peakMs: 29, levelDb: -23 }, { lo: 2000, hi: 4000, onsetMs: 7.3, peakMs: 123, levelDb: -9.6 }, { lo: 8000, hi: 16000, onsetMs: 36, peakMs: 300, levelDb: -30 }], centroidHz: [{ ms: 5, hz: 612 }, { ms: 100, hz: 2393 }] } };
15+let text = "";
16+const done = new Promise((resolve) => {
17+ ws.onmessage = (ev) => {
18+ const m = JSON.parse(ev.data);
19+ if (m.type === "delta") { text += m.text; return; }
20+ if (m.type === "tool_call") { console.log(`[page] tool call ${m.name} ${JSON.stringify(m.args)}`); ws.send(JSON.stringify({ type: "tool_result", id: m.id, text: m.name === "get_app_state" ? JSON.stringify(fakeState) : "ok" })); return; }
21+ console.log(`[event] ${JSON.stringify(m).slice(0, 200)}`);
22+ if (m.type === "result") resolve(m);
23+ };
24+});
25+ws.onopen = () => ws.send(JSON.stringify({ type: "user", text: "Look at the app state and tell me in two sentences whether the last run shows an upward cascade. Do not read any files." }));
26+const result = await Promise.race([done, new Promise((r) => setTimeout(() => r({ timeout: true }), 240000))]);
27+console.log("=== assistant text ===\n" + text + "\n=== result", JSON.stringify(result));
28+ws.close(); bridge.kill();
29+process.exit(result.ok ? 0 : 1);