broadcast

This commit is contained in:
Theo Browne
2026-02-22 02:15:29 -08:00
parent dc9ac4e7c8
commit 4fe8432a16
7 changed files with 1191 additions and 1 deletions

36
broadcast.css Normal file
View File

@@ -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;
}

20
broadcast.html Normal file
View File

@@ -0,0 +1,20 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>quipslop Broadcast</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=Inter:wght@400;500;700;900&family=JetBrains+Mono:wght@400;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="./broadcast.css" />
</head>
<body>
<canvas id="broadcast-canvas" width="1920" height="1080"></canvas>
<div id="broadcast-status"></div>
<script type="module" src="./broadcast.ts"></script>
</body>
</html>

606
broadcast.ts Normal file
View File

@@ -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<string, number>;
done: boolean;
isPaused: boolean;
generation: number;
};
type ServerMessage = {
type: "state";
data: GameState;
totalRounds: number;
viewerCount: number;
};
const MODEL_COLORS: Record<string, string> = {
"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<string, HTMLImageElement> = {};
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<string, number>) {
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<string, number>) {
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();

203
bun.lock
View File

@@ -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=="],
}
}

View File

@@ -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"
}

320
scripts/stream-browser.ts Normal file
View File

@@ -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 <live|dryrun>");
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<Uint8Array>,
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<void> | 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<void>((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);
});

View File

@@ -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<WsData>({
"/": indexHtml,
"/history": historyHtml,
"/admin": adminHtml,
"/broadcast": broadcastHtml,
},
async fetch(req, server) {
const url = new URL(req.url);