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);