- commit
- 10aeee0f0655c7896b050d6f2363e6875e1baddc
- author
- Dami <dami.etoile@gmail.com>
| A | .gitignore | 10 | + |
| A | mcp-app.html | 96 | + |
| A | migrations/ | 30 | + |
| A | migrations/ | 1 | + |
| A | package-lock.json | 4110 | ++++++++++++++++++++++++++++++ |
| A | package.json | 36 | + |
| A | progress.md | 40 | + |
| A | scripts/ | 6 | + |
| A | scripts/ | 36 | + |
| A | scripts/ | 138 | + |
| A | src/ | 484 | ++++ |
| A | src/ | 118 | + |
| A | src/ | 163 | + |
| A | src/ | 241 | ++ |
| A | src/ | 483 | ++++ |
| A | src/ | 1044 | ++++++++ |
| A | test/ | 106 | + |
| A | test/ | 54 | + |
| A | tsconfig.json | 15 | + |
| A | tsconfig.view.json | 8 | + |
| A | tsconfig.worker.json | 15 | + |
| A | vite.config.ts | 16 | + |
| A | vitest.config.ts | 9 | + |
| A | wrangler.jsonc | 30 | + |
@@ -0,0 +1,96 @@
1+2+<html lang="en">3+ <head>4+ <meta charset="UTF-8" />5+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />6+ <title>MCPSweeper</title>7+ </head>8+ <body>9+ <main class="desktop">10+ <section class="window game-window" aria-label="MCPSweeper spectator window">11+ <div class="title-bar">12+ <div class="title-bar-text"><span aria-hidden="true">💣</span> MCPSweeper</div>13+ <div class="title-bar-controls">14+ <button type="button" aria-label="Minimize" disabled></button>15+ <button type="button" aria-label="Maximize" disabled></button>16+ <button type="button" aria-label="Close" disabled></button>17+ </div>18+ </div>19+ <div class="window-body">20+ <nav class="menu-bar" aria-label="Application menu">21+ <button type="button" class="menu-item"><u>G</u>ame</button>22+ <button type="button" class="menu-item"><u>H</u>elp</button>23+ </nav>24+25+ <div class="game-area">26+ <section class="game-main" aria-label="Puzzle game">27+ <section class="hud sunken-panel" aria-label="Game counters">28+ <output id="mine-counter" class="led-counter" aria-label="Unflagged mines">000</output>29+ <button id="face-btn" class="face-button" type="button" aria-label="Pause or resume playback">30+ <span id="face-icon" class="face-glyph face-playing" aria-hidden="true"></span>31+ </button>32+ <output id="batch-counter" class="led-counter" aria-label="Agent batches">000</output>33+ </section>34+35+ <section class="board-column" aria-label="Puzzle board">36+ <div class="board-frame sunken-panel">37+ <div class="board-stage">38+ <canvas id="game" width="768" height="768" aria-label="MCPSweeper field"></canvas>39+ </div>40+ </div>41+ <div class="timeline-row board-timeline">42+ <label for="replay-slider">Batch</label>43+ <input id="replay-slider" type="range" min="0" max="0" value="0" step="1" />44+ <output id="replay-position" for="replay-slider">000 / 000</output>45+ </div>46+ <div class="status-bar">47+ <p id="status-text" class="status-bar-field intent-field">Waiting for the agent…</p>48+ <p id="connection-text" class="status-bar-field connection-field">PREVIEW</p>49+ </div>50+ </section>51+ </section>52+53+ <aside class="side-panel" aria-label="Batch history pane">54+ <section class="replay-panel sunken-panel" aria-label="Game replay">55+ <div class="replay-heading">56+ <strong>Batch history</strong>57+ <output id="comparison-phase">AFTER</output>58+ </div>59+ <div class="intent-header" aria-hidden="true">60+ <span>Batch</span><span>Δ</span><span>Agent intent</span>61+ </div>62+ <ol id="intent-log" class="intent-log" aria-label="Agent intent log">63+ <li class="intent-empty">Waiting for batch history…</li>64+ </ol>65+ </section>66+67+ <input68+ id="zoom-slider"69+ class="zoom-slider"70+ type="range"71+ min="1"72+ max="4"73+ value="3"74+ step="0.25"75+ aria-label="Board zoom"76+ aria-valuetext="300%"77+ title="Board zoom: 300%"78+ />79+80+ <nav class="controls" aria-label="Playback controls">81+ <button id="pause-btn" type="button">Replay</button>82+ <button id="compare-btn" type="button">Compare: On</button>83+ <button id="speed-btn" type="button">Speed 1×</button>84+ <button id="fit-btn" type="button">Fit board</button>85+ <a id="replay-link" class="replay-link" href="/preview" target="_blank" rel="noopener" aria-disabled="true">86+ Preparing replay link…87+ </a>88+ </nav>89+ </aside>90+ </div>91+ </div>92+ </section>93+ </main>94+ <script type="module" src="/src/view.ts"></script>95+ </body>96+</html>@@ -0,0 +1,30 @@
1+CREATE TABLE IF NOT EXISTS games (2+ id TEXT PRIMARY KEY,3+ seed TEXT NOT NULL,4+ width INTEGER NOT NULL,5+ height INTEGER NOT NULL,6+ mine_count INTEGER NOT NULL,7+ mines TEXT NOT NULL,8+ revealed TEXT NOT NULL,9+ flags TEXT NOT NULL,10+ status TEXT NOT NULL CHECK (status IN ('playing', 'won', 'lost')),11+ revision INTEGER NOT NULL DEFAULT 0,12+ exploded_index INTEGER,13+ certified_no_guess INTEGER NOT NULL DEFAULT 0,14+ created_at TEXT NOT NULL,15+ updated_at TEXT NOT NULL16+);17+18+CREATE TABLE IF NOT EXISTS moves (19+ game_id TEXT NOT NULL,20+ revision INTEGER NOT NULL,21+ intent TEXT NOT NULL DEFAULT '',22+ action_json TEXT NOT NULL,23+ event_json TEXT NOT NULL,24+ created_at TEXT NOT NULL,25+ PRIMARY KEY (game_id, revision),26+ FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE27+);28+29+CREATE INDEX IF NOT EXISTS idx_games_updated_at ON games(updated_at);30+CREATE INDEX IF NOT EXISTS idx_moves_game_revision ON moves(game_id, revision);@@ -0,0 +1,4110 @@
1+{2+ "name": "mcpsweeper",3+ "version": "0.1.0",4+ "lockfileVersion": 3,5+ "requires": true,6+ "packages": {7+ "": {8+ "name": "mcpsweeper",9+ "version": "0.1.0",10+ "dependencies": {11+ "@modelcontextprotocol/ext-apps": "^1.7.5",12+ "@modelcontextprotocol/hono": "^2.0.0",13+ "@modelcontextprotocol/sdk": "^1.29.0",14+ "@modelcontextprotocol/server": "^2.0.0",15+ "98.css": "^0.1.21",16+ "hono": "^4.13.1",17+ "zod": "^4.2.0"18+ },19+ "devDependencies": {20+ "@cloudflare/vitest-pool-workers": "latest",21+ "@types/node": "latest",22+ "playwright": "^1.62.1",23+ "typescript": "5.9.3",24+ "vite": "latest",25+ "vite-plugin-singlefile": "latest",26+ "vitest": "latest",27+ "wrangler": "^4.122.0"28+ }29+ },30+ "node_modules/@cloudflare/kv-asset-handler": {31+ "version": "0.5.0",32+ "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz",33+ "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==",34+ "dev": true,35+ "license": "MIT OR Apache-2.0",36+ "engines": {37+ "node": ">=22.0.0"38+ }39+ },40+ "node_modules/@cloudflare/unenv-preset": {41+ "version": "2.16.1",42+ "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz",43+ "integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==",44+ "dev": true,45+ "license": "MIT OR Apache-2.0",46+ "peerDependencies": {47+ "unenv": "2.0.0-rc.24",48+ "workerd": ">1.20260305.0 <2.0.0-0"49+ },50+ "peerDependenciesMeta": {51+ "workerd": {52+ "optional": true53+ }54+ }55+ },56+ "node_modules/@cloudflare/vitest-pool-workers": {57+ "version": "0.21.2",58+ "resolved": "https://registry.npmjs.org/@cloudflare/vitest-pool-workers/-/vitest-pool-workers-0.21.2.tgz",59+ "integrity": "sha512-728q7/ydJQtyAsQqw/ptxpp1hYaqQbK0i6kgFHV5HwWcaVFDh0/LWgbRcJBQ3b4ABz77O3MrdHppPu/CPMqimw==",60+ "dev": true,61+ "license": "MIT",62+ "dependencies": {63+ "cjs-module-lexer": "1.2.3",64+ "esbuild": "0.28.1",65+ "miniflare": "5.20260811.0-alpha",66+ "wrangler": "4.122.0",67+ "zod": "4.4.3"68+ },69+ "peerDependencies": {70+ "@vitest/runner": "^4.1.0",71+ "@vitest/snapshot": "^4.1.0",72+ "vitest": "^4.1.0"73+ }74+ },75+ "node_modules/@cloudflare/workerd-darwin-64": {76+ "version": "1.20260811.1",77+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260811.1.tgz",78+ "integrity": "sha512-i5jqz+ywtOefr0AJbiAc8qxBLfSim/B0WJG7aW3B+pWnoVfMJdUQvi+BWcFKZJ0MoCci3KadTx6g31VfuEEqpQ==",79+ "cpu": [80+ "x64"81+ ],82+ "dev": true,83+ "license": "Apache-2.0",84+ "optional": true,85+ "os": [86+ "darwin"87+ ],88+ "engines": {89+ "node": ">=16"90+ }91+ },92+ "node_modules/@cloudflare/workerd-darwin-arm64": {93+ "version": "1.20260811.1",94+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260811.1.tgz",95+ "integrity": "sha512-NoOUM/nvaDdm2Onlnz33FikWjtatzulNtvwvy4xs0IrHaTCHwC0c8NwIt6s+AI13FkDs02/vm2I3GTPLCT9+hQ==",96+ "cpu": [97+ "arm64"98+ ],99+ "dev": true,100+ "license": "Apache-2.0",101+ "optional": true,102+ "os": [103+ "darwin"104+ ],105+ "engines": {106+ "node": ">=16"107+ }108+ },109+ "node_modules/@cloudflare/workerd-linux-64": {110+ "version": "1.20260811.1",111+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260811.1.tgz",112+ "integrity": "sha512-sdYq2jL1AD1supa3fsi5O4zTB28wSjvTHj7Migh6/ts8EROPdvrSwv+rdGHhv8HJNAz/wbIAY3wZsi1Rw4uUIg==",113+ "cpu": [114+ "x64"115+ ],116+ "dev": true,117+ "license": "Apache-2.0",118+ "optional": true,119+ "os": [120+ "linux"121+ ],122+ "engines": {123+ "node": ">=16"124+ }125+ },126+ "node_modules/@cloudflare/workerd-linux-arm64": {127+ "version": "1.20260811.1",128+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260811.1.tgz",129+ "integrity": "sha512-RIRv4shbu1kg05sD+DHTpSFCNnb5Dl2SkPDMUykqZa508tkPqe7VVw7gO0Q5msTBGyL0FfFrLuRxwwfA8u5Sow==",130+ "cpu": [131+ "arm64"132+ ],133+ "dev": true,134+ "license": "Apache-2.0",135+ "optional": true,136+ "os": [137+ "linux"138+ ],139+ "engines": {140+ "node": ">=16"141+ }142+ },143+ "node_modules/@cloudflare/workerd-windows-64": {144+ "version": "1.20260811.1",145+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260811.1.tgz",146+ "integrity": "sha512-g6VquwjASlYAibcNW/0E6Zszht4qLkmnXOGwIjjRHl2A0Qz48kVeMcGvyH6eA0G9U3OzZojjYFpP+YeyQmmdjw==",147+ "cpu": [148+ "x64"149+ ],150+ "dev": true,151+ "license": "Apache-2.0",152+ "optional": true,153+ "os": [154+ "win32"155+ ],156+ "engines": {157+ "node": ">=16"158+ }159+ },160+ "node_modules/@cspotcode/source-map-support": {161+ "version": "0.8.1",162+ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",163+ "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",164+ "dev": true,165+ "license": "MIT",166+ "dependencies": {167+ "@jridgewell/trace-mapping": "0.3.9"168+ },169+ "engines": {170+ "node": ">=12"171+ }172+ },173+ "node_modules/@emnapi/runtime": {174+ "version": "1.11.3",175+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",176+ "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",177+ "dev": true,178+ "license": "MIT",179+ "optional": true,180+ "dependencies": {181+ "tslib": "^2.4.0"182+ }183+ },184+ "node_modules/@esbuild/aix-ppc64": {185+ "version": "0.28.1",186+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",187+ "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",188+ "cpu": [189+ "ppc64"190+ ],191+ "dev": true,192+ "license": "MIT",193+ "optional": true,194+ "os": [195+ "aix"196+ ],197+ "engines": {198+ "node": ">=18"199+ }200+ },201+ "node_modules/@esbuild/android-arm": {202+ "version": "0.28.1",203+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",204+ "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",205+ "cpu": [206+ "arm"207+ ],208+ "dev": true,209+ "license": "MIT",210+ "optional": true,211+ "os": [212+ "android"213+ ],214+ "engines": {215+ "node": ">=18"216+ }217+ },218+ "node_modules/@esbuild/android-arm64": {219+ "version": "0.28.1",220+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",221+ "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",222+ "cpu": [223+ "arm64"224+ ],225+ "dev": true,226+ "license": "MIT",227+ "optional": true,228+ "os": [229+ "android"230+ ],231+ "engines": {232+ "node": ">=18"233+ }234+ },235+ "node_modules/@esbuild/android-x64": {236+ "version": "0.28.1",237+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",238+ "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",239+ "cpu": [240+ "x64"241+ ],242+ "dev": true,243+ "license": "MIT",244+ "optional": true,245+ "os": [246+ "android"247+ ],248+ "engines": {249+ "node": ">=18"250+ }251+ },252+ "node_modules/@esbuild/darwin-arm64": {253+ "version": "0.28.1",254+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",255+ "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",256+ "cpu": [257+ "arm64"258+ ],259+ "dev": true,260+ "license": "MIT",261+ "optional": true,262+ "os": [263+ "darwin"264+ ],265+ "engines": {266+ "node": ">=18"267+ }268+ },269+ "node_modules/@esbuild/darwin-x64": {270+ "version": "0.28.1",271+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",272+ "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",273+ "cpu": [274+ "x64"275+ ],276+ "dev": true,277+ "license": "MIT",278+ "optional": true,279+ "os": [280+ "darwin"281+ ],282+ "engines": {283+ "node": ">=18"284+ }285+ },286+ "node_modules/@esbuild/freebsd-arm64": {287+ "version": "0.28.1",288+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",289+ "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",290+ "cpu": [291+ "arm64"292+ ],293+ "dev": true,294+ "license": "MIT",295+ "optional": true,296+ "os": [297+ "freebsd"298+ ],299+ "engines": {300+ "node": ">=18"301+ }302+ },303+ "node_modules/@esbuild/freebsd-x64": {304+ "version": "0.28.1",305+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",306+ "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",307+ "cpu": [308+ "x64"309+ ],310+ "dev": true,311+ "license": "MIT",312+ "optional": true,313+ "os": [314+ "freebsd"315+ ],316+ "engines": {317+ "node": ">=18"318+ }319+ },320+ "node_modules/@esbuild/linux-arm": {321+ "version": "0.28.1",322+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",323+ "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",324+ "cpu": [325+ "arm"326+ ],327+ "dev": true,328+ "license": "MIT",329+ "optional": true,330+ "os": [331+ "linux"332+ ],333+ "engines": {334+ "node": ">=18"335+ }336+ },337+ "node_modules/@esbuild/linux-arm64": {338+ "version": "0.28.1",339+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",340+ "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",341+ "cpu": [342+ "arm64"343+ ],344+ "dev": true,345+ "license": "MIT",346+ "optional": true,347+ "os": [348+ "linux"349+ ],350+ "engines": {351+ "node": ">=18"352+ }353+ },354+ "node_modules/@esbuild/linux-ia32": {355+ "version": "0.28.1",356+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",357+ "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",358+ "cpu": [359+ "ia32"360+ ],361+ "dev": true,362+ "license": "MIT",363+ "optional": true,364+ "os": [365+ "linux"366+ ],367+ "engines": {368+ "node": ">=18"369+ }370+ },371+ "node_modules/@esbuild/linux-loong64": {372+ "version": "0.28.1",373+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",374+ "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",375+ "cpu": [376+ "loong64"377+ ],378+ "dev": true,379+ "license": "MIT",380+ "optional": true,381+ "os": [382+ "linux"383+ ],384+ "engines": {385+ "node": ">=18"386+ }387+ },388+ "node_modules/@esbuild/linux-mips64el": {389+ "version": "0.28.1",390+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",391+ "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",392+ "cpu": [393+ "mips64el"394+ ],395+ "dev": true,396+ "license": "MIT",397+ "optional": true,398+ "os": [399+ "linux"400+ ],401+ "engines": {402+ "node": ">=18"403+ }404+ },405+ "node_modules/@esbuild/linux-ppc64": {406+ "version": "0.28.1",407+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",408+ "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",409+ "cpu": [410+ "ppc64"411+ ],412+ "dev": true,413+ "license": "MIT",414+ "optional": true,415+ "os": [416+ "linux"417+ ],418+ "engines": {419+ "node": ">=18"420+ }421+ },422+ "node_modules/@esbuild/linux-riscv64": {423+ "version": "0.28.1",424+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",425+ "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",426+ "cpu": [427+ "riscv64"428+ ],429+ "dev": true,430+ "license": "MIT",431+ "optional": true,432+ "os": [433+ "linux"434+ ],435+ "engines": {436+ "node": ">=18"437+ }438+ },439+ "node_modules/@esbuild/linux-s390x": {440+ "version": "0.28.1",441+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",442+ "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",443+ "cpu": [444+ "s390x"445+ ],446+ "dev": true,447+ "license": "MIT",448+ "optional": true,449+ "os": [450+ "linux"451+ ],452+ "engines": {453+ "node": ">=18"454+ }455+ },456+ "node_modules/@esbuild/linux-x64": {457+ "version": "0.28.1",458+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",459+ "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",460+ "cpu": [461+ "x64"462+ ],463+ "dev": true,464+ "license": "MIT",465+ "optional": true,466+ "os": [467+ "linux"468+ ],469+ "engines": {470+ "node": ">=18"471+ }472+ },473+ "node_modules/@esbuild/netbsd-arm64": {474+ "version": "0.28.1",475+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",476+ "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",477+ "cpu": [478+ "arm64"479+ ],480+ "dev": true,481+ "license": "MIT",482+ "optional": true,483+ "os": [484+ "netbsd"485+ ],486+ "engines": {487+ "node": ">=18"488+ }489+ },490+ "node_modules/@esbuild/netbsd-x64": {491+ "version": "0.28.1",492+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",493+ "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",494+ "cpu": [495+ "x64"496+ ],497+ "dev": true,498+ "license": "MIT",499+ "optional": true,500+ "os": [501+ "netbsd"502+ ],503+ "engines": {504+ "node": ">=18"505+ }506+ },507+ "node_modules/@esbuild/openbsd-arm64": {508+ "version": "0.28.1",509+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",510+ "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",511+ "cpu": [512+ "arm64"513+ ],514+ "dev": true,515+ "license": "MIT",516+ "optional": true,517+ "os": [518+ "openbsd"519+ ],520+ "engines": {521+ "node": ">=18"522+ }523+ },524+ "node_modules/@esbuild/openbsd-x64": {525+ "version": "0.28.1",526+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",527+ "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",528+ "cpu": [529+ "x64"530+ ],531+ "dev": true,532+ "license": "MIT",533+ "optional": true,534+ "os": [535+ "openbsd"536+ ],537+ "engines": {538+ "node": ">=18"539+ }540+ },541+ "node_modules/@esbuild/openharmony-arm64": {542+ "version": "0.28.1",543+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",544+ "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",545+ "cpu": [546+ "arm64"547+ ],548+ "dev": true,549+ "license": "MIT",550+ "optional": true,551+ "os": [552+ "openharmony"553+ ],554+ "engines": {555+ "node": ">=18"556+ }557+ },558+ "node_modules/@esbuild/sunos-x64": {559+ "version": "0.28.1",560+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",561+ "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",562+ "cpu": [563+ "x64"564+ ],565+ "dev": true,566+ "license": "MIT",567+ "optional": true,568+ "os": [569+ "sunos"570+ ],571+ "engines": {572+ "node": ">=18"573+ }574+ },575+ "node_modules/@esbuild/win32-arm64": {576+ "version": "0.28.1",577+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",578+ "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",579+ "cpu": [580+ "arm64"581+ ],582+ "dev": true,583+ "license": "MIT",584+ "optional": true,585+ "os": [586+ "win32"587+ ],588+ "engines": {589+ "node": ">=18"590+ }591+ },592+ "node_modules/@esbuild/win32-ia32": {593+ "version": "0.28.1",594+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",595+ "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",596+ "cpu": [597+ "ia32"598+ ],599+ "dev": true,600+ "license": "MIT",601+ "optional": true,602+ "os": [603+ "win32"604+ ],605+ "engines": {606+ "node": ">=18"607+ }608+ },609+ "node_modules/@esbuild/win32-x64": {610+ "version": "0.28.1",611+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",612+ "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",613+ "cpu": [614+ "x64"615+ ],616+ "dev": true,617+ "license": "MIT",618+ "optional": true,619+ "os": [620+ "win32"621+ ],622+ "engines": {623+ "node": ">=18"624+ }625+ },626+ "node_modules/@hono/node-server": {627+ "version": "2.1.0",628+ "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.0.tgz",629+ "integrity": "sha512-XovyyCCnBzW+zKu+z/zq8hwNs4KOR5rEMAOxo2f40Q5xoOI37IMm6MIg2COOUtUApo0i6850MTBKH2u4QLGIqg==",630+ "license": "MIT",631+ "engines": {632+ "node": ">=20"633+ },634+ "peerDependencies": {635+ "hono": "^4"636+ }637+ },638+ "node_modules/@img/colour": {639+ "version": "1.1.0",640+ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",641+ "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",642+ "dev": true,643+ "license": "MIT",644+ "engines": {645+ "node": ">=18"646+ }647+ },648+ "node_modules/@img/sharp-darwin-arm64": {649+ "version": "0.35.2",650+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.2.tgz",651+ "integrity": "sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==",652+ "cpu": [653+ "arm64"654+ ],655+ "dev": true,656+ "license": "Apache-2.0",657+ "optional": true,658+ "os": [659+ "darwin"660+ ],661+ "engines": {662+ "node": ">=20.9.0"663+ },664+ "funding": {665+ "url": "https://opencollective.com/libvips"666+ },667+ "optionalDependencies": {668+ "@img/sharp-libvips-darwin-arm64": "1.3.1"669+ }670+ },671+ "node_modules/@img/sharp-darwin-x64": {672+ "version": "0.35.2",673+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.2.tgz",674+ "integrity": "sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==",675+ "cpu": [676+ "x64"677+ ],678+ "dev": true,679+ "license": "Apache-2.0",680+ "optional": true,681+ "os": [682+ "darwin"683+ ],684+ "engines": {685+ "node": ">=20.9.0"686+ },687+ "funding": {688+ "url": "https://opencollective.com/libvips"689+ },690+ "optionalDependencies": {691+ "@img/sharp-libvips-darwin-x64": "1.3.1"692+ }693+ },694+ "node_modules/@img/sharp-freebsd-wasm32": {695+ "version": "0.35.2",696+ "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.2.tgz",697+ "integrity": "sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==",698+ "dev": true,699+ "license": "Apache-2.0",700+ "optional": true,701+ "os": [702+ "freebsd"703+ ],704+ "dependencies": {705+ "@img/sharp-wasm32": "0.35.2"706+ },707+ "engines": {708+ "node": ">=20.9.0"709+ },710+ "funding": {711+ "url": "https://opencollective.com/libvips"712+ }713+ },714+ "node_modules/@img/sharp-libvips-darwin-arm64": {715+ "version": "1.3.1",716+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.1.tgz",717+ "integrity": "sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==",718+ "cpu": [719+ "arm64"720+ ],721+ "dev": true,722+ "license": "LGPL-3.0-or-later",723+ "optional": true,724+ "os": [725+ "darwin"726+ ],727+ "funding": {728+ "url": "https://opencollective.com/libvips"729+ }730+ },731+ "node_modules/@img/sharp-libvips-darwin-x64": {732+ "version": "1.3.1",733+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.1.tgz",734+ "integrity": "sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==",735+ "cpu": [736+ "x64"737+ ],738+ "dev": true,739+ "license": "LGPL-3.0-or-later",740+ "optional": true,741+ "os": [742+ "darwin"743+ ],744+ "funding": {745+ "url": "https://opencollective.com/libvips"746+ }747+ },748+ "node_modules/@img/sharp-libvips-linux-arm": {749+ "version": "1.3.1",750+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.1.tgz",751+ "integrity": "sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==",752+ "cpu": [753+ "arm"754+ ],755+ "dev": true,756+ "license": "LGPL-3.0-or-later",757+ "optional": true,758+ "os": [759+ "linux"760+ ],761+ "funding": {762+ "url": "https://opencollective.com/libvips"763+ }764+ },765+ "node_modules/@img/sharp-libvips-linux-arm64": {766+ "version": "1.3.1",767+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.1.tgz",768+ "integrity": "sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==",769+ "cpu": [770+ "arm64"771+ ],772+ "dev": true,773+ "license": "LGPL-3.0-or-later",774+ "optional": true,775+ "os": [776+ "linux"777+ ],778+ "funding": {779+ "url": "https://opencollective.com/libvips"780+ }781+ },782+ "node_modules/@img/sharp-libvips-linux-ppc64": {783+ "version": "1.3.1",784+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.1.tgz",785+ "integrity": "sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==",786+ "cpu": [787+ "ppc64"788+ ],789+ "dev": true,790+ "license": "LGPL-3.0-or-later",791+ "optional": true,792+ "os": [793+ "linux"794+ ],795+ "funding": {796+ "url": "https://opencollective.com/libvips"797+ }798+ },799+ "node_modules/@img/sharp-libvips-linux-riscv64": {800+ "version": "1.3.1",801+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.1.tgz",802+ "integrity": "sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==",803+ "cpu": [804+ "riscv64"805+ ],806+ "dev": true,807+ "license": "LGPL-3.0-or-later",808+ "optional": true,809+ "os": [810+ "linux"811+ ],812+ "funding": {813+ "url": "https://opencollective.com/libvips"814+ }815+ },816+ "node_modules/@img/sharp-libvips-linux-s390x": {817+ "version": "1.3.1",818+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.1.tgz",819+ "integrity": "sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==",820+ "cpu": [821+ "s390x"822+ ],823+ "dev": true,824+ "license": "LGPL-3.0-or-later",825+ "optional": true,826+ "os": [827+ "linux"828+ ],829+ "funding": {830+ "url": "https://opencollective.com/libvips"831+ }832+ },833+ "node_modules/@img/sharp-libvips-linux-x64": {834+ "version": "1.3.1",835+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.1.tgz",836+ "integrity": "sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==",837+ "cpu": [838+ "x64"839+ ],840+ "dev": true,841+ "license": "LGPL-3.0-or-later",842+ "optional": true,843+ "os": [844+ "linux"845+ ],846+ "funding": {847+ "url": "https://opencollective.com/libvips"848+ }849+ },850+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {851+ "version": "1.3.1",852+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.1.tgz",853+ "integrity": "sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==",854+ "cpu": [855+ "arm64"856+ ],857+ "dev": true,858+ "license": "LGPL-3.0-or-later",859+ "optional": true,860+ "os": [861+ "linux"862+ ],863+ "funding": {864+ "url": "https://opencollective.com/libvips"865+ }866+ },867+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {868+ "version": "1.3.1",869+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.1.tgz",870+ "integrity": "sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==",871+ "cpu": [872+ "x64"873+ ],874+ "dev": true,875+ "license": "LGPL-3.0-or-later",876+ "optional": true,877+ "os": [878+ "linux"879+ ],880+ "funding": {881+ "url": "https://opencollective.com/libvips"882+ }883+ },884+ "node_modules/@img/sharp-linux-arm": {885+ "version": "0.35.2",886+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.2.tgz",887+ "integrity": "sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==",888+ "cpu": [889+ "arm"890+ ],891+ "dev": true,892+ "license": "Apache-2.0",893+ "optional": true,894+ "os": [895+ "linux"896+ ],897+ "engines": {898+ "node": ">=20.9.0"899+ },900+ "funding": {901+ "url": "https://opencollective.com/libvips"902+ },903+ "optionalDependencies": {904+ "@img/sharp-libvips-linux-arm": "1.3.1"905+ }906+ },907+ "node_modules/@img/sharp-linux-arm64": {908+ "version": "0.35.2",909+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.2.tgz",910+ "integrity": "sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==",911+ "cpu": [912+ "arm64"913+ ],914+ "dev": true,915+ "license": "Apache-2.0",916+ "optional": true,917+ "os": [918+ "linux"919+ ],920+ "engines": {921+ "node": ">=20.9.0"922+ },923+ "funding": {924+ "url": "https://opencollective.com/libvips"925+ },926+ "optionalDependencies": {927+ "@img/sharp-libvips-linux-arm64": "1.3.1"928+ }929+ },930+ "node_modules/@img/sharp-linux-ppc64": {931+ "version": "0.35.2",932+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.2.tgz",933+ "integrity": "sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==",934+ "cpu": [935+ "ppc64"936+ ],937+ "dev": true,938+ "license": "Apache-2.0",939+ "optional": true,940+ "os": [941+ "linux"942+ ],943+ "engines": {944+ "node": ">=20.9.0"945+ },946+ "funding": {947+ "url": "https://opencollective.com/libvips"948+ },949+ "optionalDependencies": {950+ "@img/sharp-libvips-linux-ppc64": "1.3.1"951+ }952+ },953+ "node_modules/@img/sharp-linux-riscv64": {954+ "version": "0.35.2",955+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.2.tgz",956+ "integrity": "sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==",957+ "cpu": [958+ "riscv64"959+ ],960+ "dev": true,961+ "license": "Apache-2.0",962+ "optional": true,963+ "os": [964+ "linux"965+ ],966+ "engines": {967+ "node": ">=20.9.0"968+ },969+ "funding": {970+ "url": "https://opencollective.com/libvips"971+ },972+ "optionalDependencies": {973+ "@img/sharp-libvips-linux-riscv64": "1.3.1"974+ }975+ },976+ "node_modules/@img/sharp-linux-s390x": {977+ "version": "0.35.2",978+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.2.tgz",979+ "integrity": "sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==",980+ "cpu": [981+ "s390x"982+ ],983+ "dev": true,984+ "license": "Apache-2.0",985+ "optional": true,986+ "os": [987+ "linux"988+ ],989+ "engines": {990+ "node": ">=20.9.0"991+ },992+ "funding": {993+ "url": "https://opencollective.com/libvips"994+ },995+ "optionalDependencies": {996+ "@img/sharp-libvips-linux-s390x": "1.3.1"997+ }998+ },999+ "node_modules/@img/sharp-linux-x64": {1000+ "version": "0.35.2",1001+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.2.tgz",1002+ "integrity": "sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==",1003+ "cpu": [1004+ "x64"1005+ ],1006+ "dev": true,1007+ "license": "Apache-2.0",1008+ "optional": true,1009+ "os": [1010+ "linux"1011+ ],1012+ "engines": {1013+ "node": ">=20.9.0"1014+ },1015+ "funding": {1016+ "url": "https://opencollective.com/libvips"1017+ },1018+ "optionalDependencies": {1019+ "@img/sharp-libvips-linux-x64": "1.3.1"1020+ }1021+ },1022+ "node_modules/@img/sharp-linuxmusl-arm64": {1023+ "version": "0.35.2",1024+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.2.tgz",1025+ "integrity": "sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==",1026+ "cpu": [1027+ "arm64"1028+ ],1029+ "dev": true,1030+ "license": "Apache-2.0",1031+ "optional": true,1032+ "os": [1033+ "linux"1034+ ],1035+ "engines": {1036+ "node": ">=20.9.0"1037+ },1038+ "funding": {1039+ "url": "https://opencollective.com/libvips"1040+ },1041+ "optionalDependencies": {1042+ "@img/sharp-libvips-linuxmusl-arm64": "1.3.1"1043+ }1044+ },1045+ "node_modules/@img/sharp-linuxmusl-x64": {1046+ "version": "0.35.2",1047+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.2.tgz",1048+ "integrity": "sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==",1049+ "cpu": [1050+ "x64"1051+ ],1052+ "dev": true,1053+ "license": "Apache-2.0",1054+ "optional": true,1055+ "os": [1056+ "linux"1057+ ],1058+ "engines": {1059+ "node": ">=20.9.0"1060+ },1061+ "funding": {1062+ "url": "https://opencollective.com/libvips"1063+ },1064+ "optionalDependencies": {1065+ "@img/sharp-libvips-linuxmusl-x64": "1.3.1"1066+ }1067+ },1068+ "node_modules/@img/sharp-wasm32": {1069+ "version": "0.35.2",1070+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.2.tgz",1071+ "integrity": "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==",1072+ "dev": true,1073+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",1074+ "optional": true,1075+ "dependencies": {1076+ "@emnapi/runtime": "^1.11.1"1077+ },1078+ "engines": {1079+ "node": ">=20.9.0"1080+ },1081+ "funding": {1082+ "url": "https://opencollective.com/libvips"1083+ }1084+ },1085+ "node_modules/@img/sharp-webcontainers-wasm32": {1086+ "version": "0.35.2",1087+ "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.2.tgz",1088+ "integrity": "sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==",1089+ "cpu": [1090+ "wasm32"1091+ ],1092+ "dev": true,1093+ "license": "Apache-2.0",1094+ "optional": true,1095+ "dependencies": {1096+ "@img/sharp-wasm32": "0.35.2"1097+ },1098+ "engines": {1099+ "node": ">=20.9.0"1100+ },1101+ "funding": {1102+ "url": "https://opencollective.com/libvips"1103+ }1104+ },1105+ "node_modules/@img/sharp-win32-arm64": {1106+ "version": "0.35.2",1107+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.2.tgz",1108+ "integrity": "sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==",1109+ "cpu": [1110+ "arm64"1111+ ],1112+ "dev": true,1113+ "license": "Apache-2.0 AND LGPL-3.0-or-later",1114+ "optional": true,1115+ "os": [1116+ "win32"1117+ ],1118+ "engines": {1119+ "node": ">=20.9.0"1120+ },1121+ "funding": {1122+ "url": "https://opencollective.com/libvips"1123+ }1124+ },1125+ "node_modules/@img/sharp-win32-ia32": {1126+ "version": "0.35.2",1127+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.2.tgz",1128+ "integrity": "sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==",1129+ "cpu": [1130+ "ia32"1131+ ],1132+ "dev": true,1133+ "license": "Apache-2.0 AND LGPL-3.0-or-later",1134+ "optional": true,1135+ "os": [1136+ "win32"1137+ ],1138+ "engines": {1139+ "node": "^20.9.0"1140+ },1141+ "funding": {1142+ "url": "https://opencollective.com/libvips"1143+ }1144+ },1145+ "node_modules/@img/sharp-win32-x64": {1146+ "version": "0.35.2",1147+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.2.tgz",1148+ "integrity": "sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==",1149+ "cpu": [1150+ "x64"1151+ ],1152+ "dev": true,1153+ "license": "Apache-2.0 AND LGPL-3.0-or-later",1154+ "optional": true,1155+ "os": [1156+ "win32"1157+ ],1158+ "engines": {1159+ "node": ">=20.9.0"1160+ },1161+ "funding": {1162+ "url": "https://opencollective.com/libvips"1163+ }1164+ },1165+ "node_modules/@jridgewell/resolve-uri": {1166+ "version": "3.1.2",1167+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",1168+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",1169+ "dev": true,1170+ "license": "MIT",1171+ "engines": {1172+ "node": ">=6.0.0"1173+ }1174+ },1175+ "node_modules/@jridgewell/sourcemap-codec": {1176+ "version": "1.5.5",1177+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",1178+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",1179+ "dev": true,1180+ "license": "MIT"1181+ },1182+ "node_modules/@jridgewell/trace-mapping": {1183+ "version": "0.3.9",1184+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",1185+ "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",1186+ "dev": true,1187+ "license": "MIT",1188+ "dependencies": {1189+ "@jridgewell/resolve-uri": "^3.0.3",1190+ "@jridgewell/sourcemap-codec": "^1.4.10"1191+ }1192+ },1193+ "node_modules/@modelcontextprotocol/core": {1194+ "version": "2.0.0",1195+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0.tgz",1196+ "integrity": "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==",1197+ "license": "MIT",1198+ "dependencies": {1199+ "zod": "^4.2.0"1200+ },1201+ "engines": {1202+ "node": ">=20"1203+ }1204+ },1205+ "node_modules/@modelcontextprotocol/ext-apps": {1206+ "version": "1.7.5",1207+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-1.7.5.tgz",1208+ "integrity": "sha512-TjPH2S2y5UEGKhmI6+XGFuqfqOV4ppe1x6DA3txnUaEWkgtA4G5vo14jGKFZmegdkZ1H4QMLyujLvoU1BEdnAg==",1209+ "license": "MIT",1210+ "workspaces": [1211+ "examples/*"1212+ ],1213+ "dependencies": {1214+ "@standard-schema/spec": "^1.1.0"1215+ },1216+ "engines": {1217+ "node": ">=20"1218+ },1219+ "peerDependencies": {1220+ "@modelcontextprotocol/sdk": "^1.29.0",1221+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",1222+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",1223+ "zod": "^3.25.0 || ^4.0.0"1224+ },1225+ "peerDependenciesMeta": {1226+ "react": {1227+ "optional": true1228+ },1229+ "react-dom": {1230+ "optional": true1231+ }1232+ }1233+ },1234+ "node_modules/@modelcontextprotocol/hono": {1235+ "version": "2.0.0",1236+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/hono/-/hono-2.0.0.tgz",1237+ "integrity": "sha512-lKCSXRusLxpX63G+6og9ewcRkqVbjB2xSxpvU/VJhxcUkpvGgpDrmcgNRQ4dTapGr8AspNFdRBdmJc500Vhl/A==",1238+ "license": "MIT",1239+ "engines": {1240+ "node": ">=20"1241+ },1242+ "peerDependencies": {1243+ "@modelcontextprotocol/server": "^2.0.0",1244+ "hono": "^4.11.4"1245+ }1246+ },1247+ "node_modules/@modelcontextprotocol/sdk": {1248+ "version": "1.30.0",1249+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz",1250+ "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==",1251+ "license": "MIT",1252+ "dependencies": {1253+ "@hono/node-server": "^1.19.9 || ^2.0.5",1254+ "ajv": "^8.17.1",1255+ "ajv-formats": "^3.0.1",1256+ "content-type": "^1.0.5",1257+ "cors": "^2.8.5",1258+ "cross-spawn": "^7.0.5",1259+ "eventsource": "^3.0.2",1260+ "eventsource-parser": "^3.0.0",1261+ "express": "^5.2.1",1262+ "express-rate-limit": "^8.2.1",1263+ "hono": "^4.11.4",1264+ "jose": "^6.1.3",1265+ "json-schema-typed": "^8.0.2",1266+ "pkce-challenge": "^5.0.0",1267+ "raw-body": "^3.0.0",1268+ "zod": "^3.25 || ^4.0",1269+ "zod-to-json-schema": "^3.25.1"1270+ },1271+ "engines": {1272+ "node": ">=18"1273+ },1274+ "peerDependencies": {1275+ "@cfworker/json-schema": "^4.1.1",1276+ "zod": "^3.25 || ^4.0"1277+ },1278+ "peerDependenciesMeta": {1279+ "@cfworker/json-schema": {1280+ "optional": true1281+ },1282+ "zod": {1283+ "optional": false1284+ }1285+ }1286+ },1287+ "node_modules/@modelcontextprotocol/server": {1288+ "version": "2.0.0",1289+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0.tgz",1290+ "integrity": "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==",1291+ "license": "MIT",1292+ "dependencies": {1293+ "@modelcontextprotocol/core": "2.0.0",1294+ "zod": "^4.2.0"1295+ },1296+ "engines": {1297+ "node": ">=20"1298+ }1299+ },1300+ "node_modules/@oxc-project/types": {1301+ "version": "0.144.0",1302+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.144.0.tgz",1303+ "integrity": "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==",1304+ "dev": true,1305+ "license": "MIT",1306+ "funding": {1307+ "url": "https://github.com/sponsors/Boshen"1308+ }1309+ },1310+ "node_modules/@poppinss/colors": {1311+ "version": "4.1.6",1312+ "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz",1313+ "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==",1314+ "dev": true,1315+ "license": "MIT",1316+ "dependencies": {1317+ "kleur": "^4.1.5"1318+ }1319+ },1320+ "node_modules/@poppinss/dumper": {1321+ "version": "0.6.5",1322+ "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz",1323+ "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==",1324+ "dev": true,1325+ "license": "MIT",1326+ "dependencies": {1327+ "@poppinss/colors": "^4.1.5",1328+ "@sindresorhus/is": "^7.0.2",1329+ "supports-color": "^10.0.0"1330+ }1331+ },1332+ "node_modules/@poppinss/exception": {1333+ "version": "1.2.3",1334+ "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz",1335+ "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==",1336+ "dev": true,1337+ "license": "MIT"1338+ },1339+ "node_modules/@rolldown/binding-android-arm64": {1340+ "version": "1.2.4",1341+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz",1342+ "integrity": "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==",1343+ "cpu": [1344+ "arm64"1345+ ],1346+ "dev": true,1347+ "license": "MIT",1348+ "optional": true,1349+ "os": [1350+ "android"1351+ ],1352+ "engines": {1353+ "node": "^20.19.0 || >=22.12.0"1354+ }1355+ },1356+ "node_modules/@rolldown/binding-darwin-arm64": {1357+ "version": "1.2.4",1358+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.4.tgz",1359+ "integrity": "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==",1360+ "cpu": [1361+ "arm64"1362+ ],1363+ "dev": true,1364+ "license": "MIT",1365+ "optional": true,1366+ "os": [1367+ "darwin"1368+ ],1369+ "engines": {1370+ "node": "^20.19.0 || >=22.12.0"1371+ }1372+ },1373+ "node_modules/@rolldown/binding-darwin-x64": {1374+ "version": "1.2.4",1375+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.4.tgz",1376+ "integrity": "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==",1377+ "cpu": [1378+ "x64"1379+ ],1380+ "dev": true,1381+ "license": "MIT",1382+ "optional": true,1383+ "os": [1384+ "darwin"1385+ ],1386+ "engines": {1387+ "node": "^20.19.0 || >=22.12.0"1388+ }1389+ },1390+ "node_modules/@rolldown/binding-freebsd-x64": {1391+ "version": "1.2.4",1392+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.4.tgz",1393+ "integrity": "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==",1394+ "cpu": [1395+ "x64"1396+ ],1397+ "dev": true,1398+ "license": "MIT",1399+ "optional": true,1400+ "os": [1401+ "freebsd"1402+ ],1403+ "engines": {1404+ "node": "^20.19.0 || >=22.12.0"1405+ }1406+ },1407+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {1408+ "version": "1.2.4",1409+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.4.tgz",1410+ "integrity": "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==",1411+ "cpu": [1412+ "arm"1413+ ],1414+ "dev": true,1415+ "license": "MIT",1416+ "optional": true,1417+ "os": [1418+ "linux"1419+ ],1420+ "engines": {1421+ "node": "^20.19.0 || >=22.12.0"1422+ }1423+ },1424+ "node_modules/@rolldown/binding-linux-arm64-gnu": {1425+ "version": "1.2.4",1426+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.4.tgz",1427+ "integrity": "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==",1428+ "cpu": [1429+ "arm64"1430+ ],1431+ "dev": true,1432+ "license": "MIT",1433+ "optional": true,1434+ "os": [1435+ "linux"1436+ ],1437+ "engines": {1438+ "node": "^20.19.0 || >=22.12.0"1439+ }1440+ },1441+ "node_modules/@rolldown/binding-linux-arm64-musl": {1442+ "version": "1.2.4",1443+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.4.tgz",1444+ "integrity": "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==",1445+ "cpu": [1446+ "arm64"1447+ ],1448+ "dev": true,1449+ "license": "MIT",1450+ "optional": true,1451+ "os": [1452+ "linux"1453+ ],1454+ "engines": {1455+ "node": "^20.19.0 || >=22.12.0"1456+ }1457+ },1458+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {1459+ "version": "1.2.4",1460+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.4.tgz",1461+ "integrity": "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==",1462+ "cpu": [1463+ "ppc64"1464+ ],1465+ "dev": true,1466+ "license": "MIT",1467+ "optional": true,1468+ "os": [1469+ "linux"1470+ ],1471+ "engines": {1472+ "node": "^20.19.0 || >=22.12.0"1473+ }1474+ },1475+ "node_modules/@rolldown/binding-linux-s390x-gnu": {1476+ "version": "1.2.4",1477+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.4.tgz",1478+ "integrity": "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==",1479+ "cpu": [1480+ "s390x"1481+ ],1482+ "dev": true,1483+ "license": "MIT",1484+ "optional": true,1485+ "os": [1486+ "linux"1487+ ],1488+ "engines": {1489+ "node": "^20.19.0 || >=22.12.0"1490+ }1491+ },1492+ "node_modules/@rolldown/binding-linux-x64-gnu": {1493+ "version": "1.2.4",1494+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.4.tgz",1495+ "integrity": "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==",1496+ "cpu": [1497+ "x64"1498+ ],1499+ "dev": true,1500+ "license": "MIT",1501+ "optional": true,1502+ "os": [1503+ "linux"1504+ ],1505+ "engines": {1506+ "node": "^20.19.0 || >=22.12.0"1507+ }1508+ },1509+ "node_modules/@rolldown/binding-linux-x64-musl": {1510+ "version": "1.2.4",1511+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.4.tgz",1512+ "integrity": "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==",1513+ "cpu": [1514+ "x64"1515+ ],1516+ "dev": true,1517+ "license": "MIT",1518+ "optional": true,1519+ "os": [1520+ "linux"1521+ ],1522+ "engines": {1523+ "node": "^20.19.0 || >=22.12.0"1524+ }1525+ },1526+ "node_modules/@rolldown/binding-openharmony-arm64": {1527+ "version": "1.2.4",1528+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.4.tgz",1529+ "integrity": "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==",1530+ "cpu": [1531+ "arm64"1532+ ],1533+ "dev": true,1534+ "license": "MIT",1535+ "optional": true,1536+ "os": [1537+ "openharmony"1538+ ],1539+ "engines": {1540+ "node": "^20.19.0 || >=22.12.0"1541+ }1542+ },1543+ "node_modules/@rolldown/binding-win32-arm64-msvc": {1544+ "version": "1.2.4",1545+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.4.tgz",1546+ "integrity": "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==",1547+ "cpu": [1548+ "arm64"1549+ ],1550+ "dev": true,1551+ "license": "MIT",1552+ "optional": true,1553+ "os": [1554+ "win32"1555+ ],1556+ "engines": {1557+ "node": "^20.19.0 || >=22.12.0"1558+ }1559+ },1560+ "node_modules/@rolldown/binding-win32-x64-msvc": {1561+ "version": "1.2.4",1562+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.4.tgz",1563+ "integrity": "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==",1564+ "cpu": [1565+ "x64"1566+ ],1567+ "dev": true,1568+ "license": "MIT",1569+ "optional": true,1570+ "os": [1571+ "win32"1572+ ],1573+ "engines": {1574+ "node": "^20.19.0 || >=22.12.0"1575+ }1576+ },1577+ "node_modules/@rolldown/pluginutils": {1578+ "version": "1.0.1",1579+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",1580+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",1581+ "dev": true,1582+ "license": "MIT"1583+ },1584+ "node_modules/@sindresorhus/is": {1585+ "version": "7.2.0",1586+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz",1587+ "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==",1588+ "dev": true,1589+ "license": "MIT",1590+ "engines": {1591+ "node": ">=18"1592+ },1593+ "funding": {1594+ "url": "https://github.com/sindresorhus/is?sponsor=1"1595+ }1596+ },1597+ "node_modules/@speed-highlight/core": {1598+ "version": "1.2.24",1599+ "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.24.tgz",1600+ "integrity": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==",1601+ "dev": true,1602+ "license": "CC0-1.0"1603+ },1604+ "node_modules/@standard-schema/spec": {1605+ "version": "1.1.0",1606+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",1607+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",1608+ "license": "MIT"1609+ },1610+ "node_modules/@types/chai": {1611+ "version": "5.2.3",1612+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",1613+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",1614+ "dev": true,1615+ "license": "MIT",1616+ "dependencies": {1617+ "@types/deep-eql": "*",1618+ "assertion-error": "^2.0.1"1619+ }1620+ },1621+ "node_modules/@types/deep-eql": {1622+ "version": "4.0.2",1623+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",1624+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",1625+ "dev": true,1626+ "license": "MIT"1627+ },1628+ "node_modules/@types/estree": {1629+ "version": "1.0.9",1630+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",1631+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",1632+ "dev": true,1633+ "license": "MIT"1634+ },1635+ "node_modules/@types/node": {1636+ "version": "26.2.0",1637+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz",1638+ "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==",1639+ "dev": true,1640+ "license": "MIT",1641+ "dependencies": {1642+ "undici-types": "~8.3.0"1643+ }1644+ },1645+ "node_modules/@vitest/expect": {1646+ "version": "4.1.10",1647+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz",1648+ "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==",1649+ "dev": true,1650+ "license": "MIT",1651+ "dependencies": {1652+ "@standard-schema/spec": "^1.1.0",1653+ "@types/chai": "^5.2.2",1654+ "@vitest/spy": "4.1.10",1655+ "@vitest/utils": "4.1.10",1656+ "chai": "^6.2.2",1657+ "tinyrainbow": "^3.1.0"1658+ },1659+ "funding": {1660+ "url": "https://opencollective.com/vitest"1661+ }1662+ },1663+ "node_modules/@vitest/mocker": {1664+ "version": "4.1.10",1665+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz",1666+ "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==",1667+ "dev": true,1668+ "license": "MIT",1669+ "dependencies": {1670+ "@vitest/spy": "4.1.10",1671+ "estree-walker": "^3.0.3",1672+ "magic-string": "^0.30.21"1673+ },1674+ "funding": {1675+ "url": "https://opencollective.com/vitest"1676+ },1677+ "peerDependencies": {1678+ "msw": "^2.4.9",1679+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"1680+ },1681+ "peerDependenciesMeta": {1682+ "msw": {1683+ "optional": true1684+ },1685+ "vite": {1686+ "optional": true1687+ }1688+ }1689+ },1690+ "node_modules/@vitest/pretty-format": {1691+ "version": "4.1.10",1692+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz",1693+ "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==",1694+ "dev": true,1695+ "license": "MIT",1696+ "dependencies": {1697+ "tinyrainbow": "^3.1.0"1698+ },1699+ "funding": {1700+ "url": "https://opencollective.com/vitest"1701+ }1702+ },1703+ "node_modules/@vitest/runner": {1704+ "version": "4.1.10",1705+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz",1706+ "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==",1707+ "dev": true,1708+ "license": "MIT",1709+ "dependencies": {1710+ "@vitest/utils": "4.1.10",1711+ "pathe": "^2.0.3"1712+ },1713+ "funding": {1714+ "url": "https://opencollective.com/vitest"1715+ }1716+ },1717+ "node_modules/@vitest/snapshot": {1718+ "version": "4.1.10",1719+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz",1720+ "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==",1721+ "dev": true,1722+ "license": "MIT",1723+ "dependencies": {1724+ "@vitest/pretty-format": "4.1.10",1725+ "@vitest/utils": "4.1.10",1726+ "magic-string": "^0.30.21",1727+ "pathe": "^2.0.3"1728+ },1729+ "funding": {1730+ "url": "https://opencollective.com/vitest"1731+ }1732+ },1733+ "node_modules/@vitest/spy": {1734+ "version": "4.1.10",1735+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz",1736+ "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==",1737+ "dev": true,1738+ "license": "MIT",1739+ "funding": {1740+ "url": "https://opencollective.com/vitest"1741+ }1742+ },1743+ "node_modules/@vitest/utils": {1744+ "version": "4.1.10",1745+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz",1746+ "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==",1747+ "dev": true,1748+ "license": "MIT",1749+ "dependencies": {1750+ "@vitest/pretty-format": "4.1.10",1751+ "convert-source-map": "^2.0.0",1752+ "tinyrainbow": "^3.1.0"1753+ },1754+ "funding": {1755+ "url": "https://opencollective.com/vitest"1756+ }1757+ },1758+ "node_modules/98.css": {1759+ "version": "0.1.21",1760+ "resolved": "https://registry.npmjs.org/98.css/-/98.css-0.1.21.tgz",1761+ "integrity": "sha512-ddk5qtUWyapM0Bzd5jwGExoE5fdSEGrP+F5VbYjyZLf2c9UVmn6w2NPTvCsoD4BWdGsjdLjlkQGhWwWTJcYQJQ==",1762+ "license": "MIT"1763+ },1764+ "node_modules/accepts": {1765+ "version": "2.0.0",1766+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",1767+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",1768+ "license": "MIT",1769+ "dependencies": {1770+ "mime-types": "^3.0.0",1771+ "negotiator": "^1.0.0"1772+ },1773+ "engines": {1774+ "node": ">= 0.6"1775+ }1776+ },1777+ "node_modules/ajv": {1778+ "version": "8.20.0",1779+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",1780+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",1781+ "license": "MIT",1782+ "dependencies": {1783+ "fast-deep-equal": "^3.1.3",1784+ "fast-uri": "^3.0.1",1785+ "json-schema-traverse": "^1.0.0",1786+ "require-from-string": "^2.0.2"1787+ },1788+ "funding": {1789+ "type": "github",1790+ "url": "https://github.com/sponsors/epoberezkin"1791+ }1792+ },1793+ "node_modules/ajv-formats": {1794+ "version": "3.0.1",1795+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",1796+ "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",1797+ "license": "MIT",1798+ "dependencies": {1799+ "ajv": "^8.0.0"1800+ },1801+ "peerDependencies": {1802+ "ajv": "^8.0.0"1803+ },1804+ "peerDependenciesMeta": {1805+ "ajv": {1806+ "optional": true1807+ }1808+ }1809+ },1810+ "node_modules/assertion-error": {1811+ "version": "2.0.1",1812+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",1813+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",1814+ "dev": true,1815+ "license": "MIT",1816+ "engines": {1817+ "node": ">=12"1818+ }1819+ },1820+ "node_modules/blake3-wasm": {1821+ "version": "2.1.5",1822+ "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz",1823+ "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==",1824+ "dev": true,1825+ "license": "MIT"1826+ },1827+ "node_modules/body-parser": {1828+ "version": "2.3.0",1829+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",1830+ "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",1831+ "license": "MIT",1832+ "dependencies": {1833+ "bytes": "^3.1.2",1834+ "content-type": "^2.0.0",1835+ "debug": "^4.4.3",1836+ "http-errors": "^2.0.1",1837+ "iconv-lite": "^0.7.2",1838+ "on-finished": "^2.4.1",1839+ "qs": "^6.15.2",1840+ "raw-body": "^3.0.2",1841+ "type-is": "^2.1.0"1842+ },1843+ "engines": {1844+ "node": ">=18"1845+ },1846+ "funding": {1847+ "type": "opencollective",1848+ "url": "https://opencollective.com/express"1849+ }1850+ },1851+ "node_modules/body-parser/node_modules/content-type": {1852+ "version": "2.0.0",1853+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",1854+ "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",1855+ "license": "MIT",1856+ "engines": {1857+ "node": ">=18"1858+ },1859+ "funding": {1860+ "type": "opencollective",1861+ "url": "https://opencollective.com/express"1862+ }1863+ },1864+ "node_modules/braces": {1865+ "version": "3.0.3",1866+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",1867+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",1868+ "dev": true,1869+ "license": "MIT",1870+ "dependencies": {1871+ "fill-range": "^7.1.1"1872+ },1873+ "engines": {1874+ "node": ">=8"1875+ }1876+ },1877+ "node_modules/bytes": {1878+ "version": "3.1.2",1879+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",1880+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",1881+ "license": "MIT",1882+ "engines": {1883+ "node": ">= 0.8"1884+ }1885+ },1886+ "node_modules/call-bind-apply-helpers": {1887+ "version": "1.0.2",1888+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",1889+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",1890+ "license": "MIT",1891+ "dependencies": {1892+ "es-errors": "^1.3.0",1893+ "function-bind": "^1.1.2"1894+ },1895+ "engines": {1896+ "node": ">= 0.4"1897+ }1898+ },1899+ "node_modules/call-bound": {1900+ "version": "1.0.4",1901+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",1902+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",1903+ "license": "MIT",1904+ "dependencies": {1905+ "call-bind-apply-helpers": "^1.0.2",1906+ "get-intrinsic": "^1.3.0"1907+ },1908+ "engines": {1909+ "node": ">= 0.4"1910+ },1911+ "funding": {1912+ "url": "https://github.com/sponsors/ljharb"1913+ }1914+ },1915+ "node_modules/chai": {1916+ "version": "6.2.2",1917+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",1918+ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",1919+ "dev": true,1920+ "license": "MIT",1921+ "engines": {1922+ "node": ">=18"1923+ }1924+ },1925+ "node_modules/cjs-module-lexer": {1926+ "version": "1.2.3",1927+ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz",1928+ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==",1929+ "dev": true,1930+ "license": "MIT"1931+ },1932+ "node_modules/content-disposition": {1933+ "version": "1.1.0",1934+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",1935+ "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",1936+ "license": "MIT",1937+ "engines": {1938+ "node": ">=18"1939+ },1940+ "funding": {1941+ "type": "opencollective",1942+ "url": "https://opencollective.com/express"1943+ }1944+ },1945+ "node_modules/content-type": {1946+ "version": "1.0.5",1947+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",1948+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",1949+ "license": "MIT",1950+ "engines": {1951+ "node": ">= 0.6"1952+ }1953+ },1954+ "node_modules/convert-source-map": {1955+ "version": "2.0.0",1956+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",1957+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",1958+ "dev": true,1959+ "license": "MIT"1960+ },1961+ "node_modules/cookie": {1962+ "version": "0.7.2",1963+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",1964+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",1965+ "license": "MIT",1966+ "engines": {1967+ "node": ">= 0.6"1968+ }1969+ },1970+ "node_modules/cookie-signature": {1971+ "version": "1.2.2",1972+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",1973+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",1974+ "license": "MIT",1975+ "engines": {1976+ "node": ">=6.6.0"1977+ }1978+ },1979+ "node_modules/cors": {1980+ "version": "2.8.6",1981+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",1982+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",1983+ "license": "MIT",1984+ "dependencies": {1985+ "object-assign": "^4",1986+ "vary": "^1"1987+ },1988+ "engines": {1989+ "node": ">= 0.10"1990+ },1991+ "funding": {1992+ "type": "opencollective",1993+ "url": "https://opencollective.com/express"1994+ }1995+ },1996+ "node_modules/cross-spawn": {1997+ "version": "7.0.6",1998+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",1999+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",2000+ "license": "MIT",2001+ "dependencies": {2002+ "path-key": "^3.1.0",2003+ "shebang-command": "^2.0.0",2004+ "which": "^2.0.1"2005+ },2006+ "engines": {2007+ "node": ">= 8"2008+ }2009+ },2010+ "node_modules/debug": {2011+ "version": "4.4.3",2012+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",2013+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",2014+ "license": "MIT",2015+ "dependencies": {2016+ "ms": "^2.1.3"2017+ },2018+ "engines": {2019+ "node": ">=6.0"2020+ },2021+ "peerDependenciesMeta": {2022+ "supports-color": {2023+ "optional": true2024+ }2025+ }2026+ },2027+ "node_modules/depd": {2028+ "version": "2.0.0",2029+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",2030+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",2031+ "license": "MIT",2032+ "engines": {2033+ "node": ">= 0.8"2034+ }2035+ },2036+ "node_modules/detect-libc": {2037+ "version": "2.1.2",2038+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",2039+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",2040+ "dev": true,2041+ "license": "Apache-2.0",2042+ "engines": {2043+ "node": ">=8"2044+ }2045+ },2046+ "node_modules/dunder-proto": {2047+ "version": "1.0.1",2048+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",2049+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",2050+ "license": "MIT",2051+ "dependencies": {2052+ "call-bind-apply-helpers": "^1.0.1",2053+ "es-errors": "^1.3.0",2054+ "gopd": "^1.2.0"2055+ },2056+ "engines": {2057+ "node": ">= 0.4"2058+ }2059+ },2060+ "node_modules/ee-first": {2061+ "version": "1.1.1",2062+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",2063+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",2064+ "license": "MIT"2065+ },2066+ "node_modules/encodeurl": {2067+ "version": "2.0.0",2068+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",2069+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",2070+ "license": "MIT",2071+ "engines": {2072+ "node": ">= 0.8"2073+ }2074+ },2075+ "node_modules/error-stack-parser-es": {2076+ "version": "1.0.5",2077+ "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz",2078+ "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==",2079+ "dev": true,2080+ "license": "MIT",2081+ "funding": {2082+ "url": "https://github.com/sponsors/antfu"2083+ }2084+ },2085+ "node_modules/es-define-property": {2086+ "version": "1.0.1",2087+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",2088+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",2089+ "license": "MIT",2090+ "engines": {2091+ "node": ">= 0.4"2092+ }2093+ },2094+ "node_modules/es-errors": {2095+ "version": "1.3.0",2096+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",2097+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",2098+ "license": "MIT",2099+ "engines": {2100+ "node": ">= 0.4"2101+ }2102+ },2103+ "node_modules/es-module-lexer": {2104+ "version": "2.3.1",2105+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz",2106+ "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==",2107+ "dev": true,2108+ "license": "MIT"2109+ },2110+ "node_modules/es-object-atoms": {2111+ "version": "1.1.2",2112+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",2113+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",2114+ "license": "MIT",2115+ "dependencies": {2116+ "es-errors": "^1.3.0"2117+ },2118+ "engines": {2119+ "node": ">= 0.4"2120+ }2121+ },2122+ "node_modules/esbuild": {2123+ "version": "0.28.1",2124+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",2125+ "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",2126+ "dev": true,2127+ "hasInstallScript": true,2128+ "license": "MIT",2129+ "bin": {2130+ "esbuild": "bin/esbuild"2131+ },2132+ "engines": {2133+ "node": ">=18"2134+ },2135+ "optionalDependencies": {2136+ "@esbuild/aix-ppc64": "0.28.1",2137+ "@esbuild/android-arm": "0.28.1",2138+ "@esbuild/android-arm64": "0.28.1",2139+ "@esbuild/android-x64": "0.28.1",2140+ "@esbuild/darwin-arm64": "0.28.1",2141+ "@esbuild/darwin-x64": "0.28.1",2142+ "@esbuild/freebsd-arm64": "0.28.1",2143+ "@esbuild/freebsd-x64": "0.28.1",2144+ "@esbuild/linux-arm": "0.28.1",2145+ "@esbuild/linux-arm64": "0.28.1",2146+ "@esbuild/linux-ia32": "0.28.1",2147+ "@esbuild/linux-loong64": "0.28.1",2148+ "@esbuild/linux-mips64el": "0.28.1",2149+ "@esbuild/linux-ppc64": "0.28.1",2150+ "@esbuild/linux-riscv64": "0.28.1",2151+ "@esbuild/linux-s390x": "0.28.1",2152+ "@esbuild/linux-x64": "0.28.1",2153+ "@esbuild/netbsd-arm64": "0.28.1",2154+ "@esbuild/netbsd-x64": "0.28.1",2155+ "@esbuild/openbsd-arm64": "0.28.1",2156+ "@esbuild/openbsd-x64": "0.28.1",2157+ "@esbuild/openharmony-arm64": "0.28.1",2158+ "@esbuild/sunos-x64": "0.28.1",2159+ "@esbuild/win32-arm64": "0.28.1",2160+ "@esbuild/win32-ia32": "0.28.1",2161+ "@esbuild/win32-x64": "0.28.1"2162+ }2163+ },2164+ "node_modules/escape-html": {2165+ "version": "1.0.3",2166+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",2167+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",2168+ "license": "MIT"2169+ },2170+ "node_modules/estree-walker": {2171+ "version": "3.0.3",2172+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",2173+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",2174+ "dev": true,2175+ "license": "MIT",2176+ "dependencies": {2177+ "@types/estree": "^1.0.0"2178+ }2179+ },2180+ "node_modules/etag": {2181+ "version": "1.8.1",2182+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",2183+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",2184+ "license": "MIT",2185+ "engines": {2186+ "node": ">= 0.6"2187+ }2188+ },2189+ "node_modules/eventsource": {2190+ "version": "3.0.7",2191+ "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",2192+ "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",2193+ "license": "MIT",2194+ "dependencies": {2195+ "eventsource-parser": "^3.0.1"2196+ },2197+ "engines": {2198+ "node": ">=18.0.0"2199+ }2200+ },2201+ "node_modules/eventsource-parser": {2202+ "version": "3.1.1",2203+ "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz",2204+ "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==",2205+ "license": "MIT",2206+ "engines": {2207+ "node": ">=18.0.0"2208+ }2209+ },2210+ "node_modules/expect-type": {2211+ "version": "1.4.0",2212+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",2213+ "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",2214+ "dev": true,2215+ "license": "Apache-2.0",2216+ "engines": {2217+ "node": ">=12.0.0"2218+ }2219+ },2220+ "node_modules/express": {2221+ "version": "5.2.1",2222+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",2223+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",2224+ "license": "MIT",2225+ "dependencies": {2226+ "accepts": "^2.0.0",2227+ "body-parser": "^2.2.1",2228+ "content-disposition": "^1.0.0",2229+ "content-type": "^1.0.5",2230+ "cookie": "^0.7.1",2231+ "cookie-signature": "^1.2.1",2232+ "debug": "^4.4.0",2233+ "depd": "^2.0.0",2234+ "encodeurl": "^2.0.0",2235+ "escape-html": "^1.0.3",2236+ "etag": "^1.8.1",2237+ "finalhandler": "^2.1.0",2238+ "fresh": "^2.0.0",2239+ "http-errors": "^2.0.0",2240+ "merge-descriptors": "^2.0.0",2241+ "mime-types": "^3.0.0",2242+ "on-finished": "^2.4.1",2243+ "once": "^1.4.0",2244+ "parseurl": "^1.3.3",2245+ "proxy-addr": "^2.0.7",2246+ "qs": "^6.14.0",2247+ "range-parser": "^1.2.1",2248+ "router": "^2.2.0",2249+ "send": "^1.1.0",2250+ "serve-static": "^2.2.0",2251+ "statuses": "^2.0.1",2252+ "type-is": "^2.0.1",2253+ "vary": "^1.1.2"2254+ },2255+ "engines": {2256+ "node": ">= 18"2257+ },2258+ "funding": {2259+ "type": "opencollective",2260+ "url": "https://opencollective.com/express"2261+ }2262+ },2263+ "node_modules/express-rate-limit": {2264+ "version": "8.6.2",2265+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.2.tgz",2266+ "integrity": "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==",2267+ "license": "MIT",2268+ "dependencies": {2269+ "debug": "^4.4.3",2270+ "ip-address": "^10.2.0"2271+ },2272+ "engines": {2273+ "node": ">= 16"2274+ },2275+ "funding": {2276+ "url": "https://github.com/sponsors/express-rate-limit"2277+ },2278+ "peerDependencies": {2279+ "express": ">= 4.11"2280+ }2281+ },2282+ "node_modules/fast-deep-equal": {2283+ "version": "3.1.3",2284+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",2285+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",2286+ "license": "MIT"2287+ },2288+ "node_modules/fast-uri": {2289+ "version": "3.1.5",2290+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz",2291+ "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==",2292+ "funding": [2293+ {2294+ "type": "github",2295+ "url": "https://github.com/sponsors/fastify"2296+ },2297+ {2298+ "type": "opencollective",2299+ "url": "https://opencollective.com/fastify"2300+ }2301+ ],2302+ "license": "BSD-3-Clause"2303+ },2304+ "node_modules/fdir": {2305+ "version": "6.5.0",2306+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",2307+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",2308+ "dev": true,2309+ "license": "MIT",2310+ "engines": {2311+ "node": ">=12.0.0"2312+ },2313+ "peerDependencies": {2314+ "picomatch": "^3 || ^4"2315+ },2316+ "peerDependenciesMeta": {2317+ "picomatch": {2318+ "optional": true2319+ }2320+ }2321+ },2322+ "node_modules/fill-range": {2323+ "version": "7.1.1",2324+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",2325+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",2326+ "dev": true,2327+ "license": "MIT",2328+ "dependencies": {2329+ "to-regex-range": "^5.0.1"2330+ },2331+ "engines": {2332+ "node": ">=8"2333+ }2334+ },2335+ "node_modules/finalhandler": {2336+ "version": "2.1.1",2337+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",2338+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",2339+ "license": "MIT",2340+ "dependencies": {2341+ "debug": "^4.4.0",2342+ "encodeurl": "^2.0.0",2343+ "escape-html": "^1.0.3",2344+ "on-finished": "^2.4.1",2345+ "parseurl": "^1.3.3",2346+ "statuses": "^2.0.1"2347+ },2348+ "engines": {2349+ "node": ">= 18.0.0"2350+ },2351+ "funding": {2352+ "type": "opencollective",2353+ "url": "https://opencollective.com/express"2354+ }2355+ },2356+ "node_modules/forwarded": {2357+ "version": "0.2.0",2358+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",2359+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",2360+ "license": "MIT",2361+ "engines": {2362+ "node": ">= 0.6"2363+ }2364+ },2365+ "node_modules/fresh": {2366+ "version": "2.0.0",2367+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",2368+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",2369+ "license": "MIT",2370+ "engines": {2371+ "node": ">= 0.8"2372+ }2373+ },2374+ "node_modules/fsevents": {2375+ "version": "2.3.3",2376+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",2377+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",2378+ "dev": true,2379+ "hasInstallScript": true,2380+ "license": "MIT",2381+ "optional": true,2382+ "os": [2383+ "darwin"2384+ ],2385+ "engines": {2386+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"2387+ }2388+ },2389+ "node_modules/function-bind": {2390+ "version": "1.1.2",2391+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",2392+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",2393+ "license": "MIT",2394+ "funding": {2395+ "url": "https://github.com/sponsors/ljharb"2396+ }2397+ },2398+ "node_modules/get-intrinsic": {2399+ "version": "1.3.0",2400+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",2401+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",2402+ "license": "MIT",2403+ "dependencies": {2404+ "call-bind-apply-helpers": "^1.0.2",2405+ "es-define-property": "^1.0.1",2406+ "es-errors": "^1.3.0",2407+ "es-object-atoms": "^1.1.1",2408+ "function-bind": "^1.1.2",2409+ "get-proto": "^1.0.1",2410+ "gopd": "^1.2.0",2411+ "has-symbols": "^1.1.0",2412+ "hasown": "^2.0.2",2413+ "math-intrinsics": "^1.1.0"2414+ },2415+ "engines": {2416+ "node": ">= 0.4"2417+ },2418+ "funding": {2419+ "url": "https://github.com/sponsors/ljharb"2420+ }2421+ },2422+ "node_modules/get-proto": {2423+ "version": "1.0.1",2424+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",2425+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",2426+ "license": "MIT",2427+ "dependencies": {2428+ "dunder-proto": "^1.0.1",2429+ "es-object-atoms": "^1.0.0"2430+ },2431+ "engines": {2432+ "node": ">= 0.4"2433+ }2434+ },2435+ "node_modules/gopd": {2436+ "version": "1.2.0",2437+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",2438+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",2439+ "license": "MIT",2440+ "engines": {2441+ "node": ">= 0.4"2442+ },2443+ "funding": {2444+ "url": "https://github.com/sponsors/ljharb"2445+ }2446+ },2447+ "node_modules/has-symbols": {2448+ "version": "1.1.0",2449+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",2450+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",2451+ "license": "MIT",2452+ "engines": {2453+ "node": ">= 0.4"2454+ },2455+ "funding": {2456+ "url": "https://github.com/sponsors/ljharb"2457+ }2458+ },2459+ "node_modules/hasown": {2460+ "version": "2.0.4",2461+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",2462+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",2463+ "license": "MIT",2464+ "dependencies": {2465+ "function-bind": "^1.1.2"2466+ },2467+ "engines": {2468+ "node": ">= 0.4"2469+ }2470+ },2471+ "node_modules/hono": {2472+ "version": "4.13.1",2473+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.1.tgz",2474+ "integrity": "sha512-kdJoFVv2xmayw6cY09H7AbMJMt8Jn5jdlEdXsP7AGBdF2DIptVlKlOLKXP41yPip4/a3yQPv9gVcJYI8YY04dw==",2475+ "license": "MIT",2476+ "engines": {2477+ "node": ">=16.9.0"2478+ }2479+ },2480+ "node_modules/http-errors": {2481+ "version": "2.0.1",2482+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",2483+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",2484+ "license": "MIT",2485+ "dependencies": {2486+ "depd": "~2.0.0",2487+ "inherits": "~2.0.4",2488+ "setprototypeof": "~1.2.0",2489+ "statuses": "~2.0.2",2490+ "toidentifier": "~1.0.1"2491+ },2492+ "engines": {2493+ "node": ">= 0.8"2494+ },2495+ "funding": {2496+ "type": "opencollective",2497+ "url": "https://opencollective.com/express"2498+ }2499+ },2500+ "node_modules/iconv-lite": {2501+ "version": "0.7.3",2502+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",2503+ "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",2504+ "license": "MIT",2505+ "dependencies": {2506+ "safer-buffer": ">= 2.1.2 < 3.0.0"2507+ },2508+ "engines": {2509+ "node": ">=0.10.0"2510+ },2511+ "funding": {2512+ "type": "opencollective",2513+ "url": "https://opencollective.com/express"2514+ }2515+ },2516+ "node_modules/inherits": {2517+ "version": "2.0.4",2518+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",2519+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",2520+ "license": "ISC"2521+ },2522+ "node_modules/ip-address": {2523+ "version": "10.5.0",2524+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz",2525+ "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==",2526+ "license": "MIT",2527+ "engines": {2528+ "node": ">= 12"2529+ }2530+ },2531+ "node_modules/ipaddr.js": {2532+ "version": "1.9.1",2533+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",2534+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",2535+ "license": "MIT",2536+ "engines": {2537+ "node": ">= 0.10"2538+ }2539+ },2540+ "node_modules/is-number": {2541+ "version": "7.0.0",2542+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",2543+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",2544+ "dev": true,2545+ "license": "MIT",2546+ "engines": {2547+ "node": ">=0.12.0"2548+ }2549+ },2550+ "node_modules/is-promise": {2551+ "version": "4.0.0",2552+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",2553+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",2554+ "license": "MIT"2555+ },2556+ "node_modules/isexe": {2557+ "version": "2.0.0",2558+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",2559+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",2560+ "license": "ISC"2561+ },2562+ "node_modules/jose": {2563+ "version": "6.2.8",2564+ "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.8.tgz",2565+ "integrity": "sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ==",2566+ "license": "MIT",2567+ "funding": {2568+ "url": "https://github.com/sponsors/panva"2569+ }2570+ },2571+ "node_modules/json-schema-traverse": {2572+ "version": "1.0.0",2573+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",2574+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",2575+ "license": "MIT"2576+ },2577+ "node_modules/json-schema-typed": {2578+ "version": "8.0.2",2579+ "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",2580+ "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",2581+ "license": "BSD-2-Clause"2582+ },2583+ "node_modules/kleur": {2584+ "version": "4.1.5",2585+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",2586+ "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",2587+ "dev": true,2588+ "license": "MIT",2589+ "engines": {2590+ "node": ">=6"2591+ }2592+ },2593+ "node_modules/lightningcss": {2594+ "version": "1.33.0",2595+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",2596+ "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",2597+ "dev": true,2598+ "license": "MPL-2.0",2599+ "dependencies": {2600+ "detect-libc": "^2.0.3"2601+ },2602+ "engines": {2603+ "node": ">= 12.0.0"2604+ },2605+ "funding": {2606+ "type": "opencollective",2607+ "url": "https://opencollective.com/parcel"2608+ },2609+ "optionalDependencies": {2610+ "lightningcss-android-arm64": "1.33.0",2611+ "lightningcss-darwin-arm64": "1.33.0",2612+ "lightningcss-darwin-x64": "1.33.0",2613+ "lightningcss-freebsd-x64": "1.33.0",2614+ "lightningcss-linux-arm-gnueabihf": "1.33.0",2615+ "lightningcss-linux-arm64-gnu": "1.33.0",2616+ "lightningcss-linux-arm64-musl": "1.33.0",2617+ "lightningcss-linux-x64-gnu": "1.33.0",2618+ "lightningcss-linux-x64-musl": "1.33.0",2619+ "lightningcss-win32-arm64-msvc": "1.33.0",2620+ "lightningcss-win32-x64-msvc": "1.33.0"2621+ }2622+ },2623+ "node_modules/lightningcss-android-arm64": {2624+ "version": "1.33.0",2625+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",2626+ "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",2627+ "cpu": [2628+ "arm64"2629+ ],2630+ "dev": true,2631+ "license": "MPL-2.0",2632+ "optional": true,2633+ "os": [2634+ "android"2635+ ],2636+ "engines": {2637+ "node": ">= 12.0.0"2638+ },2639+ "funding": {2640+ "type": "opencollective",2641+ "url": "https://opencollective.com/parcel"2642+ }2643+ },2644+ "node_modules/lightningcss-darwin-arm64": {2645+ "version": "1.33.0",2646+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",2647+ "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",2648+ "cpu": [2649+ "arm64"2650+ ],2651+ "dev": true,2652+ "license": "MPL-2.0",2653+ "optional": true,2654+ "os": [2655+ "darwin"2656+ ],2657+ "engines": {2658+ "node": ">= 12.0.0"2659+ },2660+ "funding": {2661+ "type": "opencollective",2662+ "url": "https://opencollective.com/parcel"2663+ }2664+ },2665+ "node_modules/lightningcss-darwin-x64": {2666+ "version": "1.33.0",2667+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",2668+ "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",2669+ "cpu": [2670+ "x64"2671+ ],2672+ "dev": true,2673+ "license": "MPL-2.0",2674+ "optional": true,2675+ "os": [2676+ "darwin"2677+ ],2678+ "engines": {2679+ "node": ">= 12.0.0"2680+ },2681+ "funding": {2682+ "type": "opencollective",2683+ "url": "https://opencollective.com/parcel"2684+ }2685+ },2686+ "node_modules/lightningcss-freebsd-x64": {2687+ "version": "1.33.0",2688+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",2689+ "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",2690+ "cpu": [2691+ "x64"2692+ ],2693+ "dev": true,2694+ "license": "MPL-2.0",2695+ "optional": true,2696+ "os": [2697+ "freebsd"2698+ ],2699+ "engines": {2700+ "node": ">= 12.0.0"2701+ },2702+ "funding": {2703+ "type": "opencollective",2704+ "url": "https://opencollective.com/parcel"2705+ }2706+ },2707+ "node_modules/lightningcss-linux-arm-gnueabihf": {2708+ "version": "1.33.0",2709+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",2710+ "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",2711+ "cpu": [2712+ "arm"2713+ ],2714+ "dev": true,2715+ "license": "MPL-2.0",2716+ "optional": true,2717+ "os": [2718+ "linux"2719+ ],2720+ "engines": {2721+ "node": ">= 12.0.0"2722+ },2723+ "funding": {2724+ "type": "opencollective",2725+ "url": "https://opencollective.com/parcel"2726+ }2727+ },2728+ "node_modules/lightningcss-linux-arm64-gnu": {2729+ "version": "1.33.0",2730+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",2731+ "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",2732+ "cpu": [2733+ "arm64"2734+ ],2735+ "dev": true,2736+ "license": "MPL-2.0",2737+ "optional": true,2738+ "os": [2739+ "linux"2740+ ],2741+ "engines": {2742+ "node": ">= 12.0.0"2743+ },2744+ "funding": {2745+ "type": "opencollective",2746+ "url": "https://opencollective.com/parcel"2747+ }2748+ },2749+ "node_modules/lightningcss-linux-arm64-musl": {2750+ "version": "1.33.0",2751+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",2752+ "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",2753+ "cpu": [2754+ "arm64"2755+ ],2756+ "dev": true,2757+ "license": "MPL-2.0",2758+ "optional": true,2759+ "os": [2760+ "linux"2761+ ],2762+ "engines": {2763+ "node": ">= 12.0.0"2764+ },2765+ "funding": {2766+ "type": "opencollective",2767+ "url": "https://opencollective.com/parcel"2768+ }2769+ },2770+ "node_modules/lightningcss-linux-x64-gnu": {2771+ "version": "1.33.0",2772+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",2773+ "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",2774+ "cpu": [2775+ "x64"2776+ ],2777+ "dev": true,2778+ "license": "MPL-2.0",2779+ "optional": true,2780+ "os": [2781+ "linux"2782+ ],2783+ "engines": {2784+ "node": ">= 12.0.0"2785+ },2786+ "funding": {2787+ "type": "opencollective",2788+ "url": "https://opencollective.com/parcel"2789+ }2790+ },2791+ "node_modules/lightningcss-linux-x64-musl": {2792+ "version": "1.33.0",2793+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",2794+ "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",2795+ "cpu": [2796+ "x64"2797+ ],2798+ "dev": true,2799+ "license": "MPL-2.0",2800+ "optional": true,2801+ "os": [2802+ "linux"2803+ ],2804+ "engines": {2805+ "node": ">= 12.0.0"2806+ },2807+ "funding": {2808+ "type": "opencollective",2809+ "url": "https://opencollective.com/parcel"2810+ }2811+ },2812+ "node_modules/lightningcss-win32-arm64-msvc": {2813+ "version": "1.33.0",2814+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",2815+ "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",2816+ "cpu": [2817+ "arm64"2818+ ],2819+ "dev": true,2820+ "license": "MPL-2.0",2821+ "optional": true,2822+ "os": [2823+ "win32"2824+ ],2825+ "engines": {2826+ "node": ">= 12.0.0"2827+ },2828+ "funding": {2829+ "type": "opencollective",2830+ "url": "https://opencollective.com/parcel"2831+ }2832+ },2833+ "node_modules/lightningcss-win32-x64-msvc": {2834+ "version": "1.33.0",2835+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",2836+ "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",2837+ "cpu": [2838+ "x64"2839+ ],2840+ "dev": true,2841+ "license": "MPL-2.0",2842+ "optional": true,2843+ "os": [2844+ "win32"2845+ ],2846+ "engines": {2847+ "node": ">= 12.0.0"2848+ },2849+ "funding": {2850+ "type": "opencollective",2851+ "url": "https://opencollective.com/parcel"2852+ }2853+ },2854+ "node_modules/magic-string": {2855+ "version": "0.30.21",2856+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",2857+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",2858+ "dev": true,2859+ "license": "MIT",2860+ "dependencies": {2861+ "@jridgewell/sourcemap-codec": "^1.5.5"2862+ }2863+ },2864+ "node_modules/math-intrinsics": {2865+ "version": "1.1.0",2866+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",2867+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",2868+ "license": "MIT",2869+ "engines": {2870+ "node": ">= 0.4"2871+ }2872+ },2873+ "node_modules/media-typer": {2874+ "version": "1.1.1",2875+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz",2876+ "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==",2877+ "license": "MIT",2878+ "engines": {2879+ "node": ">= 0.8"2880+ },2881+ "funding": {2882+ "type": "opencollective",2883+ "url": "https://opencollective.com/express"2884+ }2885+ },2886+ "node_modules/merge-descriptors": {2887+ "version": "2.0.0",2888+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",2889+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",2890+ "license": "MIT",2891+ "engines": {2892+ "node": ">=18"2893+ },2894+ "funding": {2895+ "url": "https://github.com/sponsors/sindresorhus"2896+ }2897+ },2898+ "node_modules/micromatch": {2899+ "version": "4.0.8",2900+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",2901+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",2902+ "dev": true,2903+ "license": "MIT",2904+ "dependencies": {2905+ "braces": "^3.0.3",2906+ "picomatch": "^2.3.1"2907+ },2908+ "engines": {2909+ "node": ">=8.6"2910+ }2911+ },2912+ "node_modules/micromatch/node_modules/picomatch": {2913+ "version": "2.3.2",2914+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",2915+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",2916+ "dev": true,2917+ "license": "MIT",2918+ "engines": {2919+ "node": ">=8.6"2920+ },2921+ "funding": {2922+ "url": "https://github.com/sponsors/jonschlinkert"2923+ }2924+ },2925+ "node_modules/mime-db": {2926+ "version": "1.54.0",2927+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",2928+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",2929+ "license": "MIT",2930+ "engines": {2931+ "node": ">= 0.6"2932+ }2933+ },2934+ "node_modules/mime-types": {2935+ "version": "3.0.2",2936+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",2937+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",2938+ "license": "MIT",2939+ "dependencies": {2940+ "mime-db": "^1.54.0"2941+ },2942+ "engines": {2943+ "node": ">=18"2944+ },2945+ "funding": {2946+ "type": "opencollective",2947+ "url": "https://opencollective.com/express"2948+ }2949+ },2950+ "node_modules/miniflare": {2951+ "version": "5.20260811.0-alpha",2952+ "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20260811.0-alpha.tgz",2953+ "integrity": "sha512-sypXsD5fjY88fZNedPqnwrwR1dwfnfbfW7MfvMyIfPJdtRiCCOpUnjWGeFVYYZ+0fQVICye6Juu+vZgzTEx8XA==",2954+ "dev": true,2955+ "license": "MIT",2956+ "dependencies": {2957+ "@cspotcode/source-map-support": "0.8.1",2958+ "sharp": "0.35.2",2959+ "undici": "7.29.0",2960+ "workerd": "1.20260811.1",2961+ "ws": "8.21.0",2962+ "youch": "4.1.0-beta.10"2963+ },2964+ "engines": {2965+ "node": ">=22.0.0"2966+ }2967+ },2968+ "node_modules/ms": {2969+ "version": "2.1.3",2970+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",2971+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",2972+ "license": "MIT"2973+ },2974+ "node_modules/nanoid": {2975+ "version": "3.3.18",2976+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",2977+ "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",2978+ "dev": true,2979+ "funding": [2980+ {2981+ "type": "github",2982+ "url": "https://github.com/sponsors/ai"2983+ }2984+ ],2985+ "license": "MIT",2986+ "bin": {2987+ "nanoid": "bin/nanoid.cjs"2988+ },2989+ "engines": {2990+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"2991+ }2992+ },2993+ "node_modules/negotiator": {2994+ "version": "1.0.0",2995+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",2996+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",2997+ "license": "MIT",2998+ "engines": {2999+ "node": ">= 0.6"3000+ }diff truncated
@@ -0,0 +1,36 @@
1+{2+ "name": "mcpsweeper",3+ "version": "0.1.0",4+ "private": true,5+ "type": "module",6+ "scripts": {7+ "build:view": "vite build",8+ "build": "npm run build:view && npm run typecheck",9+ "dev": "npm run build:view && wrangler dev --port 8787 --persist-to .wrangler/state",10+ "types": "wrangler types",11+ "typecheck": "tsc --noEmit -p tsconfig.worker.json && tsc --noEmit -p tsconfig.view.json",12+ "test": "vitest run",13+ "smoke": "node scripts/smoke.mjs",14+ "solve": "node scripts/solve.mjs",15+ "db:migrate:local": "wrangler d1 migrations apply mcpsweeper-db --local --persist-to .wrangler/state"16+ },17+ "dependencies": {18+ "@modelcontextprotocol/ext-apps": "^1.7.5",19+ "@modelcontextprotocol/hono": "^2.0.0",20+ "@modelcontextprotocol/sdk": "^1.29.0",21+ "@modelcontextprotocol/server": "^2.0.0",22+ "98.css": "^0.1.21",23+ "hono": "^4.13.1",24+ "zod": "^4.2.0"25+ },26+ "devDependencies": {27+ "@cloudflare/vitest-pool-workers": "latest",28+ "@types/node": "latest",29+ "playwright": "^1.62.1",30+ "typescript": "5.9.3",31+ "vite": "latest",32+ "vite-plugin-singlefile": "latest",33+ "vitest": "latest",34+ "wrangler": "^4.122.0"35+ }36+}@@ -0,0 +1,40 @@
1+Original prompt: I like it; lets iterate in `~/Developer/mcpsweeper` — make an initial version and load it into codex so we can test here2+3+## Progress4+5+- Chosen architecture: stateless Cloudflare Worker MCP endpoint, explicit `game_id` handles, D1 persistence, and a read-only MCP App spectator view.6+- Implemented the initial game engine, optimistic-revision D1 repository, four MCP tools, and the canvas spectator UI with preview mode.7+- Switched the spectator surface to 98.css and authentic classic Minesweeper chrome while retaining a canvas field for 64x64 performance.8+- Updated the Cloudflare Vitest integration; the engine tests pass.9+- Benchmarked a certified 64x64 board (409 mines) at 15 ms locally, so game creation can stay on demand.10+- Benchmarked scaling: 224x224 stayed below 750 ms across 48 default-density seeds; 256x256 showed a 2.7 s outlier, so the initial limit is 224.11+- Added an exact replay data model (initial opening plus per-batch clue values) and a 98.css timeline/intent log spectator control.12+- Validated the full MCP/D1 loop at both 64x64 and the 224x224 limit; a maximum board solved in 19 batches with complete replay history.13+- Registered `mcpsweeper` in Codex at `http://127.0.0.1:8787/mcp` and opened the live local surface in the task.14+- Diagnosed a live App crash: model-facing observations and app-facing visual snapshots shared one field, and malformed/legacy event data could terminate polling.15+- Split agent positions from visual observations. Agent positions now use delta-coded frontier/clue/flag cell IDs; the app watcher alone receives full rows.16+- Constrained the MCP App to the host viewport so zoom does not grow the iframe; only the board viewport pans. Replaced iframe fullscreen with a Fit board control.17+- Widened the UI into a board + right inspector layout. Zoom, batch scrubber, replay controls, and intent history now sit beside the puzzle.18+- Added automatic before/after comparison for the selected batch and an updates-count column in batch history.19+- Verified the 1000x720 layout has no document/body overflow: board viewport 705x535 and right inspector 258x565. Preview comparison alternated AFTER -> BEFORE with no browser errors.20+- Simplified the shell back to classic Minesweeper hierarchy (Game/Help, counters/face, board) with one docked batch-history pane; removed dashboard-like UI chrome around zoom.21+- Prevented dev UI rebuilds from deleting the generated App out from under Wrangler (`emptyOutDir: false`).22+- Replaced tiny canvas font rendering with connected 5x7 pixel clue glyphs so numerals do not break apart when zoomed.23+- Removed the narrow-width stacking breakpoint after Codex triggered it in a physically wide panel; game/history now remain left/right at every host width.24+- Disabled circular auto-resize and explicitly request a 640px inline App height from the MCP host. Do not clamp this request to the host's reported `maxHeight`; Codex reports its current short card there, which turns the resize into a no-op.25+- Large-board mine counters now expand to five digits instead of clipping values above 999.26+- Added a board stage that centers the canvas whenever it is smaller than the viewport while remaining scrollable and center-anchored at larger zoom levels.27+- Replaced fixed 768px bitmap stretching with device-pixel-aware canvas rerasterization on zoom/resize, capped at a 4096px backing store. Added overview, compact, and detailed semantic cell rendering plus unchanged-frame paint caching; `/preview?size=200` now exercises large-board scaling directly.28+- Cached the reconstructed historical observation used by automatic BEFORE/AFTER comparison so 200x200 boards remain responsive instead of rebuilding all 40,000 cells every animation frame.29+- Browser-verified 200x200 rendering at 2x device scale: 2.58px overview at fit, 4.53px compact at 175%, and 7.75px detailed at 300%. Fit, comparison toggle, and replay advancement all worked with no console errors; build/typecheck and all 5 engine tests pass.30+- Added a daily 04:17 UTC retention cron that deletes games inactive for more than 14 days by indexed `updated_at`; D1 cascades each deletion to its moves. An integration test preserves the exact cutoff boundary and recent games while verifying the expired move is removed.31+- Verified replay share URLs are self-contained gzip/base64url fragments: a fresh page restored revision 7 and replayed to revision 1 with only `GET /preview` and no MCP/D1 requests. The static preview deployment must remain available and version-1 fragment decoding must remain compatible.32+- Replaced the root preview with a white, centered landing page containing a copyable agent prompt for installing the deployed MCP endpoint and autonomously playing a 20x20 game. The spectator remains at `/preview` and in the MCP App.33+- Browser-checked the landing page at 1280x720 (centered prompt, white background, no scripts or console errors) and reran the preview game screenshot/state regression.34+- Provisioned and migrated `mcpsweeper-agent-game-db`, deployed Worker version `95dd8900-974a-469e-a007-e19b4bec5d90` at `https://mcpsweeper-agent-game.felinestatemachine.workers.dev`, and verified the live MCP endpoint by starting a 20x20 game.35+- Deployed retention and self-contained replay-link changes as Worker version `483d3852-562d-460d-bf75-8cb9a9a9eb95`; Wrangler confirmed schedule `17 4 * * *`, and the production health endpoint passed.36+37+## TODO38+39+- Verify subsequent live batches advance in the MCP App after the user starts a fresh game.40+- Keep the deployed Worker and D1 names aligned (`mcpsweeper-agent-game` and `mcpsweeper-agent-game-db`).@@ -0,0 +1,6 @@
1+const projectPlaywrightUrl = new URL("../node_modules/playwright/index.mjs", import.meta.url).href;2+3+export async function resolve(specifier, context, nextResolve) {4+ if (specifier === "playwright") return { url: projectPlaywrightUrl, shortCircuit: true };5+ return nextResolve(specifier, context);6+}@@ -0,0 +1,36 @@
1+import { Client } from "@modelcontextprotocol/sdk/client/index.js";2+import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";3+4+const url = new URL(process.env.MCPSWEEPER_URL ?? "http://127.0.0.1:8787/mcp");5+const client = new Client({ name: "mcpsweeper-smoke", version: "0.1.0" });6+7+try {8+ await client.connect(new StreamableHTTPClientTransport(url));9+ const tools = await client.listTools();10+ const resources = await client.listResources();11+ const started = await client.callTool({12+ name: "start_game",13+ arguments: { size: 16, density: 0.1, seed: "smoke-test" },14+ });15+ const gameId = started.structuredContent?.game_id;16+ if (typeof gameId !== "string") throw new Error("start_game did not return a game_id");17+18+ const observed = await client.callTool({ name: "observe_game", arguments: { game_id: gameId } });19+ const app = await client.readResource({ uri: "ui://mcpsweeper/spectator.html" });20+21+ console.log(22+ JSON.stringify(23+ {24+ tools: tools.tools.map((tool) => tool.name),25+ resources: resources.resources.map((resource) => resource.uri),26+ game_id: gameId,27+ board_size: observed.structuredContent?.position?.width,28+ app_html_bytes: app.contents[0]?.text?.length ?? 0,29+ },30+ null,31+ 2,32+ ),33+ );34+} finally {35+ await client.close();36+}@@ -0,0 +1,138 @@
1+import { Client } from "@modelcontextprotocol/sdk/client/index.js";2+import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";3+4+const url = new URL(process.env.MCPSWEEPER_URL ?? "http://127.0.0.1:8787/mcp");5+const size = Number(process.env.MCPSWEEPER_SIZE ?? 64);6+const seed = process.env.MCPSWEEPER_SEED ?? "agent-speed-run";7+const delayMs = Number(process.env.MCPSWEEPER_DELAY_MS ?? 0);8+const client = new Client({ name: "mcpsweeper-solver", version: "0.1.0" });9+10+function deduce(position) {11+ const decodeDeltas = (deltas) => {12+ let cell = 0;13+ return deltas.map((delta) => (cell += delta));14+ };15+ const frontier = decodeDeltas(position.frontier_deltas);16+ const frontierSet = new Set(frontier);17+ const flagged = new Set(decodeDeltas(position.flagged_deltas));18+ const clues = [];19+ let clueCell = 0;20+ for (const code of position.clue_deltas) {21+ clueCell += Math.floor(code / 9);22+ clues.push({ cell: clueCell, clue: code % 9 });23+ }24+ const safe = new Set();25+ const mines = new Set();26+ for (const { cell, clue } of clues) {27+ const row = Math.floor(cell / position.width);28+ const col = cell % position.width;29+ const cells = [];30+ let adjacentFlags = 0;31+ for (let rowOffset = -1; rowOffset <= 1; rowOffset += 1) {32+ for (let colOffset = -1; colOffset <= 1; colOffset += 1) {33+ if (rowOffset === 0 && colOffset === 0) continue;34+ const nextRow = row + rowOffset;35+ const nextCol = col + colOffset;36+ if (nextRow < 0 || nextRow >= position.height || nextCol < 0 || nextCol >= position.width) continue;37+ const neighbor = nextRow * position.width + nextCol;38+ if (frontierSet.has(neighbor)) cells.push(neighbor);39+ if (flagged.has(neighbor)) adjacentFlags += 1;40+ }41+ }42+ const remaining = clue - adjacentFlags;43+ if (remaining === 0) cells.forEach((cell) => safe.add(cell));44+ if (cells.length > 0 && remaining === cells.length) cells.forEach((cell) => mines.add(cell));45+ }46+ for (const mine of mines) safe.delete(mine);47+ return { reveal: [...safe], flag: [...mines] };48+}49+50+function positionFrom(result) {51+ const position = result.structuredContent?.position;52+ if (!position || !Array.isArray(position.frontier_deltas) || !Array.isArray(position.clue_deltas)) {53+ throw new Error("Tool result did not contain a compact position");54+ }55+ return position;56+}57+58+await client.connect(new StreamableHTTPClientTransport(url));59+const startedAt = performance.now();60+61+try {62+ const started = await client.callTool({ name: "start_game", arguments: { size, density: 0.1, seed } });63+ const gameId = started.structuredContent?.game_id;64+ if (typeof gameId !== "string") throw new Error("start_game did not return a game_id");65+ let position = started.structuredContent.position;66+ let totalRevealed = 0;67+ let totalFlagged = 0;68+ let visualRevision = 0;69+ let initialObservation = null;70+ let positionChars = JSON.stringify(position).length;71+ let visualChars = 0;72+73+ while (position.status === "playing") {74+ const action = deduce(position);75+ if (action.reveal.length === 0 && action.flag.length === 0) {76+ throw new Error(`No deterministic move at revision ${position.revision}`);77+ }78+ const result = await client.callTool({79+ name: "play_batch",80+ arguments: {81+ game_id: gameId,82+ expected_revision: position.revision,83+ reveal_cells: action.reveal.slice(0, 8192),84+ flag_cells: action.flag.slice(0, 8192),85+ intent: `Frontier deduction: ${Math.min(action.reveal.length, 8192)} safe, ${Math.min(action.flag.length, 8192)} mines`,86+ },87+ });88+ if (result.isError) throw new Error(result.content?.[0]?.text ?? "play_batch failed");89+ totalRevealed += Math.min(action.reveal.length, 8192);90+ totalFlagged += Math.min(action.flag.length, 8192);91+ position = positionFrom(result);92+ positionChars += JSON.stringify(position).length;93+ const watchedBatch = await client.callTool({94+ name: "watch_game",95+ arguments: { game_id: gameId, since_revision: visualRevision },96+ });97+ const visual = watchedBatch.structuredContent?.observation;98+ if (!visual || !Array.isArray(visual.rows) || visual.revision !== position.revision) {99+ throw new Error(`Visual watcher did not advance to revision ${position.revision}`);100+ }101+ if (visualRevision === 0) initialObservation = watchedBatch.structuredContent?.initial_observation;102+ visualRevision = visual.revision;103+ visualChars += JSON.stringify(visual).length;104+ if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs));105+ }106+107+ const watched = await client.callTool({108+ name: "watch_game",109+ arguments: { game_id: gameId, since_revision: 0 },110+ });111+ const replayEvents = watched.structuredContent?.events;112+ if (!Array.isArray(replayEvents) || !initialObservation) throw new Error("Replay history was not returned");113+ if (replayEvents.length !== position.revision) throw new Error("Replay history is incomplete");114+115+ console.log(116+ JSON.stringify(117+ {118+ game_id: gameId,119+ status: position.status,120+ size: `${position.width}x${position.height}`,121+ mines: position.mine_count,122+ batches: position.revision,123+ submitted_safe_moves: totalRevealed,124+ submitted_flags: totalFlagged,125+ elapsed_ms: Math.round(performance.now() - startedAt),126+ replay_events: replayEvents.length,127+ initial_revision: initialObservation.revision,128+ agent_position_chars: positionChars,129+ visual_snapshot_chars: visualChars,130+ agent_payload_reduction_percent: Math.round((1 - positionChars / visualChars) * 100),131+ },132+ null,133+ 2,134+ ),135+ );136+} finally {137+ await client.close();138+}@@ -0,0 +1,484 @@
1+export type GameStatus = "playing" | "won" | "lost";2+3+export type Coordinate = {4+ row: number;5+ col: number;6+};7+8+export type GameState = {9+ id: string;10+ seed: string;11+ width: number;12+ height: number;13+ mines: Uint8Array;14+ revealed: Uint8Array;15+ initialRevealed: Uint8Array;16+ flags: Uint8Array;17+ status: GameStatus;18+ revision: number;19+ explodedIndex: number | null;20+ certifiedNoGuess: boolean;21+ createdAt: string;22+ updatedAt: string;23+};24+25+export type BatchAction = {26+ reveal: Coordinate[];27+ flag: Coordinate[];28+ unflag: Coordinate[];29+ intent: string;30+};31+32+export type MoveEvent = {33+ revision: number;34+ intent: string;35+ submittedCount: number;36+ revealed: Coordinate[];37+ revealedValues: Array<Coordinate & { value: string }>;38+ flagged: Coordinate[];39+ unflagged: Coordinate[];40+ exploded: Coordinate | null;41+ status: GameStatus;42+ createdAt: string;43+};44+45+export type Observation = {46+ game_id: string;47+ revision: number;48+ status: GameStatus;49+ width: number;50+ height: number;51+ mine_count: number;52+ revealed_count: number;53+ flagged_count: number;54+ remaining_safe_cells: number;55+ certified_no_guess: boolean;56+ coordinate_system: string;57+ legend: Record<string, string>;58+ rows: string[];59+};60+61+export type AgentPosition = {62+ game_id: string;63+ revision: number;64+ status: GameStatus;65+ width: number;66+ height: number;67+ mine_count: number;68+ flagged_count: number;69+ remaining_safe_cells: number;70+ certified_no_guess: boolean;71+ encoding: {72+ cell: string;73+ deltas: string;74+ clue_code: string;75+ };76+ frontier_deltas: number[];77+ clue_deltas: number[];78+ flagged_deltas: number[];79+ exploded_cell?: number;80+};81+82+const NEIGHBOR_OFFSETS = [83+ [-1, -1],84+ [-1, 0],85+ [-1, 1],86+ [0, -1],87+ [0, 1],88+ [1, -1],89+ [1, 0],90+ [1, 1],91+] as const;92+93+function hashSeed(seed: string): number {94+ let hash = 2166136261;95+ for (let index = 0; index < seed.length; index += 1) {96+ hash ^= seed.charCodeAt(index);97+ hash = Math.imul(hash, 16777619);98+ }99+ return hash >>> 0 || 0x9e3779b9;100+}101+102+function createRandom(seed: string): () => number {103+ let state = hashSeed(seed);104+ return () => {105+ state ^= state << 13;106+ state ^= state >>> 17;107+ state ^= state << 5;108+ return (state >>> 0) / 0x1_0000_0000;109+ };110+}111+112+export function indexOf(width: number, coordinate: Coordinate): number {113+ return coordinate.row * width + coordinate.col;114+}115+116+export function coordinateOf(width: number, index: number): Coordinate {117+ return { row: Math.floor(index / width), col: index % width };118+}119+120+function neighbors(width: number, height: number, index: number): number[] {121+ const { row, col } = coordinateOf(width, index);122+ const values: number[] = [];123+ for (const [rowOffset, colOffset] of NEIGHBOR_OFFSETS) {124+ const nextRow = row + rowOffset;125+ const nextCol = col + colOffset;126+ if (nextRow >= 0 && nextRow < height && nextCol >= 0 && nextCol < width) {127+ values.push(nextRow * width + nextCol);128+ }129+ }130+ return values;131+}132+133+export function adjacentMineCount(state: Pick<GameState, "width" | "height" | "mines">, index: number): number {134+ return neighbors(state.width, state.height, index).reduce(135+ (total, neighbor) => total + (state.mines[neighbor] ?? 0),136+ 0,137+ );138+}139+140+function revealFlood(state: GameState, startingIndexes: Iterable<number>): number[] {141+ const queue = [...startingIndexes];142+ const newlyRevealed: number[] = [];143+ const queued = new Uint8Array(state.width * state.height);144+ for (const index of queue) queued[index] = 1;145+146+ for (let cursor = 0; cursor < queue.length; cursor += 1) {147+ const index = queue[cursor];148+ if (index === undefined || state.revealed[index] === 1 || state.flags[index] === 1 || state.mines[index] === 1) {149+ continue;150+ }151+ state.revealed[index] = 1;152+ newlyRevealed.push(index);153+ if (adjacentMineCount(state, index) !== 0) continue;154+ for (const neighbor of neighbors(state.width, state.height, index)) {155+ if (queued[neighbor] === 0) {156+ queued[neighbor] = 1;157+ queue.push(neighbor);158+ }159+ }160+ }161+ return newlyRevealed;162+}163+164+function countBits(values: Uint8Array): number {165+ let total = 0;166+ for (const value of values) total += value;167+ return total;168+}169+170+function validateCoordinates(state: GameState, coordinates: Coordinate[]): void {171+ if (coordinates.length > 8192) throw new Error("A batch may contain at most 8192 cells per action type.");172+ for (const coordinate of coordinates) {173+ if (!Number.isInteger(coordinate.row) || !Number.isInteger(coordinate.col)) {174+ throw new Error("Coordinates must use integer row and col values.");175+ }176+ if (coordinate.row < 0 || coordinate.row >= state.height || coordinate.col < 0 || coordinate.col >= state.width) {177+ throw new Error(`Coordinate (${coordinate.row}, ${coordinate.col}) is outside the board.`);178+ }179+ }180+}181+182+function uniqueIndexes(state: GameState, coordinates: Coordinate[]): number[] {183+ return [...new Set(coordinates.map((coordinate) => indexOf(state.width, coordinate)))];184+}185+186+export function countSubmittedTiles(action: BatchAction): number {187+ return new Set(188+ [...action.reveal, ...action.flag, ...action.unflag].map((coordinate) => `${coordinate.row}:${coordinate.col}`),189+ ).size;190+}191+192+export function applyBatch(state: GameState, action: BatchAction, now = new Date().toISOString()): MoveEvent {193+ if (state.status !== "playing") throw new Error(`Game is already ${state.status}.`);194+ validateCoordinates(state, action.reveal);195+ validateCoordinates(state, action.flag);196+ validateCoordinates(state, action.unflag);197+198+ const revealIndexes = uniqueIndexes(state, action.reveal);199+ const flagIndexes = uniqueIndexes(state, action.flag);200+ const unflagIndexes = uniqueIndexes(state, action.unflag);201+ const flagged: number[] = [];202+ const unflagged: number[] = [];203+ const revealed: number[] = [];204+ let explodedIndex: number | null = null;205+206+ for (const index of unflagIndexes) {207+ if (state.flags[index] === 1) {208+ state.flags[index] = 0;209+ unflagged.push(index);210+ }211+ }212+ for (const index of flagIndexes) {213+ if (state.revealed[index] === 0 && state.flags[index] === 0) {214+ state.flags[index] = 1;215+ flagged.push(index);216+ }217+ }218+ for (const index of revealIndexes) {219+ if (state.flags[index] === 1 || state.revealed[index] === 1) continue;220+ if (state.mines[index] === 1) {221+ explodedIndex = index;222+ state.explodedIndex = index;223+ state.status = "lost";224+ break;225+ }226+ revealed.push(...revealFlood(state, [index]));227+ }228+229+ if (state.status === "playing") {230+ const safeCells = state.width * state.height - countBits(state.mines);231+ if (countBits(state.revealed) === safeCells) state.status = "won";232+ }233+ state.revision += 1;234+ state.updatedAt = now;235+236+ return {237+ revision: state.revision,238+ intent: action.intent,239+ submittedCount: countSubmittedTiles(action),240+ revealed: revealed.map((index) => coordinateOf(state.width, index)),241+ revealedValues: revealed.map((index) => ({242+ ...coordinateOf(state.width, index),243+ value: String(adjacentMineCount(state, index)),244+ })),245+ flagged: flagged.map((index) => coordinateOf(state.width, index)),246+ unflagged: unflagged.map((index) => coordinateOf(state.width, index)),247+ exploded: explodedIndex === null ? null : coordinateOf(state.width, explodedIndex),248+ status: state.status,249+ createdAt: now,250+ };251+}252+253+function makeCandidate(width: number, height: number, density: number, seed: string, attempt: number): GameState {254+ const total = width * height;255+ const mines = new Uint8Array(total);256+ const random = createRandom(`${seed}:${attempt}`);257+ const start = { row: Math.floor(height / 2), col: Math.floor(width / 2) };258+ const protectedIndexes = new Set<number>();259+ for (let row = start.row - 1; row <= start.row + 1; row += 1) {260+ for (let col = start.col - 1; col <= start.col + 1; col += 1) {261+ if (row >= 0 && row < height && col >= 0 && col < width) protectedIndexes.add(row * width + col);262+ }263+ }264+ const candidates = Array.from({ length: total }, (_, index) => index).filter((index) => !protectedIndexes.has(index));265+ for (let index = candidates.length - 1; index > 0; index -= 1) {266+ const swapIndex = Math.floor(random() * (index + 1));267+ const current = candidates[index];268+ const swap = candidates[swapIndex];269+ if (current !== undefined && swap !== undefined) {270+ candidates[index] = swap;271+ candidates[swapIndex] = current;272+ }273+ }274+ const mineCount = Math.floor(total * density);275+ for (let index = 0; index < mineCount; index += 1) {276+ const mineIndex = candidates[index];277+ if (mineIndex !== undefined) mines[mineIndex] = 1;278+ }279+ const now = new Date().toISOString();280+ const state: GameState = {281+ id: "",282+ seed,283+ width,284+ height,285+ mines,286+ revealed: new Uint8Array(total),287+ initialRevealed: new Uint8Array(total),288+ flags: new Uint8Array(total),289+ status: "playing",290+ revision: 0,291+ explodedIndex: null,292+ certifiedNoGuess: false,293+ createdAt: now,294+ updatedAt: now,295+ };296+ revealFlood(state, [indexOf(width, start)]);297+ state.initialRevealed = state.revealed.slice();298+ return state;299+}300+301+function solveByVisibleDeductions(original: GameState): boolean {302+ const state: GameState = {303+ ...original,304+ mines: original.mines.slice(),305+ revealed: original.revealed.slice(),306+ flags: new Uint8Array(original.flags.length),307+ };308+ const totalSafe = state.width * state.height - countBits(state.mines);309+310+ for (let round = 0; round < state.width * state.height; round += 1) {311+ const safe = new Set<number>();312+ const mines = new Set<number>();313+ for (let index = 0; index < state.revealed.length; index += 1) {314+ if (state.revealed[index] !== 1) continue;315+ const adjacent = neighbors(state.width, state.height, index);316+ const hidden = adjacent.filter((neighbor) => state.revealed[neighbor] === 0 && state.flags[neighbor] === 0);317+ const flagged = adjacent.reduce((total, neighbor) => total + (state.flags[neighbor] ?? 0), 0);318+ const remaining = adjacentMineCount(state, index) - flagged;319+ if (hidden.length > 0 && remaining === 0) hidden.forEach((neighbor) => safe.add(neighbor));320+ if (hidden.length > 0 && remaining === hidden.length) hidden.forEach((neighbor) => mines.add(neighbor));321+ }322+ if (safe.size === 0 && mines.size === 0) break;323+ for (const index of mines) state.flags[index] = 1;324+ revealFlood(state, safe);325+ if (countBits(state.revealed) === totalSafe) return true;326+ }327+ return countBits(state.revealed) === totalSafe;328+}329+330+export function createNoGuessGame(options: {331+ id: string;332+ seed: string;333+ width: number;334+ height: number;335+ density: number;336+ maxAttempts?: number;337+}): GameState {338+ const maxAttempts = options.maxAttempts ?? 200;339+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {340+ const state = makeCandidate(options.width, options.height, options.density, options.seed, attempt);341+ if (solveByVisibleDeductions(state)) {342+ state.id = options.id;343+ state.certifiedNoGuess = true;344+ return state;345+ }346+ }347+ throw new Error(`Unable to generate a no-guess ${options.width}x${options.height} board for this seed.`);348+}349+350+export function toObservation(state: GameState, revealMines = false): Observation {351+ const rows: string[] = [];352+ for (let row = 0; row < state.height; row += 1) {353+ let value = "";354+ for (let col = 0; col < state.width; col += 1) {355+ const index = row * state.width + col;356+ if (state.explodedIndex === index) value += "X";357+ else if (state.flags[index] === 1) value += "F";358+ else if (state.revealed[index] === 1) value += String(adjacentMineCount(state, index));359+ else if (revealMines && state.mines[index] === 1) value += "*";360+ else value += "#";361+ }362+ rows.push(value);363+ }364+ const mineCount = countBits(state.mines);365+ const revealedCount = countBits(state.revealed);366+ return {367+ game_id: state.id,368+ revision: state.revision,369+ status: state.status,370+ width: state.width,371+ height: state.height,372+ mine_count: mineCount,373+ revealed_count: revealedCount,374+ flagged_count: countBits(state.flags),375+ remaining_safe_cells: state.width * state.height - mineCount - revealedCount,376+ certified_no_guess: state.certifiedNoGuess,377+ coordinate_system: "Zero-based rows increase downward; zero-based columns increase rightward.",378+ legend: {379+ "#": "hidden",380+ F: "flagged",381+ "0-8": "revealed clue",382+ X: "exploded mine",383+ "*": "mine revealed after loss",384+ },385+ rows,386+ };387+}388+389+export function toInitialObservation(state: GameState): Observation {390+ return toObservation({391+ ...state,392+ revealed: state.initialRevealed,393+ flags: new Uint8Array(state.flags.length),394+ status: "playing",395+ revision: 0,396+ explodedIndex: null,397+ });398+}399+400+export function toAgentPosition(state: GameState): AgentPosition {401+ const frontier = new Set<number>();402+ const clues: Array<{ cell: number; clue: number }> = [];403+404+ for (let index = 0; index < state.revealed.length; index += 1) {405+ if (state.revealed[index] !== 1) continue;406+ const adjacent = neighbors(state.width, state.height, index);407+ const hidden = adjacent408+ .filter((neighbor) => state.revealed[neighbor] === 0 && state.flags[neighbor] === 0)409+ .sort((left, right) => left - right);410+ if (hidden.length === 0) continue;411+ hidden.forEach((cell) => frontier.add(cell));412+ clues.push({ cell: index, clue: adjacentMineCount(state, index) });413+ }414+415+ const frontierCells = [...frontier].sort((left, right) => left - right);416+ const flaggedCells: number[] = [];417+ for (let index = 0; index < state.flags.length; index += 1) {418+ if (state.flags[index] === 1) flaggedCells.push(index);419+ }420+ const observation = toObservation(state);421+ const deltaEncode = (cells: number[]): number[] => {422+ let previous = 0;423+ return cells.map((cell) => {424+ const delta = cell - previous;425+ previous = cell;426+ return delta;427+ });428+ };429+ let previousClueCell = 0;430+ const clueDeltas = clues431+ .sort((left, right) => left.cell - right.cell)432+ .map(({ cell, clue }) => {433+ const code = (cell - previousClueCell) * 9 + clue;434+ previousClueCell = cell;435+ return code;436+ });437+ return {438+ game_id: state.id,439+ revision: state.revision,440+ status: state.status,441+ width: state.width,442+ height: state.height,443+ mine_count: observation.mine_count,444+ flagged_count: observation.flagged_count,445+ remaining_safe_cells: observation.remaining_safe_cells,446+ certified_no_guess: state.certifiedNoGuess,447+ encoding: {448+ cell: "cell = row * width + col; row = floor(cell / width); col = cell % width",449+ deltas: "decode each *_deltas array by cumulative sum to recover sorted absolute cell IDs",450+ clue_code: "for each clue_deltas code: cell += floor(code / 9), clue = code % 9",451+ },452+ frontier_deltas: deltaEncode(frontierCells),453+ clue_deltas: clueDeltas,454+ flagged_deltas: deltaEncode(flaggedCells),455+ ...(state.explodedIndex === null ? {} : { exploded_cell: state.explodedIndex }),456+ };457+}458+459+export function cellsToCoordinates(state: Pick<GameState, "width" | "height">, cells: number[]): Coordinate[] {460+ const total = state.width * state.height;461+ return cells.map((cell) => {462+ if (!Number.isInteger(cell) || cell < 0 || cell >= total) {463+ throw new Error(`Cell ${cell} is outside the board.`);464+ }465+ return coordinateOf(state.width, cell);466+ });467+}468+469+export function encodeBits(values: Uint8Array): string {470+ let binary = "";471+ for (const value of values) binary += String.fromCharCode(value);472+ return btoa(binary);473+}474+475+export function decodeBits(value: string): Uint8Array {476+ const binary = atob(value);477+ const values = new Uint8Array(binary.length);478+ for (let index = 0; index < binary.length; index += 1) values[index] = binary.charCodeAt(index);479+ return values;480+}481+482+export function countMines(state: GameState): number {483+ return countBits(state.mines);484+}@@ -0,0 +1,118 @@
1+import { createMcpHandler } from "@modelcontextprotocol/server";2+import viewHtml from "../dist/mcp-app.html";3+import { deleteGamesInactiveBefore } from "./repository.ts";4+import { createServer } from "./server.ts";5+6+const GAME_RETENTION_MS = 14 * 24 * 60 * 60 * 1_000;7+8+function jsonError(message: string, status: number): Response {9+ return Response.json({ error: message }, { status });10+}11+12+function escapeHtml(value: string): string {13+ return value14+ .replaceAll("&", "&")15+ .replaceAll("<", "<")16+ .replaceAll(">", ">")17+ .replaceAll('"', """)18+ .replaceAll("'", "'");19+}20+21+function landingPage(request: Request): string {22+ const mcpUrl = new URL("/mcp", request.url).toString();23+24+ return `<!doctype html>25+<html lang="en">26+ <head>27+ <meta charset="UTF-8" />28+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />29+ <title>MCPSweeper</title>30+ </head>31+ <body>32+ <main>33+ <h1>MCPSweeper</h1>34+ <p>MCPSweeper is a reference implementation and evaluation environment for the July 28, 2026 Model Context Protocol specification and the MCP Apps specification.</p>35+36+ <section>37+ <h2>July 2026 protocol</h2>38+ <p>It serves a no-guess Minesweeper game through the new stateless protocol core, with cacheable discovery for its app resource.</p>39+ </section>40+41+ <section>42+ <h2>MCP Apps</h2>43+ <p>The spectator is delivered as a sandboxed app attached to the game tools. It displays and replays the model's actions without exposing hidden mines during play.</p>44+ </section>45+46+ <section>47+ <h2>Evaluation tool</h2>48+ <p>Use the game to evaluate constraint solving, tool use, state tracking, and long task reliability across a model and its harness.</p>49+ <p>Suggested prompt:</p>50+ <blockquote>Install the MCPSweeper MCP server at <code>${escapeHtml(mcpUrl)}</code>, start a 20×20 game, and play autonomously until you win.</blockquote>51+ </section>52+ </main>53+ </body>54+</html>`;55+}56+57+function isAllowedLocalHost(request: Request): boolean {58+ const hostname = new URL(request.url).hostname;59+ if (hostname !== "localhost" && hostname !== "127.0.0.1" && hostname !== "[::1]") return true;60+ const host = request.headers.get("Host")?.split(":")[0];61+ return host === "localhost" || host === "127.0.0.1" || host === "[::1]";62+}63+64+async function handleMcp(request: Request, env: Env): Promise<Response> {65+ if (!isAllowedLocalHost(request)) return jsonError("Invalid Host header.", 403);66+ const previewUrl = new URL("/preview", request.url).toString();67+ const handler = createMcpHandler(() => createServer(env, viewHtml, previewUrl), {68+ legacy: "stateless",69+ responseMode: "json",70+ onerror(error) {71+ console.error(JSON.stringify({ message: "MCP request failed", error: error.message }));72+ },73+ });74+ return handler.fetch(request);75+}76+77+export default {78+ async fetch(request: Request, env: Env): Promise<Response> {79+ try {80+ const url = new URL(request.url);81+ if (url.pathname === "/mcp") return await handleMcp(request, env);82+ if (request.method === "GET" && url.pathname === "/health") {83+ return Response.json({ ok: true, service: "mcpsweeper" });84+ }85+ if (request.method === "GET" && url.pathname === "/") {86+ return new Response(landingPage(request), {87+ headers: {88+ "Content-Type": "text/html; charset=utf-8",89+ "Cache-Control": "no-store",90+ "Content-Security-Policy": "default-src 'none'; base-uri 'none'; frame-ancestors 'none'",91+ "X-Content-Type-Options": "nosniff",92+ "Referrer-Policy": "no-referrer",93+ },94+ });95+ }96+ if (request.method === "GET" && url.pathname === "/preview") {97+ return new Response(viewHtml, {98+ headers: {99+ "Content-Type": "text/html; charset=utf-8",100+ "Cache-Control": "no-store",101+ "X-Content-Type-Options": "nosniff",102+ "Referrer-Policy": "no-referrer",103+ },104+ });105+ }106+ return jsonError("Not found.", 404);107+ } catch (error) {108+ const message = error instanceof Error ? error.message : "Unknown error";109+ console.error(JSON.stringify({ message: "Unhandled Worker error", error: message }));110+ return jsonError("Internal server error.", 500);111+ }112+ },113+ async scheduled(controller: ScheduledController, env: Env): Promise<void> {114+ const cutoff = new Date(controller.scheduledTime - GAME_RETENTION_MS).toISOString();115+ const gamesDeleted = await deleteGamesInactiveBefore(env.DB, cutoff);116+ console.log(JSON.stringify({ message: "Retention cleanup complete", cutoff, gamesDeleted }));117+ },118+} satisfies ExportedHandler<Env>;@@ -0,0 +1,163 @@
1+import {2+ countSubmittedTiles,3+ decodeBits,4+ encodeBits,5+ type BatchAction,6+ type GameState,7+ type MoveEvent,8+} from "./game.ts";9+10+type GameRow = {11+ id: string;12+ seed: string;13+ width: number;14+ height: number;15+ mine_count: number;16+ mines: string;17+ revealed: string;18+ initial_revealed: string;19+ flags: string;20+ status: "playing" | "won" | "lost";21+ revision: number;22+ exploded_index: number | null;23+ certified_no_guess: number;24+ created_at: string;25+ updated_at: string;26+};27+28+type MoveRow = {29+ action_json: string;30+ event_json: string;31+};32+33+function fromMoveRow(row: MoveRow): { action: BatchAction; event: MoveEvent } {34+ const action = JSON.parse(row.action_json) as BatchAction;35+ const storedEvent = JSON.parse(row.event_json) as MoveEvent;36+ const event = Number.isInteger(storedEvent.submittedCount)37+ ? storedEvent38+ : { ...storedEvent, submittedCount: countSubmittedTiles(action) };39+ return { action, event };40+}41+42+function fromRow(row: GameRow): GameState {43+ const revealed = decodeBits(row.revealed);44+ const storedInitial = decodeBits(row.initial_revealed);45+ return {46+ id: row.id,47+ seed: row.seed,48+ width: row.width,49+ height: row.height,50+ mines: decodeBits(row.mines),51+ revealed,52+ initialRevealed: storedInitial.length === revealed.length ? storedInitial : revealed.slice(),53+ flags: decodeBits(row.flags),54+ status: row.status,55+ revision: row.revision,56+ explodedIndex: row.exploded_index,57+ certifiedNoGuess: row.certified_no_guess === 1,58+ createdAt: row.created_at,59+ updatedAt: row.updated_at,60+ };61+}62+63+export async function insertGame(db: D1Database, state: GameState, mineCount: number): Promise<void> {64+ await db65+ .prepare(66+ `INSERT INTO games (67+ id, seed, width, height, mine_count, mines, revealed, initial_revealed, flags, status,68+ revision, exploded_index, certified_no_guess, created_at, updated_at69+ ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)`,70+ )71+ .bind(72+ state.id,73+ state.seed,74+ state.width,75+ state.height,76+ mineCount,77+ encodeBits(state.mines),78+ encodeBits(state.revealed),79+ encodeBits(state.initialRevealed),80+ encodeBits(state.flags),81+ state.status,82+ state.revision,83+ state.explodedIndex,84+ state.certifiedNoGuess ? 1 : 0,85+ state.createdAt,86+ state.updatedAt,87+ )88+ .run();89+}90+91+export async function getGame(db: D1Database, gameId: string): Promise<GameState | null> {92+ const row = await db.prepare("SELECT * FROM games WHERE id = ?1").bind(gameId).first<GameRow>();93+ return row === null ? null : fromRow(row);94+}95+96+export async function saveMove(97+ db: D1Database,98+ previousRevision: number,99+ state: GameState,100+ action: BatchAction,101+ event: MoveEvent,102+): Promise<void> {103+ const results = await db.batch([104+ db105+ .prepare(106+ `UPDATE games SET107+ revealed = ?1, flags = ?2, status = ?3, revision = ?4,108+ exploded_index = ?5, updated_at = ?6109+ WHERE id = ?7 AND revision = ?8`,110+ )111+ .bind(112+ encodeBits(state.revealed),113+ encodeBits(state.flags),114+ state.status,115+ state.revision,116+ state.explodedIndex,117+ state.updatedAt,118+ state.id,119+ previousRevision,120+ ),121+ db122+ .prepare(123+ `INSERT INTO moves (game_id, revision, intent, action_json, event_json, created_at)124+ VALUES (?1, ?2, ?3, ?4, ?5, ?6)`,125+ )126+ .bind(state.id, state.revision, action.intent, JSON.stringify(action), JSON.stringify(event), event.createdAt),127+ ]);128+ const update = results[0];129+ if (update === undefined || update.meta.changes !== 1) {130+ throw new Error("Revision conflict: observe the game again before retrying.");131+ }132+}133+134+export async function findMove(135+ db: D1Database,136+ gameId: string,137+ revision: number,138+): Promise<{ action: BatchAction; event: MoveEvent } | null> {139+ const row = await db140+ .prepare("SELECT action_json, event_json FROM moves WHERE game_id = ?1 AND revision = ?2")141+ .bind(gameId, revision)142+ .first<MoveRow>();143+ if (row === null) return null;144+ return fromMoveRow(row);145+}146+147+export async function listEvents(db: D1Database, gameId: string, sinceRevision: number): Promise<MoveEvent[]> {148+ const result = await db149+ .prepare(150+ "SELECT action_json, event_json FROM moves WHERE game_id = ?1 AND revision > ?2 ORDER BY revision ASC LIMIT 200",151+ )152+ .bind(gameId, sinceRevision)153+ .all<MoveRow>();154+ return result.results.map((row) => fromMoveRow(row).event);155+}156+157+export async function deleteGamesInactiveBefore(db: D1Database, cutoff: string): Promise<number> {158+ const result = await db159+ .prepare("DELETE FROM games WHERE updated_at < ?1 RETURNING id")160+ .bind(cutoff)161+ .all<{ id: string }>();162+ return result.results.length;163+}@@ -0,0 +1,241 @@
1+import { McpServer } from "@modelcontextprotocol/server";2+import { z } from "zod";3+import {4+ applyBatch,5+ cellsToCoordinates,6+ countMines,7+ createNoGuessGame,8+ toAgentPosition,9+ toInitialObservation,10+ toObservation,11+ type BatchAction,12+ type MoveEvent,13+} from "./game.ts";14+import { findMove, getGame, insertGame, listEvents, saveMove } from "./repository.ts";15+16+const APP_URI = "ui://mcpsweeper/spectator.html";17+const APP_MIME_TYPE = "text/html;profile=mcp-app";18+19+const gameIdSchema = z.string().uuid();20+21+function textResult(text: string, structuredContent: Record<string, unknown>) {22+ return {23+ content: [{ type: "text" as const, text }],24+ structuredContent,25+ };26+}27+28+function errorResult(error: unknown) {29+ const message = error instanceof Error ? error.message : "Unknown error";30+ return {31+ isError: true as const,32+ content: [{ type: "text" as const, text: message }],33+ };34+}35+36+function sameAction(left: BatchAction, right: BatchAction): boolean {37+ return JSON.stringify(left) === JSON.stringify(right);38+}39+40+function moveSummary(event: MoveEvent, width: number) {41+ return {42+ revision: event.revision,43+ submitted_count: event.submittedCount,44+ revealed_count: event.revealed.length,45+ flagged_count: event.flagged.length,46+ unflagged_count: event.unflagged.length,47+ status: event.status,48+ ...(event.exploded === null ? {} : { exploded_cell: event.exploded.row * width + event.exploded.col }),49+ };50+}51+52+export function createServer(env: Env, viewHtml: string, previewUrl: string): McpServer {53+ const server = new McpServer({ name: "mcpsweeper", version: "0.1.0" });54+55+ server.registerResource(56+ "MCPSweeper spectator",57+ APP_URI,58+ {59+ title: "MCPSweeper",60+ description: "Read-only animated spectator view for an agent-played Minesweeper game.",61+ mimeType: APP_MIME_TYPE,62+ _meta: {63+ ui: {64+ prefersBorder: false,65+ csp: {66+ connectDomains: [],67+ resourceDomains: [],68+ },69+ },70+ },71+ cacheHint: { ttlMs: 60_000, cacheScope: "public" },72+ },73+ async (uri) => ({74+ contents: [75+ {76+ uri: uri.href,77+ mimeType: APP_MIME_TYPE,78+ text: viewHtml,79+ _meta: {80+ ui: {81+ prefersBorder: false,82+ csp: {83+ connectDomains: [],84+ resourceDomains: [],85+ },86+ },87+ },88+ },89+ ],90+ }),91+ );92+93+ server.registerTool(94+ "start_game",95+ {96+ title: "Start MCPSweeper",97+ description:98+ "Create a no-guess Minesweeper field and open its spectator view. Continue autonomously with observe_game and play_batch until the game is won or lost. Size may be any integer from 16 to 224; 64 is the default and 224 is the current empirically sub-second generation limit at default density.",99+ inputSchema: z.object({100+ size: z.number().int().min(16).max(224).default(64).describe("Square board width and height, from 16 to 224."),101+ density: z.number().min(0.06).max(0.1).default(0.1),102+ seed: z.string().min(1).max(80).optional(),103+ }),104+ _meta: {105+ ui: { resourceUri: APP_URI, visibility: ["model", "app"] },106+ "ui/resourceUri": APP_URI,107+ },108+ },109+ async ({ size, density, seed }) => {110+ try {111+ const gameId = crypto.randomUUID();112+ const gameSeed = seed ?? gameId;113+ const state = createNoGuessGame({114+ id: gameId,115+ seed: gameSeed,116+ width: size,117+ height: size,118+ density,119+ maxAttempts: 500,120+ });121+ await insertGame(env.DB, state, countMines(state));122+ const position = toAgentPosition(state);123+ return textResult(124+ `Started ${size}x${size} no-guess MCPSweeper game ${gameId}. The spectator view is live. Solve the compact frontier constraints, then play cell IDs in batches until ${position.remaining_safe_cells} safe cells are cleared.`,125+ { game_id: gameId, position, preview_url: previewUrl },126+ );127+ } catch (error) {128+ return errorResult(error);129+ }130+ },131+ );132+133+ server.registerTool(134+ "observe_game",135+ {136+ title: "Observe MCPSweeper",137+ description:138+ "Read a compact constraint frontier without exposing hidden mines. Delta-decode frontier cells, clue cells, and flags using the included encoding notes; derive forced safe/mine cells from adjacent clues.",139+ inputSchema: z.object({ game_id: gameIdSchema }),140+ annotations: { readOnlyHint: true },141+ },142+ async ({ game_id }) => {143+ const state = await getGame(env.DB, game_id);144+ if (state === null) return errorResult(new Error("Game not found."));145+ const position = toAgentPosition(state);146+ return textResult(147+ `Game ${game_id} is ${state.status} at revision ${state.revision}; ${position.remaining_safe_cells} safe cells remain.`,148+ { position },149+ );150+ },151+ );152+153+ server.registerTool(154+ "play_batch",155+ {156+ title: "Play MCPSweeper batch",157+ description:158+ "Apply one batch using absolute integer cell IDs from the compact position. Flags are applied before reveals. Every reveal must be justified by the frontier; revealing a mine loses immediately. Use expected_revision for optimistic concurrency.",159+ inputSchema: z.object({160+ game_id: gameIdSchema,161+ expected_revision: z.number().int().nonnegative(),162+ reveal_cells: z.array(z.number().int().nonnegative()).max(8192).default([]),163+ flag_cells: z.array(z.number().int().nonnegative()).max(8192).default([]),164+ unflag_cells: z.array(z.number().int().nonnegative()).max(8192).default([]),165+ intent: z.string().max(160).default(""),166+ }),167+ annotations: { destructiveHint: true, idempotentHint: true },168+ },169+ async ({ game_id, expected_revision, reveal_cells, flag_cells, unflag_cells, intent }) => {170+ try {171+ const state = await getGame(env.DB, game_id);172+ if (state === null) return errorResult(new Error("Game not found."));173+ const action: BatchAction = {174+ reveal: cellsToCoordinates(state, reveal_cells),175+ flag: cellsToCoordinates(state, flag_cells),176+ unflag: cellsToCoordinates(state, unflag_cells),177+ intent,178+ };179+180+ if (state.revision !== expected_revision) {181+ const prior = await findMove(env.DB, game_id, expected_revision + 1);182+ if (prior !== null && sameAction(prior.action, action)) {183+ return textResult("Duplicate batch replayed safely.", {184+ move: moveSummary(prior.event, state.width),185+ position: toAgentPosition(state),186+ });187+ }188+ return errorResult(189+ new Error(`Revision conflict: expected ${expected_revision}, current revision is ${state.revision}. Observe again.`),190+ );191+ }192+193+ const event = applyBatch(state, action);194+ await saveMove(env.DB, expected_revision, state, action, event);195+ const position = toAgentPosition(state);196+ const summary =197+ state.status === "won"198+ ? `Solved MCPSweeper in ${state.revision} batches.`199+ : state.status === "lost"200+ ? "A mine detonated. The game is lost."201+ : `Batch ${state.revision} accepted: ${event.revealed.length} revealed, ${event.flagged.length} flagged; ${position.remaining_safe_cells} safe cells remain.`;202+ return textResult(summary, { move: moveSummary(event, state.width), position });203+ } catch (error) {204+ return errorResult(error);205+ }206+ },207+ );208+209+ server.registerTool(210+ "watch_game",211+ {212+ title: "Watch MCPSweeper",213+ description: "Read-only polling endpoint used by the spectator App.",214+ inputSchema: z.object({215+ game_id: gameIdSchema,216+ since_revision: z.number().int().nonnegative().default(0),217+ }),218+ annotations: { readOnlyHint: true },219+ _meta: {220+ ui: { resourceUri: APP_URI, visibility: ["app"] },221+ "ui/resourceUri": APP_URI,222+ },223+ },224+ async ({ game_id, since_revision }) => {225+ const [state, events] = await Promise.all([226+ getGame(env.DB, game_id),227+ listEvents(env.DB, game_id, since_revision),228+ ]);229+ if (state === null) return errorResult(new Error("Game not found."));230+ const observation = toObservation(state, state.status === "lost");231+ return textResult(`Spectator snapshot at revision ${state.revision}.`, {232+ observation,233+ events,234+ preview_url: previewUrl,235+ ...(since_revision === 0 ? { initial_observation: toInitialObservation(state) } : {}),236+ });237+ },238+ );239+240+ return server;241+}@@ -0,0 +1,483 @@
1+:root {2+ font-synthesis: none;3+}4+5+* {6+ box-sizing: border-box;7+}8+9+html,10+body {11+ width: 100%;12+ height: 100%;13+ margin: 0;14+ overflow: hidden;15+}16+17+body {18+ overflow: hidden;19+ background: #008080;20+}21+22+.desktop {23+ width: 100%;24+ height: 100dvh;25+ min-height: 0;26+ display: grid;27+ place-items: center;28+ padding: 3px;29+ overflow: hidden;30+}31+32+.game-window {33+ width: min(100%, 1120px);34+ height: 100%;35+ min-height: 0;36+ display: flex;37+ flex-direction: column;38+ overflow: hidden;39+}40+41+.window-body {42+ min-height: 0;43+ flex: 1;44+ display: grid;45+ grid-template-rows: auto minmax(180px, 1fr);46+ margin: 7px;47+ overflow: hidden;48+}49+50+.game-area {51+ min-width: 0;52+ min-height: 0;53+ display: grid;54+ grid-template-columns: minmax(0, 1fr) clamp(200px, 30%, 280px);55+ gap: 7px;56+ overflow: hidden;57+}58+59+.game-main {60+ min-width: 0;61+ min-height: 0;62+ display: grid;63+ grid-template-rows: auto minmax(0, 1fr);64+ overflow: hidden;65+}66+67+.board-column {68+ min-width: 0;69+ min-height: 0;70+ display: grid;71+ grid-template-rows: minmax(0, 1fr) auto auto;72+ overflow: hidden;73+}74+75+.side-panel {76+ min-width: 0;77+ min-height: 0;78+ display: grid;79+ grid-template-rows: minmax(0, 1fr) auto auto;80+ gap: 7px;81+ padding-left: 7px;82+ border-left: 2px groove #fff;83+ overflow: hidden;84+}85+86+.menu-bar {87+ display: flex;88+ align-items: center;89+ min-height: 25px;90+ margin: -2px 0 5px;91+}92+93+.menu-item {94+ min-width: 0;95+ min-height: 0;96+ padding: 3px 7px;97+ border: 0;98+ box-shadow: none;99+ background: transparent;100+}101+102+.menu-item:active,103+.menu-item:focus {104+ padding: 3px 7px;105+ box-shadow: none;106+ outline: 1px dotted #000;107+ outline-offset: -3px;108+}109+110+.hud {111+ display: grid;112+ grid-template-columns: 1fr auto 1fr;113+ align-items: center;114+ min-height: 62px;115+ margin-bottom: 7px;116+ padding: 9px 12px;117+ background: #c0c0c0;118+}119+120+.led-counter {121+ display: block;122+ width: 82px;123+ padding: 2px 5px 1px;124+ border: 2px inset #fff;125+ background: #050505;126+ color: #ff1717;127+ font-family: "Courier New", ui-monospace, monospace;128+ font-size: 35px;129+ font-weight: 700;130+ line-height: 39px;131+ letter-spacing: 2px;132+ text-align: center;133+ text-shadow: 0 0 3px #8a0000;134+}135+136+.led-counter.wide-counter {137+ width: 126px;138+ font-size: 31px;139+ letter-spacing: 1px;140+}141+142+#batch-counter {143+ justify-self: end;144+}145+146+.face-button {147+ min-width: 48px;148+ min-height: 48px;149+ padding: 2px;150+}151+152+.face-glyph {153+ position: relative;154+ display: block;155+ width: 29px;156+ height: 29px;157+ margin: auto;158+ border: 1px solid #000;159+ border-radius: 50%;160+ background: #ffff00;161+}162+163+.face-glyph::before {164+ position: absolute;165+ top: 7px;166+ left: 7px;167+ width: 3px;168+ height: 5px;169+ background: #000;170+ box-shadow: 11px 0 #000;171+ content: "";172+}173+174+.face-glyph::after {175+ position: absolute;176+ top: 13px;177+ left: 7px;178+ width: 13px;179+ height: 7px;180+ border-bottom: 2px solid #000;181+ border-radius: 0 0 13px 13px;182+ content: "";183+}184+185+.face-paused::before {186+ top: 9px;187+ height: 2px;188+}189+190+.face-lost::before {191+ top: 8px;192+ width: 4px;193+ height: 2px;194+ transform: rotate(35deg);195+ box-shadow: 10px -7px #000;196+}197+198+.face-lost::after {199+ top: 17px;200+ border-top: 2px solid #000;201+ border-bottom: 0;202+ border-radius: 13px 13px 0 0;203+}204+205+.face-won::before {206+ top: 7px;207+ left: 4px;208+ width: 9px;209+ height: 4px;210+ box-shadow: 11px 0 #000;211+}212+213+.board-frame {214+ width: 100%;215+ height: auto;216+ min-height: 0;217+ overflow: auto;218+ background: #808080;219+ overscroll-behavior: contain;220+}221+222+.board-stage {223+ width: max-content;224+ height: max-content;225+ min-width: 100%;226+ min-height: 100%;227+ display: grid;228+ place-items: center;229+}230+231+canvas {232+ display: block;233+ width: 100%;234+ height: auto;235+ aspect-ratio: 1;236+ max-width: none;237+ image-rendering: pixelated;238+}239+240+.status-bar {241+ margin-top: 6px;242+}243+244+.board-timeline {245+ margin-top: 6px;246+}247+248+.status-bar-field {249+ min-height: 20px;250+ margin: 0;251+ overflow: hidden;252+ white-space: nowrap;253+ text-overflow: ellipsis;254+}255+256+.intent-field {257+ flex: 1;258+}259+260+.connection-field {261+ flex: 0 0 74px;262+ text-align: center;263+ font-weight: 700;264+}265+266+.replay-panel {267+ min-height: 0;268+ display: flex;269+ flex-direction: column;270+ padding: 6px;271+ background: #c0c0c0;272+ overflow: hidden;273+}274+275+.replay-heading {276+ display: flex;277+ align-items: center;278+ justify-content: space-between;279+ margin-bottom: 5px;280+}281+282+#comparison-phase {283+ min-width: 54px;284+ padding: 1px 3px;285+ background: #000080;286+ color: #fff;287+ font-weight: 700;288+ text-align: center;289+}290+291+.timeline-row {292+ display: grid;293+ grid-template-columns: 36px minmax(50px, 1fr) 64px;294+ gap: 5px;295+ align-items: center;296+}297+298+.timeline-row input {299+ width: 100%;300+ margin: 0;301+}302+303+#replay-position {304+ font-family: "Courier New", ui-monospace, monospace;305+ font-weight: 700;306+ text-align: right;307+}308+309+.intent-header {310+ display: grid;311+ grid-template-columns: 38px 46px minmax(0, 1fr);312+ gap: 3px;313+ margin-top: 6px;314+ padding: 0 5px;315+ font-weight: 700;316+}317+318+.intent-log {319+ min-height: 42px;320+ flex: 1;321+ margin: 5px 0 0;322+ padding: 2px;323+ overflow-y: auto;324+ border: 2px inset #fff;325+ background: #fff;326+ list-style: none;327+}328+329+.intent-log li {330+ margin: 0;331+}332+333+.intent-log button {334+ display: block;335+ width: 100%;336+ min-width: 0;337+ min-height: 0;338+ padding: 2px 4px;339+ overflow: hidden;340+ border: 0;341+ background: transparent;342+ box-shadow: none;343+ color: #000;344+ text-align: left;345+ text-overflow: ellipsis;346+ white-space: nowrap;347+}348+349+.intent-log button {350+ display: grid;351+ grid-template-columns: 38px 46px minmax(0, 1fr);352+ gap: 3px;353+}354+355+.intent-log button span:last-child {356+ overflow: hidden;357+ text-overflow: ellipsis;358+}359+360+.intent-log button:active,361+.intent-log button:focus {362+ padding: 2px 4px;363+ box-shadow: none;364+}365+366+.intent-log button[aria-current="step"] {367+ background: #000080;368+ color: #fff;369+}370+371+.intent-empty {372+ padding: 3px 4px;373+ color: #808080;374+}375+376+.controls {377+ display: grid;378+ grid-template-columns: 1fr 1fr;379+ gap: 6px;380+}381+382+#pause-btn {383+ grid-column: auto;384+}385+386+.replay-link {387+ grid-column: 1 / -1;388+ min-height: 23px;389+ padding: 3px 8px;390+ border: 2px outset #fff;391+ background: #c0c0c0;392+ color: #000;393+ text-align: center;394+ text-decoration: none;395+}396+397+.replay-link:active {398+ border-style: inset;399+ padding: 4px 7px 2px 9px;400+}401+402+.replay-link[aria-disabled="true"] {403+ color: #808080;404+ pointer-events: none;405+}406+407+.zoom-slider {408+ width: 100%;409+ margin: 0;410+}411+412+.controls button {413+ min-width: 0;414+}415+416+@media (max-width: 560px) {417+ .desktop {418+ padding: 2px;419+ }420+421+ .window-body {422+ margin: 4px;423+ }424+425+ .hud {426+ min-height: 58px;427+ padding: 6px 8px;428+ }429+430+ .led-counter {431+ width: 68px;432+ font-size: 28px;433+ line-height: 33px;434+ }435+436+ .face-button {437+ min-width: 40px;438+ min-height: 40px;439+ font-size: 21px;440+ }441+442+ .controls {443+ flex-wrap: wrap;444+ }445+446+}447+448+@media (max-height: 700px) {449+ .menu-bar {450+ min-height: 20px;451+ margin-bottom: 3px;452+ }453+454+ .hud {455+ min-height: 48px;456+ margin-bottom: 4px;457+ padding: 4px 8px;458+ }459+460+ .led-counter {461+ width: 66px;462+ font-size: 26px;463+ line-height: 31px;464+ }465+466+ .face-button {467+ min-width: 38px;468+ min-height: 38px;469+ }470+471+ .face-glyph {472+ width: 25px;473+ height: 25px;474+ }475+476+ .intent-log {477+ height: 42px;478+ }479+480+ .controls {481+ margin-top: 3px;482+ }483+}@@ -0,0 +1,1044 @@
1+import { App } from "@modelcontextprotocol/ext-apps";2+import "98.css";3+import "./view.css";4+5+type Status = "playing" | "won" | "lost";6+type Coordinate = { row: number; col: number };7+type Observation = {8+ game_id: string;9+ revision: number;10+ status: Status;11+ width: number;12+ height: number;13+ mine_count: number;14+ revealed_count: number;15+ flagged_count: number;16+ remaining_safe_cells: number;17+ certified_no_guess: boolean;18+ rows: string[];19+};20+type MoveEvent = {21+ revision: number;22+ intent: string;23+ submittedCount: number;24+ revealed: Coordinate[];25+ revealedValues?: Array<Coordinate & { value: string }>;26+ flagged: Coordinate[];27+ unflagged: Coordinate[];28+ exploded: Coordinate | null;29+ status: Status;30+ createdAt: string;31+};32+type StructuredResult = {33+ game_id?: string;34+ preview_url?: unknown;35+ observation?: unknown;36+ initial_observation?: unknown;37+ events?: unknown[];38+ event?: unknown;39+};40+type ReplayTurn = [41+ revision: number,42+ intent: string,43+ submittedCount: number,44+ revealedValues: Array<[cell: number, value: string]>,45+ flaggedCells: number[],46+ unflaggedCells: number[],47+ explodedCell: number | null,48+ status: Status,49+];50+type ReplayLinkPayload = {51+ v: 1;52+ s: Observation;53+ t: ReplayTurn[];54+};55+type Connection = "preview" | "connecting" | "live" | "replay" | "error";56+57+let hostApp: App | null = null;58+59+function requiredElement<T extends Element>(selector: string): T {60+ const element = document.querySelector<T>(selector);61+ if (element === null) throw new Error(`Missing required element: ${selector}`);62+ return element;63+}64+65+const canvas = requiredElement<HTMLCanvasElement>("#game");66+const boardFrame = requiredElement<HTMLDivElement>(".board-frame");67+const pauseButton = requiredElement<HTMLButtonElement>("#pause-btn");68+const speedButton = requiredElement<HTMLButtonElement>("#speed-btn");69+const fitButton = requiredElement<HTMLButtonElement>("#fit-btn");70+const compareButton = requiredElement<HTMLButtonElement>("#compare-btn");71+const replayLink = requiredElement<HTMLAnchorElement>("#replay-link");72+const faceButton = requiredElement<HTMLButtonElement>("#face-btn");73+const faceIcon = requiredElement<HTMLSpanElement>("#face-icon");74+const mineCounter = requiredElement<HTMLOutputElement>("#mine-counter");75+const batchCounter = requiredElement<HTMLOutputElement>("#batch-counter");76+const statusText = requiredElement<HTMLParagraphElement>("#status-text");77+const connectionText = requiredElement<HTMLParagraphElement>("#connection-text");78+const replaySlider = requiredElement<HTMLInputElement>("#replay-slider");79+const replayPosition = requiredElement<HTMLOutputElement>("#replay-position");80+const intentLog = requiredElement<HTMLOListElement>("#intent-log");81+const zoomSlider = requiredElement<HTMLInputElement>("#zoom-slider");82+const comparisonPhase = requiredElement<HTMLOutputElement>("#comparison-phase");83+const maybeContext = canvas.getContext("2d");84+if (maybeContext === null) throw new Error("Canvas 2D rendering is unavailable.");85+const context: CanvasRenderingContext2D = maybeContext;86+const MAX_CANVAS_BACKING_SIZE = 4096;87+let canvasCssSize = 768;88+let canvasRenderScale = 1;89+let lastBoardPaintKey = "";90+91+const state: {92+ observation: Observation | null;93+ initialObservation: Observation | null;94+ events: MoveEvent[];95+ latestIntent: string;96+ previewUrl: string;97+ connection: Connection;98+ error: string;99+ displayRevision: number;100+ followingLive: boolean;101+ replaying: boolean;102+ replayAccumulatorMs: number;103+ speed: number;104+ elapsedMs: number;105+ zoom: number;106+ centeredGameId: string;107+ comparisonEnabled: boolean;108+ comparisonAfter: boolean;109+ comparisonAccumulatorMs: number;110+} = {111+ observation: null,112+ initialObservation: null,113+ events: [],114+ latestIntent: "Waiting for the agent to begin…",115+ previewUrl: "",116+ connection: window.top === window ? "preview" : "connecting",117+ error: "",118+ displayRevision: 0,119+ followingLive: true,120+ replaying: false,121+ replayAccumulatorMs: 0,122+ speed: 1,123+ elapsedMs: 0,124+ zoom: 3,125+ centeredGameId: "",126+ comparisonEnabled: true,127+ comparisonAfter: true,128+ comparisonAccumulatorMs: 0,129+};130+131+const flashUntil = new Map<string, number>();132+const numberColors = ["#000000", "#0000ff", "#008000", "#ff0000", "#000080", "#800000", "#008080", "#000000", "#808080"];133+const clueGlyphs: Record<string, string[]> = {134+ "1": ["00100", "01100", "00100", "00100", "00100", "00100", "01110"],135+ "2": ["11111", "00001", "00001", "11111", "10000", "10000", "11111"],136+ "3": ["11111", "00001", "00001", "11111", "00001", "00001", "11111"],137+ "4": ["10001", "10001", "10001", "11111", "00001", "00001", "00001"],138+ "5": ["11111", "10000", "10000", "11111", "00001", "00001", "11111"],139+ "6": ["11111", "10000", "10000", "11111", "10001", "10001", "11111"],140+ "7": ["11111", "00001", "00010", "00100", "01000", "01000", "01000"],141+ "8": ["11111", "10001", "10001", "11111", "10001", "10001", "11111"],142+};143+144+function structured(value: unknown): StructuredResult | null {145+ return typeof value === "object" && value !== null ? (value as StructuredResult) : null;146+}147+148+function isCoordinate(value: unknown): value is Coordinate {149+ if (typeof value !== "object" || value === null) return false;150+ const coordinate = value as Partial<Coordinate>;151+ return Number.isInteger(coordinate.row) && Number.isInteger(coordinate.col);152+}153+154+function isVisualObservation(value: unknown): value is Observation {155+ if (typeof value !== "object" || value === null) return false;156+ const observation = value as Partial<Observation>;157+ return (158+ typeof observation.game_id === "string" &&159+ Number.isInteger(observation.revision) &&160+ Number.isInteger(observation.width) &&161+ Number.isInteger(observation.height) &&162+ Array.isArray(observation.rows) &&163+ observation.rows.length === observation.height &&164+ observation.rows.every((row) => typeof row === "string" && row.length === observation.width)165+ );166+}167+168+function normalizeEvent(value: unknown): MoveEvent | null {169+ if (typeof value !== "object" || value === null) return null;170+ const event = value as Partial<MoveEvent>;171+ if (!Number.isInteger(event.revision)) return null;172+ const status: Status = event.status === "won" || event.status === "lost" ? event.status : "playing";173+ const coordinates = (items: unknown): Coordinate[] => (Array.isArray(items) ? items.filter(isCoordinate) : []);174+ const exploded = isCoordinate(event.exploded) ? event.exploded : null;175+ const revealedValues = Array.isArray(event.revealedValues)176+ ? event.revealedValues.filter(177+ (cell): cell is Coordinate & { value: string } =>178+ isCoordinate(cell) && typeof (cell as { value?: unknown }).value === "string",179+ )180+ : [];181+ const revealed = coordinates(event.revealed);182+ const flagged = coordinates(event.flagged);183+ const unflagged = coordinates(event.unflagged);184+ return {185+ revision: event.revision as number,186+ intent: typeof event.intent === "string" ? event.intent : "",187+ submittedCount:188+ Number.isInteger(event.submittedCount) && (event.submittedCount as number) >= 0189+ ? (event.submittedCount as number)190+ : revealed.length + flagged.length + unflagged.length + (exploded === null ? 0 : 1),191+ revealed,192+ revealedValues,193+ flagged,194+ unflagged,195+ exploded,196+ status,197+ createdAt: typeof event.createdAt === "string" ? event.createdAt : "",198+ };199+}200+201+function coordinateToCell(width: number, coordinate: Coordinate): number {202+ return coordinate.row * width + coordinate.col;203+}204+205+function cellToCoordinate(width: number, cell: number): Coordinate {206+ return { row: Math.floor(cell / width), col: cell % width };207+}208+209+function eventToReplayTurn(event: MoveEvent, width: number): ReplayTurn {210+ return [211+ event.revision,212+ event.intent,213+ event.submittedCount,214+ (event.revealedValues ?? []).map((cell) => [coordinateToCell(width, cell), cell.value]),215+ event.flagged.map((cell) => coordinateToCell(width, cell)),216+ event.unflagged.map((cell) => coordinateToCell(width, cell)),217+ event.exploded === null ? null : coordinateToCell(width, event.exploded),218+ event.status,219+ ];220+}221+222+function replayTurnToEvent(value: unknown, width: number, height: number): MoveEvent | null {223+ if (!Array.isArray(value) || value.length !== 8) return null;224+ const [revision, intent, submittedCount, revealedValues, flaggedCells, unflaggedCells, explodedCell, status] = value;225+ const totalCells = width * height;226+ const validCell = (cell: unknown): cell is number => Number.isInteger(cell) && (cell as number) >= 0 && (cell as number) < totalCells;227+ if (228+ !Number.isInteger(revision) ||229+ typeof intent !== "string" ||230+ !Number.isInteger(submittedCount) ||231+ (submittedCount as number) < 0 ||232+ !Array.isArray(revealedValues) ||233+ !Array.isArray(flaggedCells) ||234+ !Array.isArray(unflaggedCells) ||235+ (explodedCell !== null && !validCell(explodedCell)) ||236+ (status !== "playing" && status !== "won" && status !== "lost")237+ ) {238+ return null;239+ }240+ const revealed = revealedValues241+ .filter(242+ (cell): cell is [number, string] =>243+ Array.isArray(cell) && cell.length === 2 && validCell(cell[0]) && typeof cell[1] === "string",244+ )245+ .map(([cell, clue]) => ({ ...cellToCoordinate(width, cell), value: clue }));246+ if (revealed.length !== revealedValues.length || !flaggedCells.every(validCell) || !unflaggedCells.every(validCell)) {247+ return null;248+ }249+ return {250+ revision: revision as number,251+ intent,252+ submittedCount: submittedCount as number,253+ revealed: revealed.map(({ row, col }) => ({ row, col })),254+ revealedValues: revealed,255+ flagged: flaggedCells.map((cell) => cellToCoordinate(width, cell)),256+ unflagged: unflaggedCells.map((cell) => cellToCoordinate(width, cell)),257+ exploded: explodedCell === null ? null : cellToCoordinate(width, explodedCell),258+ status,259+ createdAt: "",260+ };261+}262+263+function bytesToBase64Url(bytes: Uint8Array): string {264+ let binary = "";265+ for (let offset = 0; offset < bytes.length; offset += 8192) {266+ binary += String.fromCharCode(...bytes.subarray(offset, offset + 8192));267+ }268+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");269+}270+271+function base64UrlToBytes(value: string): Uint8Array {272+ const base64 = value.replaceAll("-", "+").replaceAll("_", "/").padEnd(Math.ceil(value.length / 4) * 4, "=");273+ const binary = atob(base64);274+ return Uint8Array.from(binary, (character) => character.charCodeAt(0));275+}276+277+async function encodeReplayLink(payload: ReplayLinkPayload): Promise<string> {278+ const bytes = new TextEncoder().encode(JSON.stringify(payload));279+ if (typeof CompressionStream === "undefined") return `j.${bytesToBase64Url(bytes)}`;280+ const stream = new Blob([bytes.buffer as ArrayBuffer]).stream().pipeThrough(new CompressionStream("gzip"));281+ return `g.${bytesToBase64Url(new Uint8Array(await new Response(stream).arrayBuffer()))}`;282+}283+284+async function readReplayLink(): Promise<{ start: Observation; turns: MoveEvent[] } | null> {285+ const encoded = new URLSearchParams(location.hash.slice(1)).get("replay");286+ if (encoded === null) return null;287+ try {288+ const separator = encoded.indexOf(".");289+ const format = encoded.slice(0, separator);290+ const bytes = base64UrlToBytes(encoded.slice(separator + 1));291+ const json =292+ format === "g"293+ ? await new Response(294+ new Blob([bytes.buffer as ArrayBuffer]).stream().pipeThrough(new DecompressionStream("gzip")),295+ ).text()296+ : format === "j"297+ ? new TextDecoder().decode(bytes)298+ : "";299+ const payload = JSON.parse(json) as Partial<ReplayLinkPayload>;300+ if (payload.v !== 1 || !isVisualObservation(payload.s) || !Array.isArray(payload.t)) return null;301+ const turns = payload.t.map((turn) => replayTurnToEvent(turn, payload.s!.width, payload.s!.height));302+ if (turns.some((turn) => turn === null)) return null;303+ return { start: payload.s, turns: turns as MoveEvent[] };304+ } catch {305+ return null;306+ }307+}308+309+function beginLinkedReplay(start: Observation, turns: MoveEvent[]): void {310+ state.initialObservation = start;311+ state.events = turns.sort((left, right) => left.revision - right.revision);312+ const lastTurn = state.events.at(-1);313+ const revision = lastTurn?.revision ?? start.revision;314+ state.observation = { ...start, revision, status: lastTurn?.status ?? start.status };315+ state.latestIntent = lastTurn?.intent || "Replay loaded";316+ state.connection = "replay";317+ state.displayRevision = revision;318+ state.followingLive = false;319+ state.centeredGameId = start.game_id;320+ const previewUrl = new URL(location.href);321+ previewUrl.hash = "";322+ previewUrl.search = "";323+ state.previewUrl = previewUrl.toString();324+ state.observation = observationAt(revision) ?? state.observation;325+}326+327+function consumeResult(value: unknown): void {328+ const result = structured(value);329+ if (result === null) return;330+ if (typeof result.preview_url === "string") state.previewUrl = result.preview_url;331+ if (result.game_id !== undefined && state.observation === null) {332+ state.latestIntent = `Field ${result.game_id.slice(0, 8)} initialized`;333+ }334+ if (isVisualObservation(result.initial_observation)) state.initialObservation = result.initial_observation;335+ if (isVisualObservation(result.observation)) {336+ if (state.initialObservation === null && result.observation.revision === 0) {337+ state.initialObservation = structuredClone(result.observation);338+ }339+ state.observation = result.observation;340+ if (state.centeredGameId !== result.observation.game_id) {341+ state.centeredGameId = result.observation.game_id;342+ requestAnimationFrame(() => {343+ boardFrame.scrollLeft = (boardFrame.scrollWidth - boardFrame.clientWidth) / 2;344+ boardFrame.scrollTop = (boardFrame.scrollHeight - boardFrame.clientHeight) / 2;345+ });346+ }347+ }348+ const incoming = result.events ?? (result.event === undefined ? [] : [result.event]);349+ let acceptedEvents = 0;350+ for (const value of incoming) {351+ const event = normalizeEvent(value);352+ if (event === null) continue;353+ if (state.events.some((current) => current.revision === event.revision)) continue;354+ acceptedEvents += 1;355+ state.events.push(event);356+ state.latestIntent = event.intent || `Batch ${event.revision}`;357+ for (const coordinate of [...event.revealed, ...event.flagged, ...event.unflagged]) {358+ flashUntil.set(`${coordinate.row}:${coordinate.col}`, state.elapsedMs + 1700 / state.speed);359+ }360+ if (event.exploded !== null) flashUntil.set(`${event.exploded.row}:${event.exploded.col}`, Number.POSITIVE_INFINITY);361+ }362+ state.events.sort((left, right) => left.revision - right.revision);363+ if (state.followingLive && state.observation !== null) state.displayRevision = state.observation.revision;364+ if (acceptedEvents > 0) {365+ state.comparisonAfter = false;366+ state.comparisonAccumulatorMs = 0;367+ }368+ state.connection = "live";369+ state.error = "";370+ void refreshReplayLink();371+}372+373+function drawRaisedCell(x: number, y: number, size: number): void {374+ context.fillStyle = "#c0c0c0";375+ context.fillRect(x, y, size, size);376+ const edge = Math.max(1, Math.floor(size * 0.13));377+ context.fillStyle = "#ffffff";378+ context.fillRect(x, y, size, edge);379+ context.fillRect(x, y, edge, size);380+ context.fillStyle = "#808080";381+ context.fillRect(x, y + size - edge, size, edge);382+ context.fillRect(x + size - edge, y, edge, size);383+ context.fillStyle = "#000000";384+ context.fillRect(x + edge, y + size - edge, size - edge, Math.max(1, edge / 2));385+ context.fillRect(x + size - edge, y + edge, Math.max(1, edge / 2), size - edge);386+}387+388+function drawRevealedCell(x: number, y: number, size: number): void {389+ context.fillStyle = "#c0c0c0";390+ context.fillRect(x, y, size, size);391+ context.strokeStyle = "#808080";392+ context.lineWidth = 1;393+ context.strokeRect(x + 0.5, y + 0.5, size - 1, size - 1);394+}395+396+function drawFlag(x: number, y: number, size: number): void {397+ context.fillStyle = "#000000";398+ context.fillRect(x + size * 0.45, y + size * 0.25, Math.max(1, size * 0.1), size * 0.48);399+ context.fillRect(x + size * 0.28, y + size * 0.7, size * 0.46, Math.max(1, size * 0.1));400+ context.fillStyle = "#ff0000";401+ context.beginPath();402+ context.moveTo(x + size * 0.48, y + size * 0.22);403+ context.lineTo(x + size * 0.22, y + size * 0.38);404+ context.lineTo(x + size * 0.48, y + size * 0.53);405+ context.closePath();406+ context.fill();407+}408+409+function drawMine(x: number, y: number, size: number, exploded: boolean): void {410+ if (exploded) {411+ context.fillStyle = "#ff0000";412+ context.fillRect(x, y, size, size);413+ }414+ context.fillStyle = "#000000";415+ context.beginPath();416+ context.arc(x + size / 2, y + size / 2, size * 0.24, 0, Math.PI * 2);417+ context.fill();418+ context.fillRect(x + size * 0.16, y + size * 0.46, size * 0.68, Math.max(1, size * 0.08));419+ context.fillRect(x + size * 0.46, y + size * 0.16, Math.max(1, size * 0.08), size * 0.68);420+}421+422+function drawClue(value: string, x: number, y: number, size: number): void {423+ const glyph = clueGlyphs[value];424+ if (glyph === undefined) return;425+ const pixel = Math.max(1, Math.floor((size - 1) / 7));426+ const width = 5 * pixel;427+ const height = 7 * pixel;428+ const left = Math.round(x + (size - width) / 2);429+ const top = Math.round(y + (size - height) / 2);430+ context.fillStyle = numberColors[Number(value)] ?? "#000000";431+ for (let row = 0; row < glyph.length; row += 1) {432+ const pixels = glyph[row] ?? "";433+ for (let col = 0; col < pixels.length; col += 1) {434+ if (pixels[col] === "1") context.fillRect(left + col * pixel, top + row * pixel, pixel, pixel);435+ }436+ }437+}438+439+type BoardDetail = "overview" | "compact" | "detailed";440+441+function detailForCellSize(cellSize: number): BoardDetail {442+ if (cellSize < 4) return "overview";443+ if (cellSize < 7) return "compact";444+ return "detailed";445+}446+447+function drawOverviewCell(value: string, x: number, y: number, size: number): void {448+ if (value === "X") context.fillStyle = "#ff0000";449+ else if (value === "*") context.fillStyle = "#000000";450+ else if (value === "#" || value === "F") context.fillStyle = "#a8a8a8";451+ else context.fillStyle = "#d0d0d0";452+ context.fillRect(x, y, size + 0.05, size + 0.05);453+454+ if (value === "F") {455+ context.fillStyle = "#ff0000";456+ const marker = Math.max(1 / canvasRenderScale, size * 0.5);457+ context.fillRect(x + (size - marker) / 2, y + (size - marker) / 2, marker, marker);458+ } else if (value >= "1" && value <= "8") {459+ context.fillStyle = numberColors[Number(value)] ?? "#000000";460+ const marker = Math.max(1 / canvasRenderScale, size * 0.34);461+ context.fillRect(x + (size - marker) / 2, y + (size - marker) / 2, marker, marker);462+ }463+}464+465+function drawCompactMarker(value: string, x: number, y: number, size: number): void {466+ if (value === "F") {467+ context.fillStyle = "#ff0000";468+ const marker = Math.max(1, size * 0.42);469+ context.fillRect(x + (size - marker) / 2, y + (size - marker) / 2, marker, marker);470+ } else if (value === "X" || value === "*") {471+ drawMine(x, y, size, value === "X");472+ } else if (value >= "1" && value <= "8") {473+ context.fillStyle = numberColors[Number(value)] ?? "#000000";474+ const marker = Math.max(1, size * 0.3);475+ context.fillRect(x + (size - marker) / 2, y + (size - marker) / 2, marker, marker);476+ }477+}478+479+function activeFlashCount(): number {480+ let count = 0;481+ for (const expiresAt of flashUntil.values()) {482+ if (expiresAt > state.elapsedMs) count += 1;483+ }484+ return count;485+}486+487+function configureCanvas(cssSize: number): void {488+ canvasCssSize = Math.max(1, Math.round(cssSize));489+ const deviceScale = Math.max(1, window.devicePixelRatio || 1);490+ canvasRenderScale = Math.min(deviceScale, MAX_CANVAS_BACKING_SIZE / canvasCssSize);491+ const backingSize = Math.max(1, Math.round(canvasCssSize * canvasRenderScale));492+ canvas.style.width = `${canvasCssSize}px`;493+ canvas.style.height = `${canvasCssSize}px`;494+ if (canvas.width !== backingSize || canvas.height !== backingSize) {495+ canvas.width = backingSize;496+ canvas.height = backingSize;497+ }498+ canvasRenderScale = backingSize / canvasCssSize;499+ context.setTransform(canvasRenderScale, 0, 0, canvasRenderScale, 0, 0);500+ context.imageSmoothingEnabled = false;501+ lastBoardPaintKey = "";502+}503+504+function renderBoard(observation: Observation): void {505+ const cellSize = canvasCssSize / Math.max(observation.width, observation.height);506+ const detail = detailForCellSize(cellSize);507+ const flashCount = activeFlashCount();508+ const paintKey = `${observation.game_id}:${observation.revision}:${canvas.width}:${canvasCssSize}:${detail}:${flashCount}`;509+ if (paintKey === lastBoardPaintKey) return;510+ lastBoardPaintKey = paintKey;511+512+ context.fillStyle = "#808080";513+ context.fillRect(0, 0, canvasCssSize, canvasCssSize);514+ for (let row = 0; row < observation.height; row += 1) {515+ const rowValue = observation.rows[row] ?? "";516+ for (let col = 0; col < observation.width; col += 1) {517+ const value = rowValue[col] ?? "#";518+ const x = col * cellSize;519+ const y = row * cellSize;520+ const flashing = (flashUntil.get(`${row}:${col}`) ?? 0) > state.elapsedMs;521+ if (detail === "overview") drawOverviewCell(value, x, y, cellSize);522+ else {523+ if (value === "#" || value === "F") drawRaisedCell(x, y, cellSize);524+ else drawRevealedCell(x, y, cellSize);525+ }526+527+ if (flashing && value !== "X") {528+ context.strokeStyle = "#ffff00";529+ context.lineWidth = Math.max(1 / canvasRenderScale, cellSize * 0.12);530+ const inset = Math.min(1, cellSize * 0.2);531+ context.strokeRect(x + inset, y + inset, cellSize - inset * 2, cellSize - inset * 2);532+ }533+ if (detail === "detailed") {534+ if (value === "F") drawFlag(x, y, cellSize);535+ else if (value === "X" || value === "*") drawMine(x, y, cellSize, value === "X");536+ else if (value !== "#" && value !== "0") drawClue(value, x, y, cellSize);537+ } else if (detail === "compact") {538+ drawCompactMarker(value, x, y, cellSize);539+ }540+ }541+ }542+}543+544+function renderWaiting(): void {545+ context.fillStyle = "#c0c0c0";546+ context.fillRect(0, 0, canvasCssSize, canvasCssSize);547+ context.fillStyle = "#000000";548+ context.textAlign = "center";549+ context.textBaseline = "middle";550+ context.font = "bold 28px 'MS Sans Serif', Arial, sans-serif";551+ context.fillText("Waiting for agent…", canvasCssSize / 2, canvasCssSize / 2);552+}553+554+function counterDigits(value: number, minimumDigits = 3): string {555+ return Math.max(0, value).toString().padStart(minimumDigits, "0");556+}557+558+let historicalObservationCache: { key: string; observation: Observation } | null = null;559+function observationAt(revision: number): Observation | null {560+ const live = state.observation;561+ if (live === null) return null;562+ if ((revision >= live.revision && state.connection !== "replay") || state.initialObservation === null) return live;563+ const cacheKey = `${live.game_id}:${live.revision}:${state.events.length}:${state.initialObservation.revision}:${revision}`;564+ if (historicalObservationCache?.key === cacheKey) return historicalObservationCache.observation;565+566+ const rows = state.initialObservation.rows.map((row) => row.split(""));567+ let status: Status = state.initialObservation.status;568+ for (const event of state.events) {569+ if (event.revision > revision) break;570+ for (const coordinate of event.unflagged) {571+ const row = rows[coordinate.row];572+ if (row?.[coordinate.col] === "F") row[coordinate.col] = "#";573+ }574+ for (const coordinate of event.flagged) {575+ const row = rows[coordinate.row];576+ if (row?.[coordinate.col] === "#") row[coordinate.col] = "F";577+ }578+ for (const cell of event.revealedValues ?? []) {579+ const row = rows[cell.row];580+ if (row !== undefined) row[cell.col] = cell.value;581+ }582+ if (event.exploded !== null) {583+ const row = rows[event.exploded.row];584+ if (row !== undefined) row[event.exploded.col] = "X";585+ }586+ status = event.status;587+ }588+ const renderedRows = rows.map((row) => row.join(""));589+ let revealedCount = 0;590+ let flaggedCount = 0;591+ for (const row of renderedRows) {592+ for (const cell of row) {593+ if (cell === "F") flaggedCount += 1;594+ else if (cell !== "#" && cell !== "*" && cell !== "X") revealedCount += 1;595+ }596+ }597+ const observation = {598+ ...state.initialObservation,599+ revision,600+ status,601+ rows: renderedRows,602+ revealed_count: revealedCount,603+ flagged_count: flaggedCount,604+ remaining_safe_cells: live.width * live.height - live.mine_count - revealedCount,605+ };606+ historicalObservationCache = { key: cacheKey, observation };607+ return observation;608+}609+610+function selectRevision(revision: number): void {611+ const maximum = state.observation?.revision ?? 0;612+ state.displayRevision = Math.max(0, Math.min(maximum, revision));613+ state.followingLive = state.displayRevision === maximum && state.observation?.status === "playing";614+ state.replaying = false;615+ state.replayAccumulatorMs = 0;616+ state.comparisonAfter = false;617+ state.comparisonAccumulatorMs = 0;618+}619+620+let lastIntentLogKey = "";621+function renderReplayControls(): void {622+ const maximum = state.observation?.revision ?? 0;623+ replaySlider.max = String(maximum);624+ replaySlider.value = String(Math.min(maximum, state.displayRevision));625+ replayPosition.textContent = `${counterDigits(state.displayRevision)} / ${counterDigits(maximum)}`;626+ pauseButton.textContent = state.replaying ? "Pause" : state.displayRevision < maximum ? "Resume" : "Replay";627+628+ const key = `${state.events.length}:${state.displayRevision}:${maximum}`;629+ if (key === lastIntentLogKey) return;630+ lastIntentLogKey = key;631+ intentLog.replaceChildren();632+ const entries = [633+ { revision: 0, updates: 0, intent: "Initial opening" },634+ ...state.events.map((event) => ({635+ revision: event.revision,636+ updates: event.submittedCount,637+ intent: event.intent || `Batch ${event.revision}`,638+ })),639+ ];640+ for (const entry of entries) {641+ const item = document.createElement("li");642+ const button = document.createElement("button");643+ button.type = "button";644+ const revision = document.createElement("span");645+ revision.textContent = counterDigits(entry.revision);646+ const updates = document.createElement("span");647+ updates.textContent = entry.updates.toLocaleString();648+ const intent = document.createElement("span");649+ intent.textContent = entry.intent;650+ button.append(revision, updates, intent);651+ if (entry.revision === state.displayRevision) button.setAttribute("aria-current", "step");652+ button.addEventListener("click", () => selectRevision(entry.revision));653+ item.append(button);654+ intentLog.append(item);655+ }656+ intentLog.querySelector<HTMLElement>("[aria-current='step']")?.scrollIntoView({ block: "nearest" });657+}658+659+function visibleRevision(): number {660+ return (661+ state.comparisonEnabled && !state.replaying && state.displayRevision > 0 && !state.comparisonAfter662+ ? state.displayRevision - 1663+ : state.displayRevision664+ );665+}666+667+function render(): void {668+ const comparedRevision = visibleRevision();669+ const observation = observationAt(comparedRevision);670+ if (observation === null) renderWaiting();671+ else renderBoard(observation);672+673+ const remainingMines = observation === null ? 0 : observation.mine_count - observation.flagged_count;674+ mineCounter.textContent = counterDigits(remainingMines, remainingMines > 999 ? 5 : 3);675+ mineCounter.classList.toggle("wide-counter", remainingMines > 999);676+ batchCounter.textContent = counterDigits(observation?.revision ?? 0);677+ const selectedEvent = [...state.events].reverse().find((event) => event.revision <= state.displayRevision);678+ statusText.textContent = state.error || selectedEvent?.intent || (state.displayRevision === 0 ? "Initial opening" : state.latestIntent);679+ connectionText.textContent = state.connection.toUpperCase();680+ comparisonPhase.textContent = state.displayRevision === 0 ? "OPEN" : comparedRevision < state.displayRevision ? "BEFORE" : "AFTER";681+ faceIcon.className = `face-glyph ${682+ observation?.status === "won"683+ ? "face-won"684+ : observation?.status === "lost"685+ ? "face-lost"686+ : !state.replaying && state.displayRevision < (state.observation?.revision ?? 0)687+ ? "face-paused"688+ : "face-playing"689+ }`;690+ renderReplayControls();691+}692+693+function update(deltaMs: number): void {694+ state.elapsedMs += deltaMs * state.speed;695+ if (state.comparisonEnabled && !state.replaying && state.displayRevision > 0) {696+ state.comparisonAccumulatorMs += deltaMs;697+ if (state.comparisonAccumulatorMs >= 750) {698+ state.comparisonAccumulatorMs = 0;699+ state.comparisonAfter = !state.comparisonAfter;700+ }701+ }702+ if (!state.replaying || state.observation === null) return;703+ state.replayAccumulatorMs += deltaMs * state.speed;704+ if (state.replayAccumulatorMs < 650) return;705+ state.replayAccumulatorMs = 0;706+ state.displayRevision += 1;707+ if (state.displayRevision >= state.observation.revision) {708+ state.displayRevision = state.observation.revision;709+ state.replaying = false;710+ }711+}712+713+function makePreview(): Observation {714+ const sizeParameter = new URLSearchParams(window.location.search).get("size");715+ const requestedSize = sizeParameter === null ? Number.NaN : Number(sizeParameter);716+ const size = Number.isInteger(requestedSize) ? Math.max(16, Math.min(224, requestedSize)) : 64;717+ const center = (size - 1) / 2;718+ const rows: string[] = [];719+ let revealed = 0;720+ let flagged = 0;721+ for (let row = 0; row < size; row += 1) {722+ let line = "";723+ for (let col = 0; col < size; col += 1) {724+ const distance = Math.hypot(row - center, col - center);725+ if ((row * 17 + col * 31) % 47 === 0 && distance < size * 0.43) {726+ line += "F";727+ flagged += 1;728+ } else if (distance < size * 0.4 || (row + col) % 13 === 0) {729+ line += String((row * 7 + col * 3) % 4);730+ revealed += 1;731+ } else line += "#";732+ }733+ rows.push(line);734+ }735+ return {736+ game_id: "preview-field",737+ revision: 7,738+ status: "playing",739+ width: size,740+ height: size,741+ mine_count: Math.round(size * size * 0.1),742+ revealed_count: revealed,743+ flagged_count: flagged,744+ remaining_safe_cells: size * size - Math.round(size * size * 0.1) - revealed,745+ certified_no_guess: true,746+ rows,747+ };748+}749+750+function beginPreview(): void {751+ const observation = makePreview();752+ state.observation = observation;753+ state.previewUrl = new URL("/preview", location.href).toString();754+ const intents = [755+ "Expand the zero frontier from the central opening.",756+ "Resolve the northwest edge as a complete constraint.",757+ "Mark the forced mines bordering the open basin.",758+ "Sweep the newly safe cells along the eastern seam.",759+ "Propagate the one-two pattern through the lower field.",760+ "Close the remaining isolated frontier pockets.",761+ "The northern frontier resolves as one constraint wave.",762+ ];763+ const previewBandWidth = Math.min(14, observation.width - 2);764+ const previewBandStart = Math.max(1, Math.floor(observation.width * 0.3));765+ state.latestIntent = intents.at(-1) ?? "Preview complete";766+ const revealed = Array.from({ length: 84 }, (_, index) => ({767+ row: Math.min(observation.height - 2, previewBandStart + Math.floor(index / previewBandWidth)),768+ col: Math.min(observation.width - 2, previewBandStart + (index % previewBandWidth)),769+ }));770+ const flagged: Coordinate[] = [];771+ for (let row = 0; row < observation.height; row += 1) {772+ for (let col = 0; col < observation.width; col += 1) {773+ if (observation.rows[row]?.[col] === "F") flagged.push({ row, col });774+ }775+ }776+ state.events = intents.map((intent, index) => {777+ const batchRevealed = revealed.slice(index * 12, (index + 1) * 12);778+ const flagStart = Math.floor((index * flagged.length) / intents.length);779+ const flagEnd = Math.floor(((index + 1) * flagged.length) / intents.length);780+ return {781+ revision: index + 1,782+ intent,783+ submittedCount: batchRevealed.length + flagEnd - flagStart,784+ revealed: batchRevealed,785+ revealedValues: batchRevealed.map((coordinate) => ({786+ ...coordinate,787+ value: observation.rows[coordinate.row]?.[coordinate.col] ?? "0",788+ })),789+ flagged: flagged.slice(flagStart, flagEnd),790+ unflagged: [],791+ exploded: null,792+ status: "playing",793+ createdAt: new Date().toISOString(),794+ } satisfies MoveEvent;795+ });796+ const initialRows = observation.rows.map((row) => row.split(""));797+ for (const coordinate of [...revealed, ...flagged]) {798+ const row = initialRows[coordinate.row];799+ if (row !== undefined) row[coordinate.col] = "#";800+ }801+ const initialRenderedRows = initialRows.map((row) => row.join(""));802+ const initialRevealedCount = initialRenderedRows.reduce(803+ (total, row) => total + [...row].filter((cell) => cell !== "#" && cell !== "F").length,804+ 0,805+ );806+ state.initialObservation = {807+ ...observation,808+ revision: 0,809+ status: "playing",810+ rows: initialRenderedRows,811+ revealed_count: initialRevealedCount,812+ flagged_count: 0,813+ remaining_safe_cells: observation.width * observation.height - observation.mine_count - initialRevealedCount,814+ };815+ state.displayRevision = observation.revision;816+ for (const coordinate of revealed) flashUntil.set(`${coordinate.row}:${coordinate.col}`, 1800);817+ void refreshReplayLink();818+}819+820+let replayLinkKey = "";821+let replayLinkGeneration = 0;822+async function refreshReplayLink(): Promise<void> {823+ const start = state.initialObservation;824+ const live = state.observation;825+ if (start === null || live === null || state.previewUrl === "") {826+ replayLink.setAttribute("aria-disabled", "true");827+ replayLink.textContent = "Preparing replay link…";828+ return;829+ }830+ const key = `${live.game_id}:${live.revision}:${state.events.length}:${state.previewUrl}`;831+ if (key === replayLinkKey) return;832+ replayLinkKey = key;833+ const generation = ++replayLinkGeneration;834+ replayLink.setAttribute("aria-disabled", "true");835+ replayLink.textContent = "Encoding replay…";836+ const payload: ReplayLinkPayload = {837+ v: 1,838+ s: start,839+ t: state.events.map((event) => eventToReplayTurn(event, start.width)),840+ };841+ const encoded = await encodeReplayLink(payload);842+ if (generation !== replayLinkGeneration) return;843+ const target = new URL(state.previewUrl);844+ target.search = "";845+ target.hash = `replay=${encoded}`;846+ replayLink.href = target.toString();847+ replayLink.title = `Open encoded replay link (${Math.ceil(target.toString().length / 1024).toLocaleString()} KB)`;848+ replayLink.textContent = "Open replay link";849+ replayLink.setAttribute("aria-disabled", "false");850+}851+852+function toggleReplay(): void {853+ const maximum = state.observation?.revision ?? 0;854+ if (maximum === 0) return;855+ if (state.replaying) {856+ state.replaying = false;857+ return;858+ }859+ if (state.displayRevision >= maximum) state.displayRevision = 0;860+ state.followingLive = false;861+ state.replaying = true;862+ state.replayAccumulatorMs = 0;863+}864+865+pauseButton.addEventListener("click", toggleReplay);866+faceButton.addEventListener("click", toggleReplay);867+868+replaySlider.addEventListener("input", () => selectRevision(Number(replaySlider.value)));869+870+function setZoom(zoom: number): void {871+ const horizontalCenter = (boardFrame.scrollLeft + boardFrame.clientWidth / 2) / Math.max(1, boardFrame.scrollWidth);872+ const verticalCenter = (boardFrame.scrollTop + boardFrame.clientHeight / 2) / Math.max(1, boardFrame.scrollHeight);873+ state.zoom = Math.max(1, Math.min(4, zoom));874+ const fittedSize = Math.max(1, Math.min(boardFrame.clientWidth, boardFrame.clientHeight));875+ configureCanvas(fittedSize * state.zoom);876+ const zoomLabel = `${Math.round(state.zoom * 100)}%`;877+ zoomSlider.title = `Board zoom: ${zoomLabel}`;878+ zoomSlider.setAttribute("aria-valuetext", zoomLabel);879+ requestAnimationFrame(() => {880+ boardFrame.scrollLeft = horizontalCenter * boardFrame.scrollWidth - boardFrame.clientWidth / 2;881+ boardFrame.scrollTop = verticalCenter * boardFrame.scrollHeight - boardFrame.clientHeight / 2;882+ });883+}884+885+zoomSlider.addEventListener("input", () => setZoom(Number(zoomSlider.value)));886+requestAnimationFrame(() => setZoom(state.zoom));887+let pendingResizeFrame = 0;888+function scheduleCanvasResize(): void {889+ cancelAnimationFrame(pendingResizeFrame);890+ pendingResizeFrame = requestAnimationFrame(() => setZoom(state.zoom));891+}892+window.addEventListener("resize", scheduleCanvasResize);893+new ResizeObserver(scheduleCanvasResize).observe(boardFrame);894+895+const speeds = [0.5, 1, 2, 4];896+speedButton.addEventListener("click", () => {897+ const currentIndex = speeds.indexOf(state.speed);898+ state.speed = speeds[(currentIndex + 1) % speeds.length] ?? 1;899+ speedButton.textContent = `Speed ${state.speed}×`;900+});901+902+fitButton.addEventListener("click", () => {903+ zoomSlider.value = "1";904+ setZoom(1);905+});906+907+compareButton.addEventListener("click", () => {908+ state.comparisonEnabled = !state.comparisonEnabled;909+ state.comparisonAfter = true;910+ state.comparisonAccumulatorMs = 0;911+ compareButton.textContent = `Compare: ${state.comparisonEnabled ? "On" : "Off"}`;912+});913+914+replayLink.addEventListener("click", (event) => {915+ if (replayLink.getAttribute("aria-disabled") === "true") {916+ event.preventDefault();917+ return;918+ }919+ if (hostApp?.getHostCapabilities()?.openLinks !== undefined) {920+ event.preventDefault();921+ void hostApp.openLink({ url: replayLink.href });922+ }923+});924+925+let previousTimestamp = performance.now();926+function frame(timestamp: number): void {927+ update(Math.min(50, timestamp - previousTimestamp));928+ previousTimestamp = timestamp;929+ render();930+ requestAnimationFrame(frame);931+}932+933+window.render_game_to_text = () => {934+ const observation = observationAt(visibleRevision());935+ const cellSize = observation === null ? 0 : canvasCssSize / Math.max(observation.width, observation.height);936+ return JSON.stringify({937+ mode: state.connection,938+ replaying: state.replaying,939+ display_revision: state.displayRevision,940+ live_revision: state.observation?.revision ?? 0,941+ speed: state.speed,942+ zoom: state.zoom,943+ comparison: {944+ enabled: state.comparisonEnabled,945+ phase: comparisonPhase.textContent,946+ },947+ board_render: {948+ css_size: canvasCssSize,949+ backing_size: canvas.width,950+ render_scale: Number(canvasRenderScale.toFixed(2)),951+ cell_size: Number(cellSize.toFixed(2)),952+ detail: observation === null ? "waiting" : detailForCellSize(cellSize),953+ },954+ coordinate_system: "row 0 / col 0 is top-left; rows increase down and columns right",955+ observation:956+ observation === null957+ ? null958+ : {959+ game_id: observation.game_id,960+ status: observation.status,961+ revision: observation.revision,962+ width: observation.width,963+ height: observation.height,964+ mines: observation.mine_count,965+ flags: observation.flagged_count,966+ revealed: observation.revealed_count,967+ remaining_safe_cells: observation.remaining_safe_cells,968+ },969+ latest_intent: state.latestIntent,970+ recent_batches: state.events.slice(-5).map((event) => ({971+ revision: event.revision,972+ revealed: event.revealed.length,973+ flagged: event.flagged.length,974+ status: event.status,975+ })),976+ error: state.error,977+ });978+};979+980+window.advanceTime = (milliseconds: number) => {981+ const frames = Math.max(1, Math.round(milliseconds / (1000 / 60)));982+ for (let index = 0; index < frames; index += 1) update(1000 / 60);983+ render();984+};985+986+declare global {987+ interface Window {988+ render_game_to_text: () => string;989+ advanceTime: (milliseconds: number) => void;990+ }991+}992+993+async function initialize(): Promise<void> {994+ const linkedReplay = await readReplayLink();995+ if (linkedReplay !== null) {996+ beginLinkedReplay(linkedReplay.start, linkedReplay.turns);997+ void refreshReplayLink();998+ return;999+ }1000+ if (window.top === window) {1001+ beginPreview();1002+ return;1003+ }1004+1005+ const app = new App({ name: "MCPSweeper spectator", version: "0.1.0" }, {}, { autoResize: false, strict: true });1006+ hostApp = app;1007+ let gameId = "";1008+ let lastRevision = 0;1009+ app.ontoolresult = (result) => {1010+ const value = structured(result.structuredContent);1011+ if (value?.game_id !== undefined) gameId = value.game_id;1012+ if (isVisualObservation(value?.observation)) gameId = value.observation.game_id;1013+ consumeResult(result.structuredContent);1014+ };1015+ void app1016+ .connect()1017+ .then(async () => {1018+ await app.sendSizeChanged({ height: 640 });1019+ state.connection = "live";1020+ while (true) {1021+ await new Promise((resolve) => setTimeout(resolve, 650));1022+ if (gameId === "") continue;1023+ try {1024+ const result = await app.callServerTool({1025+ name: "watch_game",1026+ arguments: { game_id: gameId, since_revision: lastRevision },1027+ });1028+ const value = structured(result.structuredContent);1029+ consumeResult(result.structuredContent);1030+ if (isVisualObservation(value?.observation)) lastRevision = value.observation.revision;1031+ } catch (error) {1032+ state.connection = "error";1033+ state.error = error instanceof Error ? error.message : "Polling failed";1034+ }1035+ }1036+ })1037+ .catch((error: unknown) => {1038+ state.connection = "error";1039+ state.error = error instanceof Error ? error.message : "Unable to connect to host";1040+ });1041+}1042+1043+void initialize();1044+requestAnimationFrame(frame);@@ -0,0 +1,106 @@
1+import { describe, expect, it } from "vitest";2+import { applyBatch, createNoGuessGame, toAgentPosition, toObservation, type GameState } from "../src/game.ts";3+4+describe("MCPSweeper engine", () => {5+ it("generates a certified board with a visible opening", () => {6+ const state = createNoGuessGame({7+ id: "74bd6db1-64e0-46fa-93c6-ed8b41957c67",8+ seed: "engine-test",9+ width: 16,10+ height: 16,11+ density: 0.1,12+ maxAttempts: 500,13+ });14+ const observation = toObservation(state);15+ expect(state.certifiedNoGuess).toBe(true);16+ expect(observation.revealed_count).toBeGreaterThan(0);17+ expect(observation.rows).toHaveLength(16);18+ });19+20+ it("loses when a hidden mine is revealed", () => {21+ const state = createNoGuessGame({22+ id: "ec2a5195-6c00-48a8-b338-dff59e54a151",23+ seed: "detonation-test",24+ width: 16,25+ height: 16,26+ density: 0.1,27+ maxAttempts: 500,28+ });29+ const mineIndex = state.mines.findIndex((value) => value === 1);30+ const event = applyBatch(state, {31+ reveal: [{ row: Math.floor(mineIndex / state.width), col: mineIndex % state.width }],32+ flag: [],33+ unflag: [],34+ intent: "Deliberate test detonation",35+ });36+ expect(event.status).toBe("lost");37+ expect(event.exploded).not.toBeNull();38+ expect(event.revealedValues).toEqual([]);39+ expect(state.revision).toBe(1);40+ });41+42+ it("records clue values needed to replay a batch", () => {43+ const state = createNoGuessGame({44+ id: "f2e03895-c572-4d4b-bcaf-718ff469b18d",45+ seed: "replay-test",46+ width: 16,47+ height: 16,48+ density: 0.1,49+ maxAttempts: 500,50+ });51+ const hiddenSafe = state.revealed.findIndex((value, index) => value === 0 && state.mines[index] === 0);52+ const event = applyBatch(state, {53+ reveal: [{ row: Math.floor(hiddenSafe / state.width), col: hiddenSafe % state.width }],54+ flag: [],55+ unflag: [],56+ intent: "Replay metadata test",57+ });58+ expect(event.revealedValues.length).toBeGreaterThan(0);59+ expect(event.revealedValues.every((cell) => /^[0-8]$/.test(cell.value))).toBe(true);60+ });61+62+ it("counts submitted tiles rather than flood-revealed tiles", () => {63+ const state: GameState = {64+ id: "9ba1f45f-e2f0-4292-94da-52146c89579c",65+ seed: "submitted-count-test",66+ width: 3,67+ height: 3,68+ mines: new Uint8Array(9),69+ revealed: new Uint8Array(9),70+ initialRevealed: new Uint8Array(9),71+ flags: new Uint8Array(9),72+ status: "playing",73+ revision: 0,74+ explodedIndex: null,75+ certifiedNoGuess: true,76+ createdAt: "2026-08-12T00:00:00.000Z",77+ updatedAt: "2026-08-12T00:00:00.000Z",78+ };79+80+ const event = applyBatch(state, {81+ reveal: [{ row: 1, col: 1 }],82+ flag: [],83+ unflag: [],84+ intent: "Reveal one empty tile",85+ });86+87+ expect(event.submittedCount).toBe(1);88+ expect(event.revealed).toHaveLength(9);89+ });90+91+ it("encodes the agent frontier more compactly than the visual board", () => {92+ const state = createNoGuessGame({93+ id: "ee3feea4-077c-4a58-9071-92368a92ed7f",94+ seed: "compact-position-test",95+ width: 64,96+ height: 64,97+ density: 0.1,98+ maxAttempts: 500,99+ });100+ const position = toAgentPosition(state);101+ let previous = 0;102+ const frontier = position.frontier_deltas.map((delta) => (previous += delta));103+ expect(frontier).toEqual([...frontier].sort((left, right) => left - right));104+ expect(JSON.stringify(position).length).toBeLessThan(JSON.stringify(toObservation(state)).length);105+ });106+});Not shown: this page already shows 6000 lines of diff.
Not shown: this page already shows 6000 lines of diff.
Not shown: this page already shows 6000 lines of diff.
Not shown: this page already shows 6000 lines of diff.
Not shown: this page already shows 6000 lines of diff.
Not shown: this page already shows 6000 lines of diff.
Not shown: this page already shows 6000 lines of diff.