diff --git a/broadcast.css b/broadcast.css
new file mode 100644
index 0000000..c06f862
--- /dev/null
+++ b/broadcast.css
@@ -0,0 +1,36 @@
+html,
+body {
+ margin: 0;
+ width: 100%;
+ height: 100%;
+ background: #0a0a0a;
+ overflow: hidden;
+}
+
+body {
+ display: grid;
+ place-items: center;
+ font-family: "Inter", sans-serif;
+ color: #ededed;
+}
+
+#broadcast-canvas {
+ width: 100vw;
+ height: 100vh;
+ object-fit: contain;
+ image-rendering: auto;
+}
+
+#broadcast-status {
+ position: fixed;
+ left: 16px;
+ bottom: 16px;
+ padding: 8px 10px;
+ border-radius: 8px;
+ background: #111;
+ border: 1px solid #1c1c1c;
+ color: #888;
+ font: 600 12px/1.2 "JetBrains Mono", monospace;
+ letter-spacing: 0.3px;
+ pointer-events: none;
+}
diff --git a/broadcast.html b/broadcast.html
new file mode 100644
index 0000000..c5d8693
--- /dev/null
+++ b/broadcast.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+ quipslop Broadcast
+
+
+
+
+
+
+
+
+
+
+
diff --git a/broadcast.ts b/broadcast.ts
new file mode 100644
index 0000000..bb88199
--- /dev/null
+++ b/broadcast.ts
@@ -0,0 +1,606 @@
+type Model = { id: string; name: string };
+type TaskInfo = {
+ model: Model;
+ startedAt: number;
+ finishedAt?: number;
+ result?: string;
+ error?: string;
+};
+type VoteInfo = {
+ voter: Model;
+ startedAt: number;
+ finishedAt?: number;
+ votedFor?: Model;
+ error?: boolean;
+};
+type RoundState = {
+ num: number;
+ phase: "prompting" | "answering" | "voting" | "done";
+ prompter: Model;
+ promptTask: TaskInfo;
+ prompt?: string;
+ contestants: [Model, Model];
+ answerTasks: [TaskInfo, TaskInfo];
+ votes: VoteInfo[];
+ scoreA?: number;
+ scoreB?: number;
+};
+type GameState = {
+ completed: RoundState[];
+ active: RoundState | null;
+ scores: Record;
+ done: boolean;
+ isPaused: boolean;
+ generation: number;
+};
+type ServerMessage = {
+ type: "state";
+ data: GameState;
+ totalRounds: number;
+ viewerCount: number;
+};
+
+const MODEL_COLORS: Record = {
+ "Gemini 3.1 Pro": "#4285F4",
+ "Kimi K2": "#00E599",
+ "DeepSeek 3.2": "#4D6BFE",
+ "GLM-5": "#1F63EC",
+ "GPT-5.2": "#10A37F",
+ "Opus 4.6": "#D97757",
+ "Sonnet 4.6": "#D97757",
+ "Grok 4.1": "#FFFFFF",
+ "MiniMax 2.5": "#FF3B30",
+};
+
+const WIDTH = 1920;
+const HEIGHT = 1080;
+
+const canvas = document.getElementById("broadcast-canvas") as HTMLCanvasElement;
+const statusEl = document.getElementById("broadcast-status") as HTMLDivElement;
+
+function get2dContext(el: HTMLCanvasElement): CanvasRenderingContext2D {
+ const context = el.getContext("2d");
+ if (!context) throw new Error("2D canvas context unavailable");
+ return context;
+}
+
+const ctx = get2dContext(canvas);
+
+let state: GameState | null = null;
+let totalRounds: number | null = null;
+let viewerCount = 0;
+let connected = false;
+let ws: WebSocket | null = null;
+let reconnectTimer: number | null = null;
+let lastMessageAt = 0;
+
+function getColor(name: string): string {
+ return MODEL_COLORS[name] ?? "#aeb6d6";
+}
+
+function getLogoUrl(name: string): string | null {
+ if (name.includes("Gemini")) return "/assets/logos/gemini.svg";
+ if (name.includes("Kimi")) return "/assets/logos/kimi.svg";
+ if (name.includes("DeepSeek")) return "/assets/logos/deepseek.svg";
+ if (name.includes("GLM")) return "/assets/logos/glm.svg";
+ if (name.includes("GPT")) return "/assets/logos/openai.svg";
+ if (name.includes("Opus") || name.includes("Sonnet")) return "/assets/logos/claude.svg";
+ if (name.includes("Grok")) return "/assets/logos/grok.svg";
+ if (name.includes("MiniMax")) return "/assets/logos/minimax.svg";
+ return null;
+}
+
+const logoCache: Record = {};
+function drawModelLogo(name: string, x: number, y: number, size: number): boolean {
+ const url = getLogoUrl(name);
+ if (!url) return false;
+ if (!logoCache[url]) {
+ const img = new Image();
+ img.src = url;
+ logoCache[url] = img;
+ }
+ const img = logoCache[url];
+ if (img.complete && img.naturalHeight !== 0) {
+ ctx.drawImage(img, x, y, size, size);
+ return true;
+ }
+ return false;
+}
+
+function setupWebSocket() {
+ const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
+ const wsUrl = `${wsProtocol}//${window.location.host}/ws`;
+ ws = new WebSocket(wsUrl);
+
+ ws.onopen = () => {
+ connected = true;
+ setStatus("WS connected");
+ };
+
+ ws.onclose = () => {
+ connected = false;
+ setStatus("WS reconnecting...");
+ if (reconnectTimer !== null) window.clearTimeout(reconnectTimer);
+ reconnectTimer = window.setTimeout(setupWebSocket, 1_000);
+ };
+
+ ws.onmessage = (e) => {
+ try {
+ const msg = JSON.parse(String(e.data)) as ServerMessage;
+ if (msg.type === "state") {
+ state = msg.data;
+ totalRounds =
+ Number.isFinite(msg.totalRounds) && msg.totalRounds >= 0
+ ? msg.totalRounds
+ : null;
+ viewerCount = msg.viewerCount;
+ lastMessageAt = Date.now();
+ }
+ } catch {
+ // Ignore malformed spectator payloads.
+ }
+ };
+}
+
+function setStatus(value: string) {
+ statusEl.textContent = value;
+}
+
+function roundRect(
+ x: number,
+ y: number,
+ w: number,
+ h: number,
+ r: number,
+ fillStyle: string,
+) {
+ const p = new Path2D();
+ p.moveTo(x + r, y);
+ p.lineTo(x + w - r, y);
+ p.quadraticCurveTo(x + w, y, x + w, y + r);
+ p.lineTo(x + w, y + h - r);
+ p.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
+ p.lineTo(x + r, y + h);
+ p.quadraticCurveTo(x, y + h, x, y + h - r);
+ p.lineTo(x, y + r);
+ p.quadraticCurveTo(x, y, x + r, y);
+ ctx.fillStyle = fillStyle;
+ ctx.fill(p);
+}
+
+function textLines(
+ text: string,
+ maxWidth: number,
+ font: string,
+ maxLines = 3,
+): string[] {
+ ctx.font = font;
+ const words = text.split(/\s+/).filter(Boolean);
+ const lines: string[] = [];
+ let current = "";
+
+ for (const word of words) {
+ const candidate = current ? `${current} ${word}` : word;
+ if (ctx.measureText(candidate).width <= maxWidth) {
+ current = candidate;
+ continue;
+ }
+ if (current) lines.push(current);
+ current = word;
+ if (lines.length >= maxLines - 1) break;
+ }
+
+ if (current) {
+ lines.push(current);
+ }
+
+ if (lines.length > maxLines) {
+ lines.length = maxLines;
+ }
+
+ if (words.length > 0 && lines.length === maxLines) {
+ const last = lines[maxLines - 1] ?? "";
+ if (ctx.measureText(last).width > maxWidth) {
+ let trimmed = last;
+ while (trimmed.length > 3 && ctx.measureText(`${trimmed}...`).width > maxWidth) {
+ trimmed = trimmed.slice(0, -1);
+ }
+ lines[maxLines - 1] = `${trimmed}...`;
+ }
+ }
+
+ return lines;
+}
+
+function drawTextBlock(
+ text: string,
+ x: number,
+ y: number,
+ maxWidth: number,
+ lineHeight: number,
+ font: string,
+ color: string,
+ maxLines: number,
+) {
+ const lines = textLines(text, maxWidth, font, maxLines);
+ ctx.font = font;
+ ctx.fillStyle = color;
+ lines.forEach((line, idx) => {
+ ctx.fillText(line, x, y + idx * lineHeight);
+ });
+}
+
+function drawHeader() {
+ ctx.fillStyle = "#0a0a0a";
+ ctx.fillRect(0, 0, WIDTH, HEIGHT);
+
+ ctx.font = '700 32px "Inter", sans-serif';
+ ctx.fillStyle = "#ededed";
+ ctx.fillText("quipslop", 48, 72);
+
+ const viewersText = `${viewerCount} viewer${viewerCount === 1 ? "" : "s"} watching`;
+ ctx.font = '600 14px "JetBrains Mono", monospace';
+ ctx.fillStyle = "#888";
+ const vWidth = ctx.measureText(viewersText).width;
+
+ const pillW = vWidth + 40;
+ const pillX = WIDTH - 380 - 48 - pillW;
+ roundRect(pillX, 44, pillW, 36, 18, "rgba(255,255,255,0.02)");
+
+ ctx.fillStyle = connected ? "#22c55e" : "#ef4444";
+ ctx.beginPath();
+ ctx.arc(pillX + 16, 62, 4, 0, Math.PI * 2);
+ ctx.fill();
+
+ ctx.fillStyle = "#888";
+ ctx.fillText(viewersText, pillX + 28, 67);
+}
+
+function drawScoreboard(scores: Record) {
+ const entries = Object.entries(scores).sort((a, b) => b[1] - a[1]);
+
+ roundRect(WIDTH - 380, 0, 380, HEIGHT, 0, "#111");
+ ctx.fillStyle = "#1c1c1c";
+ ctx.fillRect(WIDTH - 380, 0, 1, HEIGHT);
+
+ ctx.font = '700 14px "JetBrains Mono", monospace';
+ ctx.fillStyle = "#888";
+ ctx.fillText("STANDINGS", WIDTH - 340, 72);
+
+ const maxScore = entries[0]?.[1] || 1;
+
+ entries.slice(0, 10).forEach(([name, score], index) => {
+ const y = 140 + index * 60;
+ const color = getColor(name);
+ const pct = maxScore > 0 ? (score / maxScore) : 0;
+
+ ctx.font = '600 16px "JetBrains Mono", monospace';
+ ctx.fillStyle = "#888";
+ const rank = index === 0 && score > 0 ? "👑" : String(index + 1);
+ ctx.fillText(rank, WIDTH - 340, y + 20);
+
+ ctx.font = '600 16px "Inter", sans-serif';
+ ctx.fillStyle = color;
+ const nameText = name.length > 20 ? `${name.slice(0, 20)}...` : name;
+
+ const drewLogo = drawModelLogo(name, WIDTH - 300, y + 4, 20);
+ if (drewLogo) {
+ ctx.fillText(nameText, WIDTH - 300 + 28, y + 20);
+ } else {
+ ctx.fillText(nameText, WIDTH - 300, y + 20);
+ }
+
+ roundRect(WIDTH - 300, y + 36, 200, 4, 2, "#1c1c1c");
+ if (pct > 0) {
+ roundRect(WIDTH - 300, y + 36, Math.max(8, 200 * pct), 4, 2, color);
+ }
+
+ ctx.font = '700 16px "JetBrains Mono", monospace';
+ ctx.fillStyle = "#888";
+ const scoreText = String(score);
+ const scoreWidth = ctx.measureText(scoreText).width;
+ ctx.fillText(scoreText, WIDTH - 48 - scoreWidth, y + 20);
+ });
+}
+
+function drawRound(round: RoundState) {
+ const mainW = WIDTH - 380;
+
+ const phaseLabel =
+ (round.phase === "prompting"
+ ? "Writing prompt"
+ : round.phase === "answering"
+ ? "Answering"
+ : round.phase === "voting"
+ ? "Judges voting"
+ : "Complete"
+ ).toUpperCase();
+
+ ctx.font = '700 16px "JetBrains Mono", monospace';
+ ctx.fillStyle = "#ededed";
+ const totalText = totalRounds !== null ? `/${totalRounds}` : "";
+ ctx.fillText(`Round ${round.num}${totalText}`, 64, 150);
+
+ ctx.fillStyle = "#888";
+ const labelWidth = ctx.measureText(phaseLabel).width;
+ ctx.fillText(phaseLabel, mainW - 64 - labelWidth, 150);
+
+ ctx.font = '600 14px "JetBrains Mono", monospace';
+ ctx.fillStyle = "#888";
+ const promptedText = "PROMPTED BY ";
+ ctx.fillText(promptedText, 64, 210);
+
+ const pTw = ctx.measureText(promptedText).width;
+ ctx.fillStyle = getColor(round.prompter.name);
+ const drewPLogo = drawModelLogo(round.prompter.name, 64 + pTw, 210 - 12, 16);
+
+ if (drewPLogo) {
+ ctx.fillText(round.prompter.name.toUpperCase(), 64 + pTw + 20, 210);
+ } else {
+ ctx.fillText(round.prompter.name.toUpperCase(), 64 + pTw, 210);
+ }
+
+ const promptText =
+ round.prompt ??
+ (round.phase === "prompting" ? "Generating prompt..." : "Prompt unavailable");
+
+ ctx.fillStyle = "#D97757";
+ ctx.fillRect(64, 230, 4, Math.min(100, promptText.length > 100 ? 120 : 64));
+
+ drawTextBlock(
+ promptText,
+ 92,
+ 260,
+ mainW - 160,
+ 64,
+ '400 48px "DM Serif Display", serif',
+ round.prompt ? "#ededed" : "#444",
+ 2,
+ );
+
+ if (round.phase !== "prompting") {
+ const [taskA, taskB] = round.answerTasks;
+ const cardW = (mainW - 160) / 2;
+ drawContestantCard(taskA, 64, 400, cardW, 580, round);
+ drawContestantCard(taskB, 64 + cardW + 32, 400, cardW, 580, round);
+ }
+}
+
+function drawContestantCard(
+ task: TaskInfo,
+ x: number,
+ y: number,
+ w: number,
+ h: number,
+ round: RoundState,
+) {
+ const [a, b] = round.contestants;
+ let votesA = 0;
+ let votesB = 0;
+ const taskVoters: VoteInfo[] = [];
+ for (const vote of round.votes) {
+ if (vote.votedFor?.name === a.name) votesA += 1;
+ if (vote.votedFor?.name === b.name) votesB += 1;
+ if (vote.votedFor?.name === task.model.name) taskVoters.push(vote);
+ }
+ const isFirst = round.answerTasks[0].model.name === task.model.name;
+ const voteCount = isFirst ? votesA : votesB;
+ const isWinner = round.phase === "done" && voteCount > (isFirst ? votesB : votesA);
+
+ const color = getColor(task.model.name);
+
+ ctx.fillStyle = color;
+ ctx.fillRect(x, y, isWinner ? 6 : 4, h);
+
+ if (isWinner) {
+ roundRect(x, y, w, h, 0, "rgba(255,255,255,0.03)");
+ }
+
+ ctx.font = '700 24px "Inter", sans-serif';
+ ctx.fillStyle = color;
+ const drewCLogo = drawModelLogo(task.model.name, x + 24, y + 18, 24);
+ if (drewCLogo) {
+ ctx.fillText(task.model.name, x + 56, y + 40);
+ } else {
+ ctx.fillText(task.model.name, x + 24, y + 40);
+ }
+
+ if (isWinner) {
+ ctx.font = '700 12px "JetBrains Mono", monospace';
+ ctx.fillStyle = "#0a0a0a";
+ const winW = ctx.measureText("WIN").width;
+ roundRect(x + w - 24 - winW - 16, y + 20, winW + 16, 24, 4, "#ededed");
+ ctx.fillStyle = "#0a0a0a";
+ ctx.fillText("WIN", x + w - 24 - winW - 8, y + 36);
+ }
+
+ const answer =
+ !task.finishedAt && !task.result
+ ? "Writing answer..."
+ : task.error
+ ? task.error
+ : task.result ?? "No answer";
+
+ drawTextBlock(
+ task.result ? `"${answer}"` : answer,
+ x + 24,
+ y + 110,
+ w - 48,
+ 44,
+ '400 32px "DM Serif Display", serif',
+ isWinner ? "#ededed" : (!task.finishedAt && !task.result ? "#444" : "#888"),
+ 6,
+ );
+
+ const showVotes = round.phase === "voting" || round.phase === "done";
+ if (showVotes) {
+ const totalVotes = votesA + votesB;
+ const pct = totalVotes > 0 ? Math.round((voteCount / totalVotes) * 100) : 0;
+
+ roundRect(x + 24, y + h - 60, w - 48, 4, 2, "#1c1c1c");
+ if (pct > 0) {
+ roundRect(x + 24, y + h - 60, Math.max(8, ((w - 48) * pct) / 100), 4, 2, color);
+ }
+
+ ctx.font = '700 20px "JetBrains Mono", monospace';
+ ctx.fillStyle = color;
+ ctx.fillText(String(voteCount), x + 24, y + h - 24);
+
+ ctx.font = '600 14px "JetBrains Mono", monospace';
+ ctx.fillStyle = "#444";
+ const vTxt = `vote${voteCount === 1 ? "" : "s"}`;
+ const vCountW = ctx.measureText(String(voteCount)).width;
+ const vTxtW = ctx.measureText(vTxt).width;
+ ctx.fillText(vTxt, x + 24 + vCountW + 8, y + h - 25);
+
+ let avatarX = x + 24 + vCountW + 8 + vTxtW + 16;
+ const avatarY = y + h - 42;
+ const avatarSize = 24;
+
+ for (const v of taskVoters) {
+ const vColor = getColor(v.voter.name);
+ const drewLogo = drawModelLogo(v.voter.name, avatarX, avatarY, avatarSize);
+
+ if (!drewLogo) {
+ ctx.beginPath();
+ ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2);
+ ctx.fillStyle = vColor;
+ ctx.fill();
+ ctx.font = '700 12px "Inter", sans-serif';
+ ctx.fillStyle = "#0a0a0a";
+ const initial = v.voter.name[0] ?? "?";
+ const tw = ctx.measureText(initial).width;
+ ctx.fillText(initial, avatarX + avatarSize / 2 - tw / 2, avatarY + avatarSize / 2 + 4);
+ }
+
+ avatarX += avatarSize + 8;
+ }
+ }
+}
+
+function drawFooter() {
+ ctx.font = '600 12px "JetBrains Mono", monospace';
+ ctx.fillStyle = "#444";
+ const ageMs = Date.now() - lastMessageAt;
+ const freshness =
+ lastMessageAt === 0 ? "waiting for state" : `${Math.floor(ageMs / 1000)}s old`;
+ ctx.fillText(`viewers:${viewerCount} updates:${freshness}`, 24, HEIGHT - 24);
+}
+
+function drawWaiting() {
+ const mainW = WIDTH - 380;
+ ctx.font = '400 48px "DM Serif Display", serif';
+ ctx.fillStyle = "#888";
+ const text = "Waiting for game state...";
+ const tw = ctx.measureText(text).width;
+ ctx.fillText(text, (mainW - tw) / 2, HEIGHT / 2);
+}
+
+function drawDone(scores: Record) {
+ const mainW = WIDTH - 380;
+ const winner = Object.entries(scores).sort((a, b) => b[1] - a[1])[0];
+ if (!winner) return;
+ const [name, points] = winner;
+
+ ctx.font = '700 20px "JetBrains Mono", monospace';
+ ctx.fillStyle = "#444";
+ const go = "GAME OVER";
+ const gow = ctx.measureText(go).width;
+ ctx.fillText(go, (mainW - gow) / 2, HEIGHT / 2 - 100);
+
+ ctx.font = '400 80px "DM Serif Display", serif';
+ ctx.fillStyle = getColor(name);
+ const nw = ctx.measureText(name).width;
+ ctx.fillText(name, (mainW - nw) / 2, HEIGHT / 2);
+
+ ctx.font = '600 24px "Inter", sans-serif';
+ ctx.fillStyle = "#888";
+ const wins = `is the funniest AI`;
+ const ww = ctx.measureText(wins).width;
+ ctx.fillText(wins, (mainW - ww) / 2, HEIGHT / 2 + 60);
+}
+
+function draw() {
+ drawHeader();
+ if (!state) {
+ drawWaiting();
+ drawFooter();
+ return;
+ }
+
+ drawScoreboard(state.scores);
+
+ const lastCompleted = state.completed[state.completed.length - 1];
+ const isNextPrompting = state.active?.phase === "prompting" && !state.active.prompt;
+ const displayRound = isNextPrompting && lastCompleted ? lastCompleted : state.active;
+
+ if (state.done) {
+ drawDone(state.scores);
+ } else if (displayRound) {
+ drawRound(displayRound);
+ } else {
+ drawWaiting();
+ }
+ drawFooter();
+}
+
+function renderLoop() {
+ draw();
+ window.requestAnimationFrame(renderLoop);
+}
+
+function startCanvasCaptureSink() {
+ const params = new URLSearchParams(window.location.search);
+ const sink = params.get("sink");
+ if (!sink) return;
+
+ if (!("MediaRecorder" in window)) {
+ setStatus("MediaRecorder unavailable");
+ return;
+ }
+
+ const fps = Number.parseInt(params.get("captureFps") ?? "30", 10);
+ const bitRate = Number.parseInt(params.get("captureBitrate") ?? "12000000", 10);
+ const stream = canvas.captureStream(Number.isFinite(fps) && fps > 0 ? fps : 30);
+ const socket = new WebSocket(sink);
+ socket.binaryType = "arraybuffer";
+
+ let recorder: MediaRecorder | null = null;
+ const mimeCandidates = [
+ "video/webm;codecs=vp8",
+ "video/webm;codecs=vp9",
+ "video/webm",
+ ];
+ const mimeType =
+ mimeCandidates.find((value) => MediaRecorder.isTypeSupported(value)) ?? "";
+
+ socket.onopen = () => {
+ const options: MediaRecorderOptions = {
+ videoBitsPerSecond: Number.isFinite(bitRate) && bitRate > 0 ? bitRate : 12_000_000,
+ };
+ if (mimeType) options.mimeType = mimeType;
+
+ recorder = new MediaRecorder(stream, options);
+ recorder.ondataavailable = async (event) => {
+ if (event.data.size === 0) return;
+ if (socket.readyState !== WebSocket.OPEN) return;
+ if (socket.bufferedAmount > 16_000_000) return;
+ const chunk = await event.data.arrayBuffer();
+ socket.send(chunk);
+ };
+ recorder.onerror = () => {
+ setStatus("Recorder error");
+ };
+ recorder.start(250);
+ setStatus(`capture->ws ${fps}fps`);
+ };
+
+ socket.onclose = () => {
+ recorder?.stop();
+ setStatus("capture sink closed");
+ };
+}
+
+setupWebSocket();
+startCanvasCaptureSink();
+renderLoop();
diff --git a/bun.lock b/bun.lock
index 6129a3e..ae913d8 100644
--- a/bun.lock
+++ b/bun.lock
@@ -8,6 +8,7 @@
"@openrouter/ai-sdk-provider": "^2.2.3",
"ai": "^6.0.94",
"ink": "^6.8.0",
+ "puppeteer": "^24.2.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
},
@@ -30,12 +31,20 @@
"@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.2.5", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw=="],
+ "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
"@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.2.3", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-NovC+BaCfEeJwhToDrs8JeDYXXlJdEyz7lcxkjtyePSE4eoAKik872SyDK0MzXKcz8MRkv7XlNhPI6zz4TQp0g=="],
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
+ "@puppeteer/browsers": ["@puppeteer/browsers@2.13.0", "", { "dependencies": { "debug": "^4.4.3", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.7.4", "tar-fs": "^3.1.1", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA=="],
+
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
+ "@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="],
+
"@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
"@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
@@ -44,8 +53,12 @@
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
+ "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
+
"@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="],
+ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
+
"ai": ["ai@6.0.94", "", { "dependencies": { "@ai-sdk/gateway": "3.0.52", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.15", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/F9wh262HbK05b/5vILh38JvPiheonT+kBj1L97712E7VPchqmcx7aJuZN3QSk5Pj6knxUJLm2FFpYJI1pHXUA=="],
"ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="],
@@ -54,94 +67,284 @@
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
+ "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
+
+ "ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="],
+
"auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="],
+ "b4a": ["b4a@1.8.0", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg=="],
+
+ "bare-events": ["bare-events@2.8.2", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="],
+
+ "bare-fs": ["bare-fs@4.5.4", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-POK4oplfA7P7gqvetNmCs4CNtm9fNsx+IAh7jH7GgU0OJdge2rso0R20TNWVq6VoWcCvsTdlNDaleLHGaKx8CA=="],
+
+ "bare-os": ["bare-os@3.6.2", "", {}, "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A=="],
+
+ "bare-path": ["bare-path@3.0.0", "", { "dependencies": { "bare-os": "^3.0.1" } }, "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw=="],
+
+ "bare-stream": ["bare-stream@2.8.0", "", { "dependencies": { "streamx": "^2.21.0", "teex": "^1.0.1" }, "peerDependencies": { "bare-buffer": "*", "bare-events": "*" }, "optionalPeers": ["bare-buffer", "bare-events"] }, "sha512-reUN0M2sHRqCdG4lUK3Fw8w98eeUIZHL5c3H7Mbhk2yVBL+oofgaIp0ieLfD5QXwPCypBpmEEKU2WZKzbAk8GA=="],
+
+ "bare-url": ["bare-url@2.3.2", "", { "dependencies": { "bare-path": "^3.0.0" } }, "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw=="],
+
+ "basic-ftp": ["basic-ftp@5.1.0", "", {}, "sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw=="],
+
+ "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
+
"bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
+ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
+
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
+ "chromium-bidi": ["chromium-bidi@14.0.0", "", { "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw=="],
+
"cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
"cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="],
"cli-truncate": ["cli-truncate@5.1.1", "", { "dependencies": { "slice-ansi": "^7.1.0", "string-width": "^8.0.0" } }, "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A=="],
+ "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
+
"code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="],
+ "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
+
+ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
+
"convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="],
+ "cosmiconfig": ["cosmiconfig@9.0.0", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg=="],
+
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
+ "data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="],
+
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+ "degenerator": ["degenerator@5.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" } }, "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ=="],
+
+ "devtools-protocol": ["devtools-protocol@0.0.1566079", "", {}, "sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ=="],
+
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
+ "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
+
+ "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
+
"environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="],
+ "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="],
+
"es-toolkit": ["es-toolkit@1.44.0", "", {}, "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg=="],
+ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
+
"escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="],
+ "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="],
+
+ "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
+
+ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
+
+ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
+
+ "events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="],
+
"eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
+ "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="],
+
+ "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
+
+ "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
+
+ "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
+
"get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="],
+ "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
+
+ "get-uri": ["get-uri@6.0.5", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg=="],
+
+ "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
+
+ "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
+
+ "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
+
"indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="],
"ink": ["ink@6.8.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.4", "ansi-escapes": "^7.3.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.6.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^5.1.1", "code-excerpt": "^4.0.0", "es-toolkit": "^1.39.10", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.33.0", "scheduler": "^0.27.0", "signal-exit": "^3.0.7", "slice-ansi": "^8.0.0", "stack-utils": "^2.0.6", "string-width": "^8.1.1", "terminal-size": "^4.0.1", "type-fest": "^5.4.1", "widest-line": "^6.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": ">=6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-sbl1RdLOgkO9isK42WCZlJCFN9hb++sX9dsklOvfd1YQ3bQ2AiFu12Q6tFlr0HvEUvzraJntQCCpfEoUe9DSzA=="],
+ "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
+
+ "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
+
"is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
"is-in-ci": ["is-in-ci@2.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w=="],
+ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
+
+ "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
+
+ "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
+
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
+ "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
+
+ "lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
+
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
+ "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
+
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="],
+
+ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
+
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
+ "pac-proxy-agent": ["pac-proxy-agent@7.2.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA=="],
+
+ "pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="],
+
+ "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
+
+ "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
+
"patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="],
+ "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
+
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+ "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
+
+ "proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="],
+
+ "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
+
+ "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
+
+ "puppeteer": ["puppeteer@24.37.5", "", { "dependencies": { "@puppeteer/browsers": "2.13.0", "chromium-bidi": "14.0.0", "cosmiconfig": "^9.0.0", "devtools-protocol": "0.0.1566079", "puppeteer-core": "24.37.5", "typed-query-selector": "^2.12.0" }, "bin": { "puppeteer": "lib/cjs/puppeteer/node/cli.js" } }, "sha512-3PAOIQLceyEmn1Fi76GkGO2EVxztv5OtdlB1m8hMUZL3f8KDHnlvXbvCXv+Ls7KzF1R0KdKBqLuT/Hhrok12hQ=="],
+
+ "puppeteer-core": ["puppeteer-core@24.37.5", "", { "dependencies": { "@puppeteer/browsers": "2.13.0", "chromium-bidi": "14.0.0", "debug": "^4.4.3", "devtools-protocol": "0.0.1566079", "typed-query-selector": "^2.12.0", "webdriver-bidi-protocol": "0.4.1", "ws": "^8.19.0" } }, "sha512-ybL7iE78YPN4T6J+sPLO7r0lSByp/0NN6PvfBEql219cOnttoTFzCWKiBOjstXSqi/OKpwae623DWAsL7cn2MQ=="],
+
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
"react-reconciler": ["react-reconciler@0.33.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA=="],
+ "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
+
+ "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
+
"restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
+ "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
+
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="],
+ "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
+
+ "socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="],
+
+ "socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
+
+ "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
+
"stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="],
+ "streamx": ["streamx@2.23.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg=="],
+
"string-width": ["string-width@8.2.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw=="],
"strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
"tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
+ "tar-fs": ["tar-fs@3.1.1", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg=="],
+
+ "tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
+
+ "teex": ["teex@1.0.1", "", { "dependencies": { "streamx": "^2.12.5" } }, "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg=="],
+
"terminal-size": ["terminal-size@4.0.1", "", {}, "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ=="],
+ "text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="],
+
+ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
"type-fest": ["type-fest@5.4.4", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw=="],
+ "typed-query-selector": ["typed-query-selector@2.12.0", "", {}, "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg=="],
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
+ "webdriver-bidi-protocol": ["webdriver-bidi-protocol@0.4.1", "", {}, "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw=="],
+
"widest-line": ["widest-line@6.0.0", "", { "dependencies": { "string-width": "^8.1.0" } }, "sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA=="],
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
+ "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
+
"ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
+ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
+
+ "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
+
+ "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
+
+ "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
+
"yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="],
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
+ "chromium-bidi/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
+
"cli-truncate/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="],
+ "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
+ "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+ "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
+
"wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
+
+ "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
+ "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
+
+ "cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
+
+ "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
+ "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
+ "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
+
+ "yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
+
+ "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+ "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
}
}
diff --git a/package.json b/package.json
index f562721..50f949b 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,9 @@
"scripts": {
"start": "bun server.ts",
"start:cli": "bun quipslop.tsx",
- "start:web": "bun --hot server.ts"
+ "start:web": "bun --hot server.ts",
+ "start:stream": "bun ./scripts/stream-browser.ts live",
+ "start:stream:dryrun": "bun ./scripts/stream-browser.ts dryrun"
},
"devDependencies": {
"@types/bun": "latest",
@@ -20,6 +22,7 @@
"@openrouter/ai-sdk-provider": "^2.2.3",
"ai": "^6.0.94",
"ink": "^6.8.0",
+ "puppeteer": "^24.2.0",
"react": "^19.2.4",
"react-dom": "^19.2.4"
}
diff --git a/scripts/stream-browser.ts b/scripts/stream-browser.ts
new file mode 100644
index 0000000..f8d3c9e
--- /dev/null
+++ b/scripts/stream-browser.ts
@@ -0,0 +1,320 @@
+import puppeteer from "puppeteer";
+
+type Mode = "live" | "dryrun";
+
+type SinkWriter = {
+ write(chunk: Uint8Array): number;
+ end(error?: Error): number;
+};
+
+function parsePositiveInt(value: string | undefined, fallback: number): number {
+ const parsed = Number.parseInt(value ?? "", 10);
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
+}
+
+function usage(): never {
+ console.error("Usage: bun scripts/stream-browser.ts ");
+ console.error("Required for live mode: TWITCH_STREAM_KEY");
+ process.exit(1);
+}
+
+function resolveMode(value: string | undefined): Mode {
+ if (value === "live" || value === "dryrun") return value;
+ return usage();
+}
+
+const mode = resolveMode(process.argv[2]);
+
+const streamFps = parsePositiveInt(process.env.STREAM_FPS, 30);
+const captureBitrate = parsePositiveInt(process.env.STREAM_CAPTURE_BITRATE, 12_000_000);
+const targetSize = process.env.STREAM_TARGET_SIZE ?? "1920x1080";
+const targetParts = targetSize.split("x");
+const targetWidth = targetParts[0] ?? "1920";
+const targetHeight = targetParts[1] ?? "1080";
+const videoBitrate = process.env.STREAM_VIDEO_BITRATE ?? "6000k";
+const maxrate = process.env.STREAM_MAXRATE ?? "6000k";
+const bufsize = process.env.STREAM_BUFSIZE ?? "12000k";
+const gop = String(parsePositiveInt(process.env.STREAM_GOP, 60));
+const audioBitrate = process.env.STREAM_AUDIO_BITRATE ?? "160k";
+const streamKey = process.env.TWITCH_STREAM_KEY;
+const serverPort = process.env.STREAM_APP_PORT ?? "5109";
+const broadcastUrl = process.env.BROADCAST_URL ?? `http://127.0.0.1:${serverPort}/broadcast`;
+
+if (mode === "live" && !streamKey) {
+ console.error("TWITCH_STREAM_KEY is not set.");
+ process.exit(1);
+}
+
+async function assertBroadcastReachable(url: string) {
+ const timeoutMs = 5_000;
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
+ try {
+ const res = await fetch(url, { signal: controller.signal });
+ if (!res.ok) {
+ throw new Error(`HTTP ${res.status}`);
+ }
+ } catch (error) {
+ const detail = error instanceof Error ? error.message : String(error);
+ throw new Error(
+ `Cannot reach broadcast page at ${url} (${detail}). Start the app server first (bun run start or bun run start:web).`,
+ );
+ } finally {
+ clearTimeout(timeout);
+ }
+}
+
+function buildFfmpegArgs(currentMode: Mode): string[] {
+ const args = [
+ "-hide_banner",
+ "-loglevel",
+ "warning",
+ "-fflags",
+ "+genpts",
+ "-f",
+ "webm",
+ "-i",
+ "pipe:0",
+ "-f",
+ "lavfi",
+ "-i",
+ "anullsrc=channel_layout=stereo:sample_rate=44100",
+ "-map",
+ "0:v:0",
+ "-map",
+ "1:a:0",
+ "-vf",
+ `scale=${targetWidth}:${targetHeight}:force_original_aspect_ratio=decrease,pad=${targetWidth}:${targetHeight}:(ow-iw)/2:(oh-ih)/2`,
+ "-c:v",
+ "libx264",
+ "-preset",
+ "veryfast",
+ "-tune",
+ "zerolatency",
+ "-pix_fmt",
+ "yuv420p",
+ "-b:v",
+ videoBitrate,
+ "-maxrate",
+ maxrate,
+ "-bufsize",
+ bufsize,
+ "-g",
+ gop,
+ "-keyint_min",
+ gop,
+ "-sc_threshold",
+ "0",
+ "-c:a",
+ "aac",
+ "-b:a",
+ audioBitrate,
+ "-ar",
+ "44100",
+ "-ac",
+ "2",
+ ];
+
+ if (currentMode === "live") {
+ args.push("-f", "flv", `rtmp://live.twitch.tv/app/${streamKey}`);
+ return args;
+ }
+
+ args.push("-f", "mpegts", "pipe:1");
+ return args;
+}
+
+async function pipeReadableToSink(
+ readable: ReadableStream,
+ sink: SinkWriter,
+) {
+ const reader = readable.getReader();
+ try {
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ if (value) sink.write(value);
+ }
+ } finally {
+ sink.end();
+ }
+}
+
+async function main() {
+ await assertBroadcastReachable(broadcastUrl);
+
+ const ffmpegArgs = buildFfmpegArgs(mode);
+ const ffmpeg = Bun.spawn(["ffmpeg", ...ffmpegArgs], {
+ stdin: "pipe",
+ stdout: mode === "dryrun" ? "pipe" : "inherit",
+ stderr: "inherit",
+ });
+
+ let ffplay: Bun.Subprocess | null = null;
+ let ffplayPump: Promise | null = null;
+ if (mode === "dryrun") {
+ ffplay = Bun.spawn(
+ [
+ "ffplay",
+ "-hide_banner",
+ "-fflags",
+ "nobuffer",
+ "-flags",
+ "low_delay",
+ "-framedrop",
+ "-i",
+ "pipe:0",
+ ],
+ {
+ stdin: "pipe",
+ stdout: "inherit",
+ stderr: "inherit",
+ },
+ );
+ const stdout = ffmpeg.stdout;
+ if (!stdout || !ffplay.stdin) {
+ throw new Error("Failed to pipe ffmpeg output into ffplay.");
+ }
+ if (typeof ffplay.stdin === "number") {
+ throw new Error("ffplay stdin is not writable.");
+ }
+ ffplayPump = pipeReadableToSink(stdout, ffplay.stdin as SinkWriter);
+ }
+
+ let firstChunkResolve: (() => void) | null = null;
+ let firstChunkReject: ((error: Error) => void) | null = null;
+ const firstChunk = new Promise((resolve, reject) => {
+ firstChunkResolve = resolve;
+ firstChunkReject = reject;
+ });
+
+ const chunkServer = Bun.serve({
+ port: 0,
+ fetch(req, server) {
+ const url = new URL(req.url);
+ if (url.pathname === "/chunks" && server.upgrade(req)) {
+ return;
+ }
+ return new Response("Not found", { status: 404 });
+ },
+ websocket: {
+ message(_ws, message) {
+ if (!ffmpeg.stdin) return;
+ if (typeof message === "string") return;
+
+ let chunk: Uint8Array | null = null;
+ if (message instanceof ArrayBuffer) {
+ chunk = new Uint8Array(message);
+ } else if (ArrayBuffer.isView(message)) {
+ chunk = new Uint8Array(
+ message.buffer,
+ message.byteOffset,
+ message.byteLength,
+ );
+ }
+ if (!chunk) return;
+
+ try {
+ ffmpeg.stdin.write(chunk);
+ firstChunkResolve?.();
+ firstChunkResolve = null;
+ firstChunkReject = null;
+ } catch (error) {
+ const detail = error instanceof Error ? error : new Error(String(error));
+ firstChunkReject?.(detail);
+ }
+ },
+ },
+ });
+
+ const browser = await puppeteer.launch({
+ headless: true,
+ args: [
+ "--autoplay-policy=no-user-gesture-required",
+ "--disable-background-timer-throttling",
+ "--disable-renderer-backgrounding",
+ "--disable-backgrounding-occluded-windows",
+ ],
+ });
+
+ const page = await browser.newPage();
+ await page.setViewport({ width: 1920, height: 1080, deviceScaleFactor: 1 });
+ page.on("console", (msg) => {
+ if (process.env.STREAM_DEBUG === "1") {
+ console.log(`[broadcast] ${msg.type()}: ${msg.text()}`);
+ }
+ });
+
+ const captureUrl = new URL(broadcastUrl);
+ captureUrl.searchParams.set("sink", `ws://127.0.0.1:${chunkServer.port}/chunks`);
+ captureUrl.searchParams.set("captureFps", String(streamFps));
+ captureUrl.searchParams.set("captureBitrate", String(captureBitrate));
+
+ await page.goto(captureUrl.toString(), { waitUntil: "networkidle2" });
+ await page.waitForSelector("#broadcast-canvas", { timeout: 10_000 });
+
+ const firstChunkTimer = setTimeout(() => {
+ firstChunkReject?.(
+ new Error("No media chunks received from headless browser within 10s."),
+ );
+ }, 10_000);
+
+ await firstChunk.finally(() => clearTimeout(firstChunkTimer));
+ console.log(`Streaming from ${broadcastUrl} in ${mode} mode`);
+
+ let shuttingDown = false;
+ const shutdown = async () => {
+ if (shuttingDown) return;
+ shuttingDown = true;
+ try {
+ chunkServer.stop(true);
+ } catch {}
+ try {
+ await browser.close();
+ } catch {}
+ try {
+ ffmpeg.stdin?.end();
+ } catch {}
+ try {
+ ffmpeg.kill();
+ } catch {}
+ if (ffplay) {
+ try {
+ if (ffplay.stdin && typeof ffplay.stdin !== "number") {
+ ffplay.stdin.end();
+ }
+ } catch {}
+ try {
+ ffplay.kill();
+ } catch {}
+ }
+ };
+
+ process.on("SIGINT", () => {
+ void shutdown();
+ });
+ process.on("SIGTERM", () => {
+ void shutdown();
+ });
+
+ const exitCode = await ffmpeg.exited;
+ if (ffplayPump) {
+ await ffplayPump.catch(() => {
+ // Ignore downstream pipe failures on shutdown.
+ });
+ }
+ if (ffplay) {
+ await ffplay.exited;
+ }
+ await shutdown();
+
+ if (exitCode !== 0) {
+ process.exit(exitCode);
+ }
+}
+
+main().catch((error) => {
+ const detail = error instanceof Error ? error.message : String(error);
+ console.error(detail);
+ process.exit(1);
+});
diff --git a/server.ts b/server.ts
index db697a4..39e514c 100644
--- a/server.ts
+++ b/server.ts
@@ -3,6 +3,7 @@ import { timingSafeEqual } from "node:crypto";
import indexHtml from "./index.html";
import historyHtml from "./history.html";
import adminHtml from "./admin.html";
+import broadcastHtml from "./broadcast.html";
import { clearAllRounds, getRounds, getAllRounds } from "./db.ts";
import {
MODELS,
@@ -251,6 +252,7 @@ const server = Bun.serve({
"/": indexHtml,
"/history": historyHtml,
"/admin": adminHtml,
+ "/broadcast": broadcastHtml,
},
async fetch(req, server) {
const url = new URL(req.url);