diff --git a/chat/package.json b/chat/package.json index 6fb0edad..d9f3bd6e 100644 --- a/chat/package.json +++ b/chat/package.json @@ -1,9 +1,9 @@ { "name": "@patchwork/chat", - "version": "0.4.0", + "version": "0.5.0", "description": "chitterchatter: chat with emoji reactions, presence/typing indicators, image paste, voice notes, and message replies.", "type": "module", - "main": "./main.js", + "main": "./dist/index.js", "exports": { ".": "./dist/index.js" }, @@ -25,8 +25,8 @@ "dependencies": { "@automerge/automerge": "^3.2.1", "@automerge/automerge-repo": "^2.5.0", - "@chee/patchwork-llm": "^0.2.1", "@chee/patchwork-transcript": "^0.1.0", + "@grjte/patchwork-worker": "^0.0.2", "@inkandswitch/patchwork-elements": "^6.0.2", "@inkandswitch/patchwork-filesystem": "^0.2.9", "cute.txt": "^0.0.6", diff --git a/chat/pnpm-lock.yaml b/chat/pnpm-lock.yaml index 7d4289fa..2df59cc2 100644 --- a/chat/pnpm-lock.yaml +++ b/chat/pnpm-lock.yaml @@ -31,38 +31,38 @@ importers: dependencies: '@automerge/automerge': specifier: ^3.2.1 - version: 3.4.0 + version: 3.5.0 '@automerge/automerge-repo': specifier: ^2.5.0 version: 2.5.6 - '@chee/patchwork-llm': - specifier: ^0.2.1 - version: 0.2.1(@automerge/automerge-repo@2.5.6) '@chee/patchwork-transcript': specifier: ^0.1.0 version: 0.1.0(@automerge/automerge-repo@2.5.6) + '@grjte/patchwork-worker': + specifier: ^0.0.2 + version: 0.0.2(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0)) '@inkandswitch/patchwork-elements': specifier: ^6.0.2 - version: 6.0.2(@automerge/automerge-repo@2.5.6)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.4.0))(@inkandswitch/patchwork-plugins@1.2.5(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.4.0)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.4.0)))(@inkandswitch/patchwork-providers@0.5.2(@automerge/automerge-repo@2.5.6))(solid-js@1.9.14) + version: 6.0.2(@automerge/automerge-repo@2.5.6)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0))(@inkandswitch/patchwork-plugins@1.2.5(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0)))(@inkandswitch/patchwork-providers@0.5.2(@automerge/automerge-repo@2.5.6))(solid-js@1.9.15) '@inkandswitch/patchwork-filesystem': specifier: ^0.2.9 - version: 0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.4.0) + version: 0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0) cute.txt: specifier: ^0.0.6 - version: 0.0.6(@automerge/automerge@3.4.0)(@codemirror/state@6.7.5)(@codemirror/view@6.43.12)(solid-js@1.9.14) + version: 0.0.6(@automerge/automerge@3.5.0)(@codemirror/state@6.7.6)(@codemirror/view@6.43.13)(solid-js@1.9.15) solid-automerge: specifier: ^2.0.1 - version: 2.0.1(@automerge/automerge-repo@2.5.6)(solid-js@1.9.14) + version: 2.0.1(@automerge/automerge-repo@2.5.6)(solid-js@1.9.15) solid-js: specifier: ^1.9.11 - version: 1.9.14 + version: 1.9.15 devDependencies: '@chee/patchwork-bundles': specifier: ^0.2.0 - version: 0.2.0(@automerge/automerge@3.4.0) + version: 0.2.0(@automerge/automerge@3.5.0) '@inkandswitch/patchwork-bootloader': specifier: ^0.7.2 - version: 0.7.2(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.4.0) + version: 0.7.2(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0) happy-dom: specifier: ^15.11.7 version: 15.11.7 @@ -77,7 +77,7 @@ importers: version: 3.5.2(vite@5.4.21(@types/node@20.19.43)) vite-plugin-solid: specifier: ^2.10.0 - version: 2.11.14(solid-js@1.9.14)(vite@5.4.21(@types/node@20.19.43)) + version: 2.11.14(solid-js@1.9.15)(vite@5.4.21(@types/node@20.19.43)) vitest: specifier: ^3.2.4 version: 3.2.7(@types/debug@4.1.13)(@types/node@20.19.43)(happy-dom@15.11.7) @@ -123,8 +123,8 @@ packages: '@automerge/automerge@3.3.2': resolution: {integrity: sha512-9vCdCL7pdQwUra66SBxPVHr+/t9epXKni9KDeak2rNBFMzABVh2u6gSpcwxi3jkR5qr047jVqZv9DlrOfVxLFw==} - '@automerge/automerge@3.4.0': - resolution: {integrity: sha512-THmghtTNGGt2xsI0pM3o1i3PM8oZKcYFgOj25FOzW7l6e94SQOivNtCwy6xc0I8hVJsQSSotoBNs+yk/9hM2dg==} + '@automerge/automerge@3.5.0': + resolution: {integrity: sha512-ejbqJWfXWM2QbTcUK/5ugYiQiyOP7LYVAp/RuSvthCc07oek0FClMxEuouuhjrLjVPDSI34RP1ftGc45PF7WRA==} '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} @@ -184,8 +184,8 @@ packages: resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} engines: {node: '>=6.9.0'} - '@babel/parser@7.29.8': - resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} + '@babel/parser@7.29.9': + resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==} engines: {node: '>=6.0.0'} hasBin: true @@ -240,11 +240,6 @@ packages: '@chee/patchwork-bundles@0.2.0': resolution: {integrity: sha512-Hc8YhdYM1s/6ij08wMFFBa0+LUyPyW7EBh8yrIsWtBzurWcsHnCtdF7ObZtWVqslkLjlgLqcvpDHYpWhfg6I0g==} - '@chee/patchwork-llm@0.2.1': - resolution: {integrity: sha512-9XZj8AiGqvHZrtFXdLth+2DwK3bBXjZWtbdxWJmZXv/5hxuIB6sV92OZgtuU6vPy4yjN1RY0+OMHJjS2VOnqTw==} - peerDependencies: - '@automerge/automerge-repo': ^2.5.6 - '@chee/patchwork-transcript@0.1.0': resolution: {integrity: sha512-yBBBAS7OP+3dlUt11LqGW95VhIbZDlAJUN53xUNb99Lf4XFwGHNPkSpLpTM9chbWcDciaMr3h4ZNzfRjtXfqYw==} peerDependencies: @@ -256,11 +251,11 @@ packages: '@codemirror/language@6.12.4': resolution: {integrity: sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==} - '@codemirror/state@6.7.5': - resolution: {integrity: sha512-QjLbZmY1Au3JiRrDVYFLRD0BZ3SOKS9pR3yjIkd7u27YY8TFD9/Q9fhPnLV5l1mHFSo3hHU/N31vpwEJOx4owQ==} + '@codemirror/state@6.7.6': + resolution: {integrity: sha512-kAz+AncRtKuIknedxT1bq4XwXv4UowhbkHU1myPrtVb/jZtImWuV5BXzv5vK6i3kYACsdiZiQKFQQ5Mq7elW8w==} - '@codemirror/view@6.43.12': - resolution: {integrity: sha512-Nv0vxQ19NAqvB/c2pFzjIzFlzzJl7jmdtNkwOwGbn0Ks9mFAzibvumz7cQem5cRsFA2cEw2fg+uHZGbcHupLQQ==} + '@codemirror/view@6.43.13': + resolution: {integrity: sha512-sihaFrUzAsYBQsL9J2t69y8nfMQGwcYmggAZsk+kjPbjYZMyuf2hU8tUNTZ+P+isb6XRr8JE22TZlJxBoVdH1A==} '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} @@ -400,6 +395,9 @@ packages: cpu: [x64] os: [win32] + '@grjte/patchwork-worker@0.0.2': + resolution: {integrity: sha512-a7vJ0O2ZNyOUzCyK32PwaGJbVxUFqE/lHhRGadR3lA1XWpbIJ059B4qDpthn8R4eUzGPAIqFy2k0lawaLsKktA==} + '@inkandswitch/patchwork-bootloader@0.7.2': resolution: {integrity: sha512-b5ki9fodXB7uEzvBvpZ0uAkaM7CwwtAdD596XKZJY+TjaSnYsWnx0IP0ewz9rL6ASU282ItyCB8917PKxr8ckA==} peerDependencies: @@ -441,6 +439,13 @@ packages: '@automerge/automerge': '*' '@automerge/automerge-repo': '*' + '@inkandswitch/patchwork-plugins@0.0.11': + resolution: {integrity: sha512-ElwDEixpZN64gdoE7EU8QFz4WzvyJt6j4Zn0pTezsW6SzGlx+Rc6Ox+7POK7gqEzQEcl3Iqo8+QdYRucYllRFw==} + peerDependencies: + '@automerge/automerge': '*' + '@automerge/automerge-repo': '*' + '@inkandswitch/patchwork-filesystem': ^0.0.8 + '@inkandswitch/patchwork-plugins@1.2.5': resolution: {integrity: sha512-4R/N6W4v4Xyfp47RaG7WesN0jUdNsQmZshJt9pHfsTbAFEbvNvgnmC2JF9mzSL5CagCy1njACIoasOVvkv/A/A==} peerDependencies: @@ -468,8 +473,8 @@ packages: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} @@ -500,141 +505,141 @@ packages: resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} - '@rollup/rollup-android-arm-eabi@4.62.4': - resolution: {integrity: sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==} + '@rollup/rollup-android-arm-eabi@4.63.4': + resolution: {integrity: sha512-I+BSHzTAhKN2n7ZwGZsegGcZjDpLqFOMAtJz/u6uFGe0pUFbq56dEHjqJV/ZUdRJtNXNxA+hREUatZBvMR3Oiw==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.62.4': - resolution: {integrity: sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==} + '@rollup/rollup-android-arm64@4.63.4': + resolution: {integrity: sha512-pu3BdjS2LtEzRu2elmGzS3fIeWSZy4BMDIaLNwjorO76+k2d0LMluijhsDx3KQyQBQ/lLUZCQA9/s6csvUfuhw==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.62.4': - resolution: {integrity: sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==} + '@rollup/rollup-darwin-arm64@4.63.4': + resolution: {integrity: sha512-xfSrj9MHnWK9GaSqT9U0ImHtH/N8WZlHLx4cZHiuLcqs640hvZ3hLPd5UR2AZS57FaE8HrRUSpltbZdWRxHiDA==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.62.4': - resolution: {integrity: sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==} + '@rollup/rollup-darwin-x64@4.63.4': + resolution: {integrity: sha512-bqU99PLJb/dqb3S0GIMdeuyAEETSUgZBoqXYd3Sd+WCsV+MmPhnN6JrotWyir31+QgH7EvvE5/mwGJlEoci8Fw==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.62.4': - resolution: {integrity: sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==} + '@rollup/rollup-freebsd-arm64@4.63.4': + resolution: {integrity: sha512-JinsFZ5G40oXQb+sUuiA5x689vhr6dDYK0H0NL+rwKdL6CqnmYN8PE4ZwfRSoIjrCxqTQG/SLfTtSvHeGxoVlw==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.62.4': - resolution: {integrity: sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==} + '@rollup/rollup-freebsd-x64@4.63.4': + resolution: {integrity: sha512-GAdA4UxpiNm27cLHr2GqXBpAD0x9FqwYBY7/YSP0Ss0/PNi4k8gbviqpIpYbVSRBaS2ZcegXEzgTQMbRNCwxCw==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.62.4': - resolution: {integrity: sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==} + '@rollup/rollup-linux-arm-gnueabihf@4.63.4': + resolution: {integrity: sha512-qDd6NoA1znaLjp4jR5U/KWCdLAKDJNB8W9ChbbDaKbo0xA+Atln5HK6LFCZ4oJQpemtRZA288DCirFRjrspptw==} cpu: [arm] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.62.4': - resolution: {integrity: sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==} + '@rollup/rollup-linux-arm-musleabihf@4.63.4': + resolution: {integrity: sha512-WtB5Tz5KTNINb8ZA+8sQ7bmjuS1JrRT7YverYIhUGdWWDlpzVWmIwuZE+jidkEXUn1l0zrEkaIMa8dHF3NGcsA==} cpu: [arm] os: [linux] libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.62.4': - resolution: {integrity: sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==} + '@rollup/rollup-linux-arm64-gnu@4.63.4': + resolution: {integrity: sha512-VcQ3L1tjnkKzWjryAVaFhHEWcqOfICX9uxVVoDzm2t0DpgKRHd2zOpVrJc0xsWeBZcBFyYROCIBdyR/fS174pg==} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.62.4': - resolution: {integrity: sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==} + '@rollup/rollup-linux-arm64-musl@4.63.4': + resolution: {integrity: sha512-6+ZQX6P5s0cMDN2Ypb8Lbm2+/sZYmZjdaYny992ujUU9UKi/4CWoJWsl1pNvjWJHNHGK51m+jKGLlh1ylb2ifQ==} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.62.4': - resolution: {integrity: sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==} + '@rollup/rollup-linux-loong64-gnu@4.63.4': + resolution: {integrity: sha512-D72ZnvkFkBXOfzMMQLcwfPLyGkKb7HZ9/mf97B7v6/P5Lbv4oFOtSY/uHbS8lH6uKUOxoKiuokdb50XZSzzbJw==} cpu: [loong64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-loong64-musl@4.62.4': - resolution: {integrity: sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==} + '@rollup/rollup-linux-loong64-musl@4.63.4': + resolution: {integrity: sha512-piU6BxeqA3O9KSu3kRCIQQtNqFFaTu21SEV4FwaRZowpnj3bLaWPZHw+xFqCs0XlJ+aOH3PTRWGoglH+mKA/OA==} cpu: [loong64] os: [linux] libc: [musl] - '@rollup/rollup-linux-ppc64-gnu@4.62.4': - resolution: {integrity: sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==} + '@rollup/rollup-linux-ppc64-gnu@4.63.4': + resolution: {integrity: sha512-/5PGpHwqt2EEEOUs1XwzubE/ucr0dWDQ+to3zqi4Ds7EWpwtQ79wXc4JBoxqj/OwpawTsKWzJxHfSuBOq3DrWA==} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.62.4': - resolution: {integrity: sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==} + '@rollup/rollup-linux-ppc64-musl@4.63.4': + resolution: {integrity: sha512-cX3beZDLWt7G2oJF+nhChiT+qtaihs+S2xi7ziGmVB+2pwPng6D0Ed0HmElQOgv2UsUmSJJLGwpBao/3TDx3VA==} cpu: [ppc64] os: [linux] libc: [musl] - '@rollup/rollup-linux-riscv64-gnu@4.62.4': - resolution: {integrity: sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==} + '@rollup/rollup-linux-riscv64-gnu@4.63.4': + resolution: {integrity: sha512-1uz2mGWHyptR7DgHHrlbdRAjXK7v7elGZ9lMja910/RP+ZYbX6xAmCiU9UZSX4hqmgtHMv6lr5l3kq1HIOpcag==} cpu: [riscv64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.62.4': - resolution: {integrity: sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==} + '@rollup/rollup-linux-riscv64-musl@4.63.4': + resolution: {integrity: sha512-nLS8topojxyz7SRpKR2IODRpQ0XPZ+xaOXvT3+hqK/Uy8Lo5HFgkkIBiIrCu5tL5YqzTvgovGw55PwpahTAGig==} cpu: [riscv64] os: [linux] libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.62.4': - resolution: {integrity: sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==} + '@rollup/rollup-linux-s390x-gnu@4.63.4': + resolution: {integrity: sha512-gs7DRKotr3l3q+jGPQBjH0ng1FjlEDm5ueQrkw5JtQvtLyEIcLASqAEaor56BhkKRzk+IcQzrcanBdb/bBQn8g==} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.62.4': - resolution: {integrity: sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==} + '@rollup/rollup-linux-x64-gnu@4.63.4': + resolution: {integrity: sha512-791ET7W17NnScOZM7h4dX5hYspxE28htPFsb1awY/NRR8+PRNkS53e475rDdxXXDrP+kwnCcNWg9CX5ztn/Aqw==} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.62.4': - resolution: {integrity: sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==} + '@rollup/rollup-linux-x64-musl@4.63.4': + resolution: {integrity: sha512-iwZQRcmj7g88g3tzefIrQY7qvmuA/cfYwhrDtTBhsmukO4U2huVO5W+86XacUMRvdSFVAc6kZUZy21JaRwiB9w==} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.62.4': - resolution: {integrity: sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==} + '@rollup/rollup-openbsd-x64@4.63.4': + resolution: {integrity: sha512-dVHFp9gRWrdTpnqQuGfCwd7hOQDatK1VCP2iWhLY/cGrOQs/ucFzJ6A5SRqbXX12ZDI8EUuejSM5kwg+ja7Png==} cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.62.4': - resolution: {integrity: sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==} + '@rollup/rollup-openharmony-arm64@4.63.4': + resolution: {integrity: sha512-t3NlauOW6gxZVVFcBEnO62Cb4wbyDFL416gTg1uFI/2tgqYQlf69FbSE115Ajre9I+c26Lk4mcmdFUsS/DGifQ==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.62.4': - resolution: {integrity: sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==} + '@rollup/rollup-win32-arm64-msvc@4.63.4': + resolution: {integrity: sha512-xWuIaSye5FWZF8+UYtVEcHtRJDN5kN9Kfgxx3Kq8XIov9KSKbc1fiqQCm90SKrgQbUXZelbnUhnlUJmfSE7P9A==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.62.4': - resolution: {integrity: sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==} + '@rollup/rollup-win32-ia32-msvc@4.63.4': + resolution: {integrity: sha512-9ALJJUOg/ZflMJepVo2PlgsGxSaxN7SQ4Z8GoZfVlarWr6r3rkHUNsd/zAio7p4YMtChSMXPionxej4Hkf6CXQ==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.62.4': - resolution: {integrity: sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==} + '@rollup/rollup-win32-x64-gnu@4.63.4': + resolution: {integrity: sha512-blj9z5qx/Pv4WU0W1NMFDB97e0JH5ed+aZGywW8WCvp/NhWX/4PFAq5uu6Q0AebNn+Vo6KzUYDT++JzTT5ojlQ==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.62.4': - resolution: {integrity: sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==} + '@rollup/rollup-win32-x64-msvc@4.63.4': + resolution: {integrity: sha512-Erx822VRBwLa124shbj+wNXe//BOgMEctDV0m1aqTQdNO1S69DgNUCFKC1RCeZfixs1J31l6igk1ziyXErbigQ==} cpu: [x64] os: [win32] @@ -714,16 +719,16 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - babel-plugin-jsx-dom-expressions@0.40.7: - resolution: {integrity: sha512-/O6JWUmjv03OI9lL2ry9bUjpD5S3PclM55RRJEyCdcFZ5W2SEA/59d+l2hNsk3gI6kiWRdRPdOtqZmsQzFN1pQ==} + babel-plugin-jsx-dom-expressions@0.40.10: + resolution: {integrity: sha512-lxve6Y02YiZTldB7efKpnbf1BH00XCFZNYYW235jSGsYaJNFtHrYlKV6/O+miHbjqpIr9FTe5+0no4hofAMbfA==} peerDependencies: '@babel/core': ^7.20.12 - babel-preset-solid@1.9.12: - resolution: {integrity: sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg==} + babel-preset-solid@1.9.15: + resolution: {integrity: sha512-GBmg1OiPb+OwcH51XbDAKPtvrPfQW7rCJTJxcp8+yhtWwN+kqnbEJk2SgVybd+uhTxTKAvjaFyiQSr/eUZBwzg==} peerDependencies: '@babel/core': ^7.0.0 - solid-js: ^1.9.12 + solid-js: ^1.9.15 peerDependenciesMeta: solid-js: optional: true @@ -734,13 +739,13 @@ packages: base-x@5.0.1: resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} - baseline-browser-mapping@2.11.12: - resolution: {integrity: sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==} + baseline-browser-mapping@2.11.25: + resolution: {integrity: sha512-gMmEShwwq7FJqMwvfRwvCl00v4kN+KOfJqXn+f4nrufak5gNHJOksd/60Dvjuz7sI8Y5WiSFBa8FEYr+zoyqCw==} engines: {node: '>=6.0.0'} hasBin: true - browserslist@4.28.7: - resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==} + browserslist@4.29.0: + resolution: {integrity: sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -763,15 +768,15 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} - caniuse-lite@1.0.30001807: - resolution: {integrity: sha512-daRXJ9EB/rdRgu7kV+TTl1YUKtlsMWblPl2sLnpg9DZae16QCegol6A1SmCE31Lm9mXC1sRWGt/krouH+/dl7Q==} + caniuse-lite@1.0.30001810: + resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==} cbor-extract@2.2.2: resolution: {integrity: sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng==} hasBin: true - cbor-x@1.6.5: - resolution: {integrity: sha512-yO64CxnSh6kp+pHNRK9IfwnMvCB+c8HvmUjQY/9l9YRF0/cAPka/tUHLwS64QqUpFCq3/OtbKziVJYXH2EaRig==} + cbor-x@1.6.6: + resolution: {integrity: sha512-8QiD9PGOxyQHo7s2pzwTBH6lTjqekxPdl9Aq6fXvZgCuCJHOht1puDEA/fTr6mciB76c+M+Gi0qT2i1a4pm4Wg==} chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} @@ -824,8 +829,8 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - electron-to-chromium@1.5.402: - resolution: {integrity: sha512-/oOpMaPT6Yg+6/1XQhyIPlzgj7Ye9zf+nNM2Uh6OcE2G2oNptWazFa+qB2Pdqqbsc9KnIDzgAntoYN0dbwOXwA==} + electron-to-chromium@1.5.438: + resolution: {integrity: sha512-AN9xMU1hJiT65LkCUPL1DZm5TCbOPb2Qsm5pYwFLEXp6/qj9SdT4yMiqs7Qqstwvkn1zF7l36SRGt+s6XcJ0FA==} entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} @@ -838,8 +843,8 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - es-module-lexer@2.3.1: - resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} @@ -929,8 +934,8 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.17: - resolution: {integrity: sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==} + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -938,8 +943,8 @@ packages: resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} hasBin: true - node-releases@2.0.53: - resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==} + node-releases@2.0.57: + resolution: {integrity: sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==} engines: {node: '>=18'} parse5@7.3.0: @@ -955,20 +960,20 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@4.0.5: - resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} engines: {node: '>=12'} - postcss@8.5.26: - resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} engines: {node: ^10 || ^12 || >=14} resolve.exports@2.0.3: resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} engines: {node: '>=10'} - rollup@4.62.4: - resolution: {integrity: sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==} + rollup@4.63.4: + resolution: {integrity: sha512-4U0liVayNIoLp3GFl1FcI8561WepLnZ1rqfraGh7S9B3Ur5F9S283y8Futii7RUU2C/97tOBmBy7nYvhoiOpbQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -995,8 +1000,8 @@ packages: '@automerge/automerge-repo': ^2.6.0-subduction.34 solid-js: ^1.9.13 - solid-js@1.9.14: - resolution: {integrity: sha512-sAEXC0Kk0S1EDg+8ysEWJDbYhA3RRoEjwuySUGlKIemeo0I5YZfOyumNjNs9Sv3y2nmhD+0rW66ag2HsMuQiGQ==} + solid-js@1.9.15: + resolution: {integrity: sha512-EeiY2xfpZJqPLjXspVEKjAII4yv8NyG//NxZ3IpOFHdUNnnTyL0uJOeS9LWGvA7cFCz5y94cjFwYlmw5Luncsg==} solid-refresh@0.6.3: resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==} @@ -1041,8 +1046,8 @@ packages: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} - tinyspy@4.0.4: - resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + tinyspy@4.0.6: + resolution: {integrity: sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==} engines: {node: '>=14.0.0'} typescript@5.9.3: @@ -1057,14 +1062,14 @@ packages: resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} engines: {node: '>=18.12.0'} - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + update-browserslist-db@1.3.3: + resolution: {integrity: sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' - uuid@14.0.1: - resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==} + uuid@14.0.2: + resolution: {integrity: sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==} hasBin: true uuid@9.0.1: @@ -1178,8 +1183,8 @@ packages: engines: {node: '>=8'} hasBin: true - ws@8.21.2: - resolution: {integrity: sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw==} + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -1190,8 +1195,8 @@ packages: utf-8-validate: optional: true - xstate@5.32.5: - resolution: {integrity: sha512-ULazi1oe6wGrXl0Frb6otSlkm5HLifbbVTkMk5kkSKqz4TkxJaVpnl6jOJwKeid3ORPxYyZQgNLUSYX9q65SIA==} + xstate@5.33.2: + resolution: {integrity: sha512-8tC7yXgeCvpT8gKEeEje6ikJJG1wpnoLiXe+HfECW8m10ubMd3QxKOwWA8KxPJVBcLwfdRoMKYxIBGYKmo37/A==} yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -1218,10 +1223,10 @@ snapshots: '@automerge/automerge-repo-network-websocket@2.6.0-subduction.48': dependencies: '@automerge/automerge-repo': 2.6.0-subduction.48 - cbor-x: 1.6.5 + cbor-x: 1.6.6 debug: 4.4.3 eventemitter3: 5.0.4 - ws: 8.21.2 + ws: 8.21.3 transitivePeerDependencies: - bufferutil - supports-color @@ -1237,14 +1242,14 @@ snapshots: '@automerge/automerge-repo@2.5.6': dependencies: - '@automerge/automerge': 3.4.0 + '@automerge/automerge': 3.5.0 bs58check: 3.0.1 - cbor-x: 1.6.5 + cbor-x: 1.6.6 debug: 4.4.3 eventemitter3: 5.0.4 fast-sha256: 1.3.0 uuid: 9.0.1 - xstate: 5.32.5 + xstate: 5.33.2 transitivePeerDependencies: - supports-color @@ -1253,14 +1258,14 @@ snapshots: '@automerge/automerge': 3.3.0-fragments.2 '@automerge/automerge-subduction': 0.16.0 bs58check: 4.0.0 - cbor-x: 1.6.5 + cbor-x: 1.6.6 debug: 4.4.3 eventemitter3: 5.0.4 fast-sha256: 1.3.0 - isomorphic-ws: 5.0.0(ws@8.21.2) - uuid: 14.0.1 - ws: 8.21.2 - xstate: 5.32.5 + isomorphic-ws: 5.0.0(ws@8.21.3) + uuid: 14.0.2 + ws: 8.21.3 + xstate: 5.33.2 transitivePeerDependencies: - bufferutil - supports-color @@ -1271,14 +1276,14 @@ snapshots: '@automerge/automerge': 3.3.2 '@automerge/automerge-subduction': 0.16.1 bs58check: 4.0.0 - cbor-x: 1.6.5 + cbor-x: 1.6.6 debug: 4.4.3 eventemitter3: 5.0.4 fast-sha256: 1.3.0 - isomorphic-ws: 5.0.0(ws@8.21.2) - uuid: 14.0.1 - ws: 8.21.2 - xstate: 5.32.5 + isomorphic-ws: 5.0.0(ws@8.21.3) + uuid: 14.0.2 + ws: 8.21.3 + xstate: 5.33.2 transitivePeerDependencies: - bufferutil - supports-color @@ -1292,7 +1297,7 @@ snapshots: '@automerge/automerge@3.3.2': {} - '@automerge/automerge@3.4.0': {} + '@automerge/automerge@3.5.0': {} '@babel/code-frame@7.29.7': dependencies: @@ -1309,7 +1314,7 @@ snapshots: '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) '@babel/helpers': 7.29.7 - '@babel/parser': 7.29.8 + '@babel/parser': 7.29.9 '@babel/template': 7.29.7 '@babel/traverse': 7.29.8 '@babel/types': 7.29.8 @@ -1324,7 +1329,7 @@ snapshots: '@babel/generator@7.29.8': dependencies: - '@babel/parser': 7.29.8 + '@babel/parser': 7.29.9 '@babel/types': 7.29.8 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 @@ -1334,7 +1339,7 @@ snapshots: dependencies: '@babel/compat-data': 7.29.7 '@babel/helper-validator-option': 7.29.7 - browserslist: 4.28.7 + browserslist: 4.29.0 lru-cache: 5.1.1 semver: 6.3.1 @@ -1373,7 +1378,7 @@ snapshots: '@babel/template': 7.29.7 '@babel/types': 7.29.8 - '@babel/parser@7.29.8': + '@babel/parser@7.29.9': dependencies: '@babel/types': 7.29.8 @@ -1385,7 +1390,7 @@ snapshots: '@babel/template@7.29.7': dependencies: '@babel/code-frame': 7.29.7 - '@babel/parser': 7.29.8 + '@babel/parser': 7.29.9 '@babel/types': 7.29.8 '@babel/traverse@7.29.8': @@ -1393,7 +1398,7 @@ snapshots: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.8 '@babel/helper-globals': 7.29.7 - '@babel/parser': 7.29.8 + '@babel/parser': 7.29.9 '@babel/template': 7.29.7 '@babel/types': 7.29.8 debug: 4.4.3 @@ -1423,12 +1428,12 @@ snapshots: '@cbor-extract/cbor-extract-win32-x64@2.2.2': optional: true - '@chee/patchwork-bundles@0.2.0(@automerge/automerge@3.4.0)': + '@chee/patchwork-bundles@0.2.0(@automerge/automerge@3.5.0)': dependencies: '@automerge/automerge-repo': 2.6.0-subduction.40 - '@inkandswitch/patchwork-bootloader': 0.7.2(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.4.0) - '@inkandswitch/patchwork-filesystem': 0.2.9(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.4.0) - es-module-lexer: 2.3.1 + '@inkandswitch/patchwork-bootloader': 0.7.2(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.5.0) + '@inkandswitch/patchwork-filesystem': 0.2.9(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.5.0) + es-module-lexer: 2.3.2 resolve.exports: 2.0.3 unplugin: 2.3.11 transitivePeerDependencies: @@ -1441,11 +1446,6 @@ snapshots: - supports-color - utf-8-validate - '@chee/patchwork-llm@0.2.1(@automerge/automerge-repo@2.5.6)': - dependencies: - '@automerge/automerge-repo': 2.5.6 - '@inkandswitch/patchwork-providers': 0.3.0(@automerge/automerge-repo@2.5.6) - '@chee/patchwork-transcript@0.1.0(@automerge/automerge-repo@2.5.6)': dependencies: '@automerge/automerge-repo': 2.5.6 @@ -1454,26 +1454,26 @@ snapshots: '@codemirror/commands@6.11.1': dependencies: '@codemirror/language': 6.12.4 - '@codemirror/state': 6.7.5 - '@codemirror/view': 6.43.12 + '@codemirror/state': 6.7.6 + '@codemirror/view': 6.43.13 '@lezer/common': 1.5.2 '@codemirror/language@6.12.4': dependencies: - '@codemirror/state': 6.7.5 - '@codemirror/view': 6.43.12 + '@codemirror/state': 6.7.6 + '@codemirror/view': 6.43.13 '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 '@lezer/lr': 1.4.10 style-mod: 4.1.4 - '@codemirror/state@6.7.5': + '@codemirror/state@6.7.6': dependencies: '@marijn/find-cluster-break': 1.0.4 - '@codemirror/view@6.43.12': + '@codemirror/view@6.43.13': dependencies: - '@codemirror/state': 6.7.5 + '@codemirror/state': 6.7.6 crelt: 1.0.7 style-mod: 4.1.4 w3c-keyname: 2.2.8 @@ -1547,7 +1547,17 @@ snapshots: '@esbuild/win32-x64@0.21.5': optional: true - '@inkandswitch/patchwork-bootloader@0.7.2(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.4.0)': + '@grjte/patchwork-worker@0.0.2(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0))': + dependencies: + '@inkandswitch/patchwork-plugins': 0.0.11(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0)) + '@inkandswitch/patchwork-providers': 0.5.2(@automerge/automerge-repo@2.5.6) + transitivePeerDependencies: + - '@automerge/automerge' + - '@automerge/automerge-repo' + - '@inkandswitch/patchwork-filesystem' + - supports-color + + '@inkandswitch/patchwork-bootloader@0.7.2(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0)': dependencies: '@automerge/automerge-repo-network-broadcastchannel': 2.6.0-subduction.48 '@automerge/automerge-repo-network-messagechannel': 2.6.0-subduction.48 @@ -1556,21 +1566,21 @@ snapshots: '@automerge/automerge-subduction': 0.16.1 '@codemirror/commands': 6.11.1 '@codemirror/language': 6.12.4 - '@codemirror/state': 6.7.5 - '@codemirror/view': 6.43.12 - '@inkandswitch/patchwork-elements': 6.0.2(@automerge/automerge-repo@2.5.6)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.4.0))(@inkandswitch/patchwork-plugins@1.2.5(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.4.0)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.4.0)))(@inkandswitch/patchwork-providers@0.5.2(@automerge/automerge-repo@2.5.6))(solid-js@1.9.14) - '@inkandswitch/patchwork-filesystem': 0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.4.0) - '@inkandswitch/patchwork-plugins': 1.2.5(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.4.0)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.4.0)) + '@codemirror/state': 6.7.6 + '@codemirror/view': 6.43.13 + '@inkandswitch/patchwork-elements': 6.0.2(@automerge/automerge-repo@2.5.6)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0))(@inkandswitch/patchwork-plugins@1.2.5(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0)))(@inkandswitch/patchwork-providers@0.5.2(@automerge/automerge-repo@2.5.6))(solid-js@1.9.15) + '@inkandswitch/patchwork-filesystem': 0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0) + '@inkandswitch/patchwork-plugins': 1.2.5(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0)) '@inkandswitch/patchwork-providers': 0.5.2(@automerge/automerge-repo@2.5.6) '@keyhive/keyhive': 0.1.0-alpha.8 '@types/debug': 4.1.13 debug: 4.4.3 resolve.exports: 2.0.3 service-worker-types: '@types/serviceworker@0.0.153' - solid-js: 1.9.14 + solid-js: 1.9.15 tinyargs: 0.1.4 optionalDependencies: - '@automerge/automerge': 3.4.0 + '@automerge/automerge': 3.5.0 '@automerge/automerge-repo': 2.5.6 transitivePeerDependencies: - '@types/react' @@ -1579,7 +1589,7 @@ snapshots: - supports-color - utf-8-validate - '@inkandswitch/patchwork-bootloader@0.7.2(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.4.0)': + '@inkandswitch/patchwork-bootloader@0.7.2(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.5.0)': dependencies: '@automerge/automerge-repo-network-broadcastchannel': 2.6.0-subduction.48 '@automerge/automerge-repo-network-messagechannel': 2.6.0-subduction.48 @@ -1588,21 +1598,21 @@ snapshots: '@automerge/automerge-subduction': 0.16.1 '@codemirror/commands': 6.11.1 '@codemirror/language': 6.12.4 - '@codemirror/state': 6.7.5 - '@codemirror/view': 6.43.12 - '@inkandswitch/patchwork-elements': 6.0.2(@automerge/automerge-repo@2.6.0-subduction.40)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.4.0))(@inkandswitch/patchwork-plugins@1.2.5(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.4.0)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.4.0)))(@inkandswitch/patchwork-providers@0.5.2(@automerge/automerge-repo@2.6.0-subduction.40))(solid-js@1.9.14) - '@inkandswitch/patchwork-filesystem': 0.2.9(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.4.0) - '@inkandswitch/patchwork-plugins': 1.2.5(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.4.0)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.4.0)) + '@codemirror/state': 6.7.6 + '@codemirror/view': 6.43.13 + '@inkandswitch/patchwork-elements': 6.0.2(@automerge/automerge-repo@2.6.0-subduction.40)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.5.0))(@inkandswitch/patchwork-plugins@1.2.5(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.5.0)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.5.0)))(@inkandswitch/patchwork-providers@0.5.2(@automerge/automerge-repo@2.6.0-subduction.40))(solid-js@1.9.15) + '@inkandswitch/patchwork-filesystem': 0.2.9(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.5.0) + '@inkandswitch/patchwork-plugins': 1.2.5(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.5.0)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.5.0)) '@inkandswitch/patchwork-providers': 0.5.2(@automerge/automerge-repo@2.6.0-subduction.40) '@keyhive/keyhive': 0.1.0-alpha.8 '@types/debug': 4.1.13 debug: 4.4.3 resolve.exports: 2.0.3 service-worker-types: '@types/serviceworker@0.0.153' - solid-js: 1.9.14 + solid-js: 1.9.15 tinyargs: 0.1.4 optionalDependencies: - '@automerge/automerge': 3.4.0 + '@automerge/automerge': 3.5.0 '@automerge/automerge-repo': 2.6.0-subduction.40 transitivePeerDependencies: - '@types/react' @@ -1611,53 +1621,66 @@ snapshots: - supports-color - utf-8-validate - '@inkandswitch/patchwork-elements@6.0.2(@automerge/automerge-repo@2.5.6)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.4.0))(@inkandswitch/patchwork-plugins@1.2.5(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.4.0)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.4.0)))(@inkandswitch/patchwork-providers@0.5.2(@automerge/automerge-repo@2.5.6))(solid-js@1.9.14)': + '@inkandswitch/patchwork-elements@6.0.2(@automerge/automerge-repo@2.5.6)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0))(@inkandswitch/patchwork-plugins@1.2.5(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0)))(@inkandswitch/patchwork-providers@0.5.2(@automerge/automerge-repo@2.5.6))(solid-js@1.9.15)': dependencies: '@automerge/automerge-repo': 2.5.6 - '@inkandswitch/patchwork-filesystem': 0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.4.0) - '@inkandswitch/patchwork-plugins': 1.2.5(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.4.0)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.4.0)) + '@inkandswitch/patchwork-filesystem': 0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0) + '@inkandswitch/patchwork-plugins': 1.2.5(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0)) '@inkandswitch/patchwork-providers': 0.5.2(@automerge/automerge-repo@2.5.6) debug: 4.4.3 optionalDependencies: - solid-js: 1.9.14 + solid-js: 1.9.15 transitivePeerDependencies: - supports-color - '@inkandswitch/patchwork-elements@6.0.2(@automerge/automerge-repo@2.6.0-subduction.40)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.4.0))(@inkandswitch/patchwork-plugins@1.2.5(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.4.0)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.4.0)))(@inkandswitch/patchwork-providers@0.5.2(@automerge/automerge-repo@2.6.0-subduction.40))(solid-js@1.9.14)': + '@inkandswitch/patchwork-elements@6.0.2(@automerge/automerge-repo@2.6.0-subduction.40)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.5.0))(@inkandswitch/patchwork-plugins@1.2.5(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.5.0)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.5.0)))(@inkandswitch/patchwork-providers@0.5.2(@automerge/automerge-repo@2.6.0-subduction.40))(solid-js@1.9.15)': dependencies: '@automerge/automerge-repo': 2.6.0-subduction.40 - '@inkandswitch/patchwork-filesystem': 0.2.9(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.4.0) - '@inkandswitch/patchwork-plugins': 1.2.5(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.4.0)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.4.0)) + '@inkandswitch/patchwork-filesystem': 0.2.9(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.5.0) + '@inkandswitch/patchwork-plugins': 1.2.5(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.5.0)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.5.0)) '@inkandswitch/patchwork-providers': 0.5.2(@automerge/automerge-repo@2.6.0-subduction.40) debug: 4.4.3 optionalDependencies: - solid-js: 1.9.14 + solid-js: 1.9.15 transitivePeerDependencies: - supports-color - '@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.4.0)': + '@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0)': dependencies: - '@automerge/automerge': 3.4.0 + '@automerge/automerge': 3.5.0 '@automerge/automerge-repo': 2.5.6 debug: 4.4.3 resolve.exports: 2.0.3 transitivePeerDependencies: - supports-color - '@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.4.0)': + '@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.5.0)': dependencies: - '@automerge/automerge': 3.4.0 + '@automerge/automerge': 3.5.0 '@automerge/automerge-repo': 2.6.0-subduction.40 debug: 4.4.3 resolve.exports: 2.0.3 transitivePeerDependencies: - supports-color - '@inkandswitch/patchwork-plugins@1.2.5(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.4.0)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.4.0))': + '@inkandswitch/patchwork-plugins@0.0.11(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0))': + dependencies: + '@automerge/automerge': 3.5.0 + '@automerge/automerge-repo': 2.5.6 + '@inkandswitch/patchwork-filesystem': 0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0) + '@types/debug': 4.1.13 + '@types/node': 20.19.43 + debug: 4.4.3 + eventemitter3: 5.0.4 + resolve.exports: 2.0.3 + transitivePeerDependencies: + - supports-color + + '@inkandswitch/patchwork-plugins@1.2.5(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0))': dependencies: - '@automerge/automerge': 3.4.0 + '@automerge/automerge': 3.5.0 '@automerge/automerge-repo': 2.5.6 - '@inkandswitch/patchwork-filesystem': 0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.4.0) + '@inkandswitch/patchwork-filesystem': 0.2.9(@automerge/automerge-repo@2.5.6)(@automerge/automerge@3.5.0) '@types/debug': 4.1.13 '@types/node': 20.19.43 debug: 4.4.3 @@ -1666,11 +1689,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@inkandswitch/patchwork-plugins@1.2.5(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.4.0)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.4.0))': + '@inkandswitch/patchwork-plugins@1.2.5(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.5.0)(@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.5.0))': dependencies: - '@automerge/automerge': 3.4.0 + '@automerge/automerge': 3.5.0 '@automerge/automerge-repo': 2.6.0-subduction.40 - '@inkandswitch/patchwork-filesystem': 0.2.9(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.4.0) + '@inkandswitch/patchwork-filesystem': 0.2.9(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.5.0) '@types/debug': 4.1.13 '@types/node': 20.19.43 debug: 4.4.3 @@ -1693,7 +1716,7 @@ snapshots: '@jridgewell/gen-mapping@0.3.13': dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/remapping@2.3.5': @@ -1703,12 +1726,12 @@ snapshots: '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/sourcemap-codec@1.6.0': {} '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 '@keyhive/keyhive@0.1.0-alpha.8': {} @@ -1729,88 +1752,88 @@ snapshots: '@noble/hashes@1.8.0': {} - '@rollup/rollup-android-arm-eabi@4.62.4': + '@rollup/rollup-android-arm-eabi@4.63.4': optional: true - '@rollup/rollup-android-arm64@4.62.4': + '@rollup/rollup-android-arm64@4.63.4': optional: true - '@rollup/rollup-darwin-arm64@4.62.4': + '@rollup/rollup-darwin-arm64@4.63.4': optional: true - '@rollup/rollup-darwin-x64@4.62.4': + '@rollup/rollup-darwin-x64@4.63.4': optional: true - '@rollup/rollup-freebsd-arm64@4.62.4': + '@rollup/rollup-freebsd-arm64@4.63.4': optional: true - '@rollup/rollup-freebsd-x64@4.62.4': + '@rollup/rollup-freebsd-x64@4.63.4': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.62.4': + '@rollup/rollup-linux-arm-gnueabihf@4.63.4': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.62.4': + '@rollup/rollup-linux-arm-musleabihf@4.63.4': optional: true - '@rollup/rollup-linux-arm64-gnu@4.62.4': + '@rollup/rollup-linux-arm64-gnu@4.63.4': optional: true - '@rollup/rollup-linux-arm64-musl@4.62.4': + '@rollup/rollup-linux-arm64-musl@4.63.4': optional: true - '@rollup/rollup-linux-loong64-gnu@4.62.4': + '@rollup/rollup-linux-loong64-gnu@4.63.4': optional: true - '@rollup/rollup-linux-loong64-musl@4.62.4': + '@rollup/rollup-linux-loong64-musl@4.63.4': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.62.4': + '@rollup/rollup-linux-ppc64-gnu@4.63.4': optional: true - '@rollup/rollup-linux-ppc64-musl@4.62.4': + '@rollup/rollup-linux-ppc64-musl@4.63.4': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.62.4': + '@rollup/rollup-linux-riscv64-gnu@4.63.4': optional: true - '@rollup/rollup-linux-riscv64-musl@4.62.4': + '@rollup/rollup-linux-riscv64-musl@4.63.4': optional: true - '@rollup/rollup-linux-s390x-gnu@4.62.4': + '@rollup/rollup-linux-s390x-gnu@4.63.4': optional: true - '@rollup/rollup-linux-x64-gnu@4.62.4': + '@rollup/rollup-linux-x64-gnu@4.63.4': optional: true - '@rollup/rollup-linux-x64-musl@4.62.4': + '@rollup/rollup-linux-x64-musl@4.63.4': optional: true - '@rollup/rollup-openbsd-x64@4.62.4': + '@rollup/rollup-openbsd-x64@4.63.4': optional: true - '@rollup/rollup-openharmony-arm64@4.62.4': + '@rollup/rollup-openharmony-arm64@4.63.4': optional: true - '@rollup/rollup-win32-arm64-msvc@4.62.4': + '@rollup/rollup-win32-arm64-msvc@4.63.4': optional: true - '@rollup/rollup-win32-ia32-msvc@4.62.4': + '@rollup/rollup-win32-ia32-msvc@4.63.4': optional: true - '@rollup/rollup-win32-x64-gnu@4.62.4': + '@rollup/rollup-win32-x64-gnu@4.63.4': optional: true - '@rollup/rollup-win32-x64-msvc@4.62.4': + '@rollup/rollup-win32-x64-msvc@4.63.4': optional: true - '@solid-primitives/utils@6.4.1(solid-js@1.9.14)': + '@solid-primitives/utils@6.4.1(solid-js@1.9.15)': dependencies: - solid-js: 1.9.14 + solid-js: 1.9.15 '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.29.8 + '@babel/parser': 7.29.9 '@babel/types': 7.29.8 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 @@ -1822,7 +1845,7 @@ snapshots: '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.29.8 + '@babel/parser': 7.29.9 '@babel/types': 7.29.8 '@types/babel__traverse@7.28.0': @@ -1884,7 +1907,7 @@ snapshots: '@vitest/spy@3.2.7': dependencies: - tinyspy: 4.0.4 + tinyspy: 4.0.6 '@vitest/utils@3.2.7': dependencies: @@ -1896,7 +1919,7 @@ snapshots: assertion-error@2.0.1: {} - babel-plugin-jsx-dom-expressions@0.40.7(@babel/core@7.29.7): + babel-plugin-jsx-dom-expressions@0.40.10(@babel/core@7.29.7): dependencies: '@babel/core': 7.29.7 '@babel/helper-module-imports': 7.18.6 @@ -1905,26 +1928,26 @@ snapshots: html-entities: 2.3.3 parse5: 7.3.0 - babel-preset-solid@1.9.12(@babel/core@7.29.7)(solid-js@1.9.14): + babel-preset-solid@1.9.15(@babel/core@7.29.7)(solid-js@1.9.15): dependencies: '@babel/core': 7.29.7 - babel-plugin-jsx-dom-expressions: 0.40.7(@babel/core@7.29.7) + babel-plugin-jsx-dom-expressions: 0.40.10(@babel/core@7.29.7) optionalDependencies: - solid-js: 1.9.14 + solid-js: 1.9.15 base-x@4.0.1: {} base-x@5.0.1: {} - baseline-browser-mapping@2.11.12: {} + baseline-browser-mapping@2.11.25: {} - browserslist@4.28.7: + browserslist@4.29.0: dependencies: - baseline-browser-mapping: 2.11.12 - caniuse-lite: 1.0.30001807 - electron-to-chromium: 1.5.402 - node-releases: 2.0.53 - update-browserslist-db: 1.2.3(browserslist@4.28.7) + baseline-browser-mapping: 2.11.25 + caniuse-lite: 1.0.30001810 + electron-to-chromium: 1.5.438 + node-releases: 2.0.57 + update-browserslist-db: 1.3.3(browserslist@4.29.0) bs58@5.0.0: dependencies: @@ -1952,7 +1975,7 @@ snapshots: cac@6.7.14: {} - caniuse-lite@1.0.30001807: {} + caniuse-lite@1.0.30001810: {} cbor-extract@2.2.2: dependencies: @@ -1966,7 +1989,7 @@ snapshots: '@cbor-extract/cbor-extract-win32-x64': 2.2.2 optional: true - cbor-x@1.6.5: + cbor-x@1.6.6: optionalDependencies: cbor-extract: 2.2.2 @@ -1986,12 +2009,12 @@ snapshots: csstype@3.2.3: {} - cute.txt@0.0.6(@automerge/automerge@3.4.0)(@codemirror/state@6.7.5)(@codemirror/view@6.43.12)(solid-js@1.9.14): + cute.txt@0.0.6(@automerge/automerge@3.5.0)(@codemirror/state@6.7.6)(@codemirror/view@6.43.13)(solid-js@1.9.15): optionalDependencies: - '@automerge/automerge': 3.4.0 - '@codemirror/state': 6.7.5 - '@codemirror/view': 6.43.12 - solid-js: 1.9.14 + '@automerge/automerge': 3.5.0 + '@codemirror/state': 6.7.6 + '@codemirror/view': 6.43.13 + solid-js: 1.9.15 debug@4.4.3: dependencies: @@ -2002,7 +2025,7 @@ snapshots: detect-libc@2.1.2: optional: true - electron-to-chromium@1.5.402: {} + electron-to-chromium@1.5.438: {} entities@4.5.0: {} @@ -2010,7 +2033,7 @@ snapshots: es-module-lexer@1.7.0: {} - es-module-lexer@2.3.1: {} + es-module-lexer@2.3.2: {} esbuild@0.21.5: optionalDependencies: @@ -2050,9 +2073,9 @@ snapshots: fast-sha256@1.3.0: {} - fdir@6.5.0(picomatch@4.0.5): + fdir@6.5.0(picomatch@4.0.7): optionalDependencies: - picomatch: 4.0.5 + picomatch: 4.0.7 fsevents@2.3.3: optional: true @@ -2069,9 +2092,9 @@ snapshots: is-what@4.1.16: {} - isomorphic-ws@5.0.0(ws@8.21.2): + isomorphic-ws@5.0.0(ws@8.21.3): dependencies: - ws: 8.21.2 + ws: 8.21.3 js-tokens@4.0.0: {} @@ -2089,7 +2112,7 @@ snapshots: magic-string@0.30.21: dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 merge-anything@5.1.7: dependencies: @@ -2097,14 +2120,14 @@ snapshots: ms@2.1.3: {} - nanoid@3.3.17: {} + nanoid@3.3.19: {} node-gyp-build-optional-packages@5.1.1: dependencies: detect-libc: 2.1.2 optional: true - node-releases@2.0.53: {} + node-releases@2.0.57: {} parse5@7.3.0: dependencies: @@ -2116,46 +2139,46 @@ snapshots: picocolors@1.1.1: {} - picomatch@4.0.5: {} + picomatch@4.0.7: {} - postcss@8.5.26: + postcss@8.5.28: dependencies: - nanoid: 3.3.17 + nanoid: 3.3.19 picocolors: 1.1.1 source-map-js: 1.2.1 resolve.exports@2.0.3: {} - rollup@4.62.4: + rollup@4.63.4: dependencies: '@types/estree': 1.0.9 optionalDependencies: '@napi-rs/lzma-linux-x64-gnu': 1.5.1 - '@rollup/rollup-android-arm-eabi': 4.62.4 - '@rollup/rollup-android-arm64': 4.62.4 - '@rollup/rollup-darwin-arm64': 4.62.4 - '@rollup/rollup-darwin-x64': 4.62.4 - '@rollup/rollup-freebsd-arm64': 4.62.4 - '@rollup/rollup-freebsd-x64': 4.62.4 - '@rollup/rollup-linux-arm-gnueabihf': 4.62.4 - '@rollup/rollup-linux-arm-musleabihf': 4.62.4 - '@rollup/rollup-linux-arm64-gnu': 4.62.4 - '@rollup/rollup-linux-arm64-musl': 4.62.4 - '@rollup/rollup-linux-loong64-gnu': 4.62.4 - '@rollup/rollup-linux-loong64-musl': 4.62.4 - '@rollup/rollup-linux-ppc64-gnu': 4.62.4 - '@rollup/rollup-linux-ppc64-musl': 4.62.4 - '@rollup/rollup-linux-riscv64-gnu': 4.62.4 - '@rollup/rollup-linux-riscv64-musl': 4.62.4 - '@rollup/rollup-linux-s390x-gnu': 4.62.4 - '@rollup/rollup-linux-x64-gnu': 4.62.4 - '@rollup/rollup-linux-x64-musl': 4.62.4 - '@rollup/rollup-openbsd-x64': 4.62.4 - '@rollup/rollup-openharmony-arm64': 4.62.4 - '@rollup/rollup-win32-arm64-msvc': 4.62.4 - '@rollup/rollup-win32-ia32-msvc': 4.62.4 - '@rollup/rollup-win32-x64-gnu': 4.62.4 - '@rollup/rollup-win32-x64-msvc': 4.62.4 + '@rollup/rollup-android-arm-eabi': 4.63.4 + '@rollup/rollup-android-arm64': 4.63.4 + '@rollup/rollup-darwin-arm64': 4.63.4 + '@rollup/rollup-darwin-x64': 4.63.4 + '@rollup/rollup-freebsd-arm64': 4.63.4 + '@rollup/rollup-freebsd-x64': 4.63.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.63.4 + '@rollup/rollup-linux-arm-musleabihf': 4.63.4 + '@rollup/rollup-linux-arm64-gnu': 4.63.4 + '@rollup/rollup-linux-arm64-musl': 4.63.4 + '@rollup/rollup-linux-loong64-gnu': 4.63.4 + '@rollup/rollup-linux-loong64-musl': 4.63.4 + '@rollup/rollup-linux-ppc64-gnu': 4.63.4 + '@rollup/rollup-linux-ppc64-musl': 4.63.4 + '@rollup/rollup-linux-riscv64-gnu': 4.63.4 + '@rollup/rollup-linux-riscv64-musl': 4.63.4 + '@rollup/rollup-linux-s390x-gnu': 4.63.4 + '@rollup/rollup-linux-x64-gnu': 4.63.4 + '@rollup/rollup-linux-x64-musl': 4.63.4 + '@rollup/rollup-openbsd-x64': 4.63.4 + '@rollup/rollup-openharmony-arm64': 4.63.4 + '@rollup/rollup-win32-arm64-msvc': 4.63.4 + '@rollup/rollup-win32-ia32-msvc': 4.63.4 + '@rollup/rollup-win32-x64-gnu': 4.63.4 + '@rollup/rollup-win32-x64-msvc': 4.63.4 fsevents: 2.3.3 semver@6.3.1: {} @@ -2168,27 +2191,27 @@ snapshots: siginfo@2.0.0: {} - solid-automerge@2.0.1(@automerge/automerge-repo@2.5.6)(solid-js@1.9.14): + solid-automerge@2.0.1(@automerge/automerge-repo@2.5.6)(solid-js@1.9.15): dependencies: '@automerge/automerge-repo': 2.5.6 - '@solid-primitives/utils': 6.4.1(solid-js@1.9.14) + '@solid-primitives/utils': 6.4.1(solid-js@1.9.15) cabbages: 0.2.10 - solid-js: 1.9.14 + solid-js: 1.9.15 transitivePeerDependencies: - supports-color - solid-js@1.9.14: + solid-js@1.9.15: dependencies: csstype: 3.2.3 seroval: 1.5.6 seroval-plugins: 1.5.6(seroval@1.5.6) - solid-refresh@0.6.3(solid-js@1.9.14): + solid-refresh@0.6.3(solid-js@1.9.15): dependencies: '@babel/generator': 7.29.8 '@babel/helper-module-imports': 7.29.7 '@babel/types': 7.29.8 - solid-js: 1.9.14 + solid-js: 1.9.15 transitivePeerDependencies: - supports-color @@ -2212,14 +2235,14 @@ snapshots: tinyglobby@0.2.17: dependencies: - fdir: 6.5.0(picomatch@4.0.5) - picomatch: 4.0.5 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 tinypool@1.1.1: {} tinyrainbow@2.0.0: {} - tinyspy@4.0.4: {} + tinyspy@4.0.6: {} typescript@5.9.3: {} @@ -2229,16 +2252,16 @@ snapshots: dependencies: '@jridgewell/remapping': 2.3.5 acorn: 8.18.0 - picomatch: 4.0.5 + picomatch: 4.0.7 webpack-virtual-modules: 0.6.2 - update-browserslist-db@1.2.3(browserslist@4.28.7): + update-browserslist-db@1.3.3(browserslist@4.29.0): dependencies: - browserslist: 4.28.7 + browserslist: 4.29.0 escalade: 3.2.0 picocolors: 1.1.1 - uuid@14.0.1: {} + uuid@14.0.2: {} uuid@9.0.1: {} @@ -2264,14 +2287,14 @@ snapshots: dependencies: vite: 5.4.21(@types/node@20.19.43) - vite-plugin-solid@2.11.14(solid-js@1.9.14)(vite@5.4.21(@types/node@20.19.43)): + vite-plugin-solid@2.11.14(solid-js@1.9.15)(vite@5.4.21(@types/node@20.19.43)): dependencies: '@babel/core': 7.29.7 '@types/babel__core': 7.20.5 - babel-preset-solid: 1.9.12(@babel/core@7.29.7)(solid-js@1.9.14) + babel-preset-solid: 1.9.15(@babel/core@7.29.7)(solid-js@1.9.15) merge-anything: 5.1.7 - solid-js: 1.9.14 - solid-refresh: 0.6.3(solid-js@1.9.14) + solid-js: 1.9.15 + solid-refresh: 0.6.3(solid-js@1.9.15) vite: 5.4.21(@types/node@20.19.43) vitefu: 1.1.3(vite@5.4.21(@types/node@20.19.43)) transitivePeerDependencies: @@ -2280,8 +2303,8 @@ snapshots: vite@5.4.21(@types/node@20.19.43): dependencies: esbuild: 0.21.5 - postcss: 8.5.26 - rollup: 4.62.4 + postcss: 8.5.28 + rollup: 4.63.4 optionalDependencies: '@types/node': 20.19.43 fsevents: 2.3.3 @@ -2305,7 +2328,7 @@ snapshots: expect-type: 1.4.0 magic-string: 0.30.21 pathe: 2.0.3 - picomatch: 4.0.5 + picomatch: 4.0.7 std-env: 3.10.0 tinybench: 2.9.0 tinyexec: 0.3.2 @@ -2343,8 +2366,8 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 - ws@8.21.2: {} + ws@8.21.3: {} - xstate@5.32.5: {} + xstate@5.33.2: {} yallist@3.1.1: {} diff --git a/chat/src/components/ChatRoot.tsx b/chat/src/components/ChatRoot.tsx index 08efaad0..419dc697 100644 --- a/chat/src/components/ChatRoot.tsx +++ b/chat/src/components/ChatRoot.tsx @@ -19,24 +19,53 @@ import {TypingBar} from "./TypingBar" import {InputArea} from "./InputArea" import {PluginPanel} from "./PluginPanel" import {Lightbox} from "./Lightbox" -// @ts-ignore — plain-JS library, ships no type declarations -import { - generate as llmGenerate, - popup as llmPopup, - ensureConfig as llmEnsureConfig, - readConfig as llmReadConfig, - writeConfig as llmWriteConfig, - readScopedConfig as llmReadScopedConfig, - describeConfig as llmDescribeConfig, - fetchOpenRouterModels as llmFetchOpenRouterModels, - parseToolCalls as llmParseToolCalls, -} from "@chee/patchwork-llm" +// Chat imports NOTHING from patchwork-llm and has NO build-time dependency on the +// LLM host package. Generation goes through the generic @grjte/patchwork-worker +// transport; the LLM client (the protocol layer: frame vocabulary in, {text, +// toolCalls} out) is resolved at RUNTIME from the plugin registry — whichever +// package registered a `patchwork:worker-client` plugin with id "llm" (today +// @grjte/llm-host) supplies a factory `(session) => {generate}`. The host +// worker-provider resolves config and runs the worker; config lives entirely in +// the host-side LLM config tray, opened via a patchwork:open-tool event. See +// libraries/patchwork-worker/README.md ("The paired client plugin"). +import {connectWorkerClient} from "@grjte/patchwork-worker/client.js" import {generateId} from "../lib/helpers" import {automergeUrlToServiceWorkerUrl} from "@inkandswitch/patchwork-filesystem" import {transcribeVoiceNote} from "../lib/transcription" import {reloadPreviewIframe} from "../lib/preview-frame" import "../styles/chat.css" +function clog(...args: any[]) { + try { + console.log("[chat-llm]", ...args) + } catch {} +} + +/** The LLM client API the "llm" worker-client factory returns (llm-host/src/client.js). */ +interface LLMClient { + generate( + messages: any[] | string, + opts?: Record + ): Promise<{text: string; toolCalls: any[] | null; toolMode?: string}> +} + +// One client for the whole tool, resolved on first use. The registry lookup is +// global (no element needed); the session inside it opens on the first request, +// multiplexes every generation by id, and reconnects on its own. A FAILED +// resolution is not cached: a tool that asked before the LLM package registered +// gets another try on the next generation instead of being wedged. +let llmClientPromise: Promise | null = null +function llmClient(): Promise { + if (!llmClientPromise) { + llmClientPromise = connectWorkerClient("llm", { + sessionOpts: {idPrefix: "chatllm", onLog: clog}, + }) as Promise + llmClientPromise.catch(() => { + llmClientPromise = null + }) + } + return llmClientPromise +} export function ChatRoot(props: { handle: DocHandle @@ -103,57 +132,45 @@ export function ChatRoot(props: { // Sidebar state const [sidebarVisible, setSidebarVisible] = createSignal(false) - // The per-tool / per-doc config scope this chat resolves against. Generation - // passes the same scope to generate(), so every config READ in the UI must go - // through scopedCfg() too — otherwise the UI shows the global default while the - // computer runs with this chat's override. - const llmScope = () => ({toolId: "chitterchatter", docId: props.handle.url}) - function scopedCfg(): any { - try { - return llmReadScopedConfig(llmScope()) - } catch { - return llmReadConfig() - } - } + // The per-tool / per-doc config scope this chat resolves against. Chat never + // reads config itself — it passes this scope to the host on every generation + // (the host worker-provider resolves provider/model/apiKey/prompts/toolToggles + // from it) and when opening the config picker. `docName` is a display label for + // the picker's scope switcher. + const llmScope = () => ({ + toolId: "chitterchatter", + docId: props.handle.url, + toolName: "Chitterchatter", + docName: (props.handle.doc() as any)?.title || "this chat", + }) - // Model picker (the @chee/patchwork-llm popover lives in the light DOM) - let modelPickerEl: HTMLElement | null = null - async function openModelPicker() { - if (modelPickerEl) return - await llmEnsureConfig(llmScope(), props.element) - // Surface @computer's built-in system prompt in the picker so it's visible - // and can be forked into an editable override. - const el = llmPopup({ - toolName: "Chitterchatter", - // Per-tool / per-doc config scope (Default · This tool · This doc switcher). - scope: { - toolId: "chitterchatter", - docId: props.handle.url, - toolName: "Chitterchatter", - docName: (props.handle.doc() as any)?.title || "this chat", - }, - toolPrompt: { - name: "Chitterchatter · Computer", - text: computerSystemPrompt(), - }, - toolTools: activeTools().map((t) => ({ - name: t.name, - description: t.description, - })), - }) - modelPickerEl = el - document.body.append(el) - ;(el as any).showPopover?.() - try { - await (el as any).result - } finally { - el.remove() - modelPickerEl = null - // Refresh the label and, if the model actually changed, announce it in - // the chat — regardless of whether this tab is the computer host, so a - // model switch is always visible to everyone. - void syncModelLabel({announce: true}) - } + // Open the host-side LLM config tool (the @grjte/llm-config tray). Chat can't + // render the picker itself (config lives in the host; the settings doc is + // denylisted under isolation) — it dispatches a patchwork:open-tool event that + // the host tray answers directly, or that the isolation open-tool bridge + // relays to the host when chat runs inside the sandbox. Same call either way. + // We pass chat's system prompt + tool list (with defaultOff flags) so the host + // picker can surface them; opt-in tools render unchecked with no doc write. + function openModelPicker() { + props.element.dispatchEvent( + new CustomEvent("patchwork:open-tool", { + detail: { + component: "llm-config-tray", + scope: llmScope(), + toolPrompt: { + name: "Chitterchatter · Computer", + text: computerSystemPrompt(), + }, + toolTools: activeTools().map((t) => ({ + name: t.name, + description: t.description, + defaultOff: GLOBAL_DEFAULT_OFF.includes(t.name) || CONTEXT_DEFAULT_OFF.includes(t.name), + })), + }, + bubbles: true, + composed: true, + }) + ) } // Drop state @@ -173,19 +190,17 @@ export function ChatRoot(props: { // Human-readable label of the model the computer is currently running, shown // in the computer's username (e.g. "computer (OpenRouter Claude Opus 4)"). const [modelLabel, setModelLabel] = createSignal("") - function refreshModelLabel() { - describeCurrentModel().then(setModelLabel).catch(() => {}) - } - // Resolve the live model label and, if it changed, announce it in the chat so - // everyone sees which model the computer switched to. Returns the new label. - async function syncModelLabel({announce = false} = {}): Promise { + // The model label now arrives on the generation stream (the host worker emits a + // {type:"model", label} frame — chat reads no config to compute it). This is + // called from the generate loop's onModel callback: it updates the label and, + // if it changed since last time, announces the switch in the chat. + function noteModelLabel(label: string) { + if (!label) return const before = modelLabel() - const after = await describeCurrentModel().catch(() => before) - setModelLabel(after) - if (announce && after && after !== before) { - sendComputerMessage("🔌 switched model to " + after) + setModelLabel(label) + if (before && label !== before) { + sendComputerMessage("🔌 switched model to " + label) } - return after } // The model name suffix shown after the computer's username, e.g. " (Browser // Qwen3 0.6B)". Empty until the label resolves. @@ -199,11 +214,9 @@ export function ChatRoot(props: { const owner = (props.handle.doc() as any)?.computerOwner return "computer" + (owner ? " (" + owner + ")" : "") + modelSuffix() } - // Keep the label populated from the start so the very first computer message - // already carries the model name (not just messages sent after the picker). - onMount(() => { - refreshModelLabel() - }) + // The model label is populated from the generation stream (noteModelLabel), so + // there's nothing to resolve at rest — the first computer message picks it up + // as soon as generation emits the model frame. const [computerAbort, setComputerAbort] = createSignal(null) const computerRespondedToIds = new Set() @@ -516,16 +529,24 @@ Keep responses concise. When you create a tool, explain briefly what it does.` // Pick the prompt for the active model: local/in-browser models (small, // limited context) get the compact prompt; capable cloud providers get the - // full one with worked examples. Falls back to full on any read error. + // full one with worked examples. The provider is resolved HOST-side, so chat + // no longer reads config to choose — it sends BOTH variants as a + // provider-conditional map and the worker picks by resolved provider (see + // computerSystemForFrame). computerSystemPrompt() returns the full prompt for + // display (e.g. in the picker). function computerSystemPrompt(): string { if (isContext()) return CONTEXT_SYSTEM_PROMPT - try { - const cfg = scopedCfg() - return cfg?.provider === "local" - ? COMPUTER_SYSTEM_PROMPT_COMPACT - : COMPUTER_SYSTEM_PROMPT_FULL - } catch { - return COMPUTER_SYSTEM_PROMPT_FULL + return COMPUTER_SYSTEM_PROMPT_FULL + } + + // The provider-conditional system prompt sent on each generation: local models + // get the compact prompt, everything else the full one. The worker resolves + // the provider and picks the matching entry (falling back to `default`). + function computerSystemForFrame(): string | Record { + if (isContext()) return CONTEXT_SYSTEM_PROMPT + return { + default: COMPUTER_SYSTEM_PROMPT_FULL, + local: COMPUTER_SYSTEM_PROMPT_COMPACT, } } @@ -686,94 +707,68 @@ Never overwrite an entire long field with a key-assign (range:"content") just to return ["tool: " + call.name, ...lines].join("\n") } - // Tools to offer this turn — the active mode's set, minus any the user disabled - // in the picker (cfg.toolToggles[name] === false). Most default to enabled; - // define_tool (any mode) and context mode's inspect_dom/eval_js are opt-IN - // (offered only if the toggle is explicitly true). + // Tools to offer this turn: the active mode's full set, each tagged with + // `defaultOff` (chat's opt-in policy). Chat does NOT filter by toolToggles — + // the host worker applies the generic rule (include unless off; defaultOff + // tools included only when explicitly on) using the config it resolves. So the + // user's per-tool toggles still take effect, host-side, without chat reading + // config. Picker default-off rendering is handled via the same `defaultOff` + // flag (no seed-write needed). function enabledComputerTools() { - const toggles = (scopedCfg()?.toolToggles) || {} - return activeTools().filter((t) => { - const optIn = + return activeTools().map((t) => ({ + ...t, + defaultOff: GLOBAL_DEFAULT_OFF.includes(t.name) || - (isContext() && CONTEXT_DEFAULT_OFF.includes(t.name)) - if (optIn) return toggles[t.name] === true - return toggles[t.name] !== false - }) + (isContext() && CONTEXT_DEFAULT_OFF.includes(t.name)), + })) } - // The model picker renders a tool's checkbox CHECKED unless - // toolToggles[name] === false (it has no "default off" concept). So our opt-in - // tools would show checked while actually being off. Seed them to `false` once - // (only if unset — a user who turns one ON stays ON) so the picker shows them - // unchecked, matching enabledComputerTools(). - onMount(async () => { - if (!has("computer")) return - try { - await llmEnsureConfig(llmScope(), props.element) - const tg = {...((llmReadConfig() as any)?.toolToggles || {})} - let changed = false - for (const n of [...GLOBAL_DEFAULT_OFF, ...CONTEXT_DEFAULT_OFF]) { - if (tg[n] === undefined) { - tg[n] = false - changed = true - } - } - if (changed) llmWriteConfig({toolToggles: tg} as any) - } catch {} - }) - - // ---- LLM generation (via @chee/patchwork-llm) ---- - // Provider / model / API key / sampling parameters all live on the account - // doc and are configured through the shared model picker (`/model` → - // openModelPicker). The library runs local (transformers.js) / OpenRouter / - // Ollama in a refresh-surviving SharedWorker and streams tokens back. + // (Former onMount toolToggles seed-write removed: the picker now renders opt-in + // tools unchecked via their `defaultOff` flag, and the worker filters by that + // flag + the user's toggles — no client-side config read/write needed.) + + // ---- LLM generation (via the host worker over @grjte/patchwork-worker) ---- + // Provider / model / API key / sampling all live in the LLM settings doc and + // are configured through the host-side config tool (`/model` → openModelPicker + // dispatches patchwork:open-tool). The host worker-provider resolves config + // from `scope` and runs the worker (local transformers.js / OpenRouter / + // Ollama), streaming tokens back. Chat reads no config. The client itself + // comes from the registry (see llmClient), not from an import. async function generateLLM( messages: any[], onToken: (text: string) => void, signal?: AbortSignal, onStatus?: (status: string) => void, - system?: string, - tools?: any[] + system?: string | Record, + tools?: any[], + onModel?: (label: string) => void ): Promise<{text: string; toolCalls: any[] | null}> { - const {text, toolCalls} = await llmGenerate(messages, { + const client = await llmClient() + const {text, toolCalls} = await client.generate(messages, { sessionKey: props.handle.url, - // Resolve config for this tool + this chat doc (whole-scope overrides - // configured in the picker; falls back to tool, then default). + // A node inside the mounted , so connectWorker's + // discovery dispatches patchwork:worker-channel from here — reaching the + // host-realm worker provider (directly, or relayed across isolation). + element: props.element, + // The scope the host resolves config from (provider/model/apiKey/prompts/ + // toolToggles) — chat sends it but reads none of it. scope: llmScope(), - // The system prompt goes through the lib (opts.system) so it composes - // with any user-selected/forked system prompt via effectiveSystem. + // Provider-conditional system prompt ({default, local}); the worker picks + // by resolved provider and composes with any user-forked prompt host-side. ...(system ? {system} : {}), - // Real tool calls: native function-calling for OpenRouter/Ollama, the - // convention for local. Execution stays in runToolByName. + // All active tools, each tagged defaultOff; the worker filters by the + // user's toolToggles. Execution stays in runToolByName. ...(tools && tools.length ? {tools} : {}), onToken: (_delta: string, full: string) => onToken(full), onStatus: (status: string) => onStatus?.(status), + onModel, signal, }) return {text, toolCalls: (toolCalls as any) || null} } - // Human-readable label for the model that's currently selected (provider + - // model name). Used in the computer's join message so people can see/change - // which model is answering. Falls back gracefully if the config or the - // OpenRouter catalogue can't be read. - async function describeCurrentModel(): Promise { - try { - await llmEnsureConfig(llmScope(), props.element) - const cfg = scopedCfg() - let openrouterModels: any[] = [] - if (cfg.provider === "openrouter") { - try { - openrouterModels = await llmFetchOpenRouterModels() - } catch { - openrouterModels = [] - } - } - return llmDescribeConfig(cfg, {openrouterModels}) - } catch { - return "the configured model" - } - } + // (describeCurrentModel removed — the model label is computed HOST-side and + // arrives on the generation stream via noteModelLabel; chat reads no config.) // ---- Rich block parsing ---- function parseRichBlocks(response: string) { @@ -2118,20 +2113,19 @@ Never overwrite an entire long field with a key-assign (range:"content") just to } setComputerActive(true) claimComputerHost() - describeCurrentModel().then((model) => { - setModelLabel(model) - sendComputerMessage( - [ - "hello! i'm computer, a computer program. mention @computer or reply to my messages and i'll respond.", - "", - "• currently running: " + model, - "• /model — pick a different model or provider", - "• /computer nosey — make me respond to everything", - "• /computer owner — see who's hosting me; /computer own to take over", - "• /computer kick — send me away", - ].join("\n") - ) - }) + // The model label arrives on the first generation stream (noteModelLabel); + // we don't resolve it at join time (chat reads no config). The join message + // points people at /model to see/change which model is running. + sendComputerMessage( + [ + "hello! i'm computer, a computer program. mention @computer or reply to my messages and i'll respond.", + "", + "• /model — pick the model or provider (opens the config panel)", + "• /computer nosey — make me respond to everything", + "• /computer owner — see who's hosting me; /computer own to take over", + "• /computer kick — send me away", + ].join("\n") + ) startComputerListener() } @@ -2454,22 +2448,26 @@ Never overwrite an entire long field with a key-assign (range:"content") just to .includes("@momputer") // Generate a tool name for this response — the LLM uses it if it builds a tool const suggestedToolName = randomToolName() - // The built-in COMPUTER_SYSTEM_PROMPT is the *default* — but if the user - // forked it (a selected system prompt doc, cfg.systemUrl), that override - // fully replaces it (the lib prepends it via effectiveSystem). Either way, - // the per-response addenda below always apply. System goes through - // opts.system (not a chat message) so the lib can compose them. - const forked = !!scopedCfg()?.systemUrl - let systemPrompt = forked ? "" : computerSystemPrompt() - systemPrompt += - (systemPrompt ? "\n\n" : "") + - '## Your Tool ID\nIf you build a patchwork tool in this response, use `"' + + // The provider-conditional base system prompt ({default, local}) plus + // per-response addenda that always apply. Chat reads no config; the worker + // picks the base by resolved provider and composes. (Forked/custom system + // prompts via cfg.systemUrl are not applied for now — see the config + // redesign plan; chat no longer reads systemUrl.) The addenda are appended + // to every provider variant so they apply regardless of which is chosen. + const base = computerSystemForFrame() + const addenda = + '\n\n## Your Tool ID\nIf you build a patchwork tool in this response, use `"' + suggestedToolName + - '"` as the id for both the datatype and tool plugins, and in supportedDatatypes.' - if (isMomputer) { - systemPrompt += - '\n\n## Special Mode: Momputer\nThe user addressed you as @momputer. Be warm, nurturing, and motherly in your response. Use gentle encouragement, express care and concern, and be supportive like a loving mom would be. You can use pet names like "sweetie", "honey", "dear", etc. Still be helpful and knowledgeable, but with a cozy maternal energy.' - } + '"` as the id for both the datatype and tool plugins, and in supportedDatatypes.' + + (isMomputer + ? '\n\n## Special Mode: Momputer\nThe user addressed you as @momputer. Be warm, nurturing, and motherly in your response. Use gentle encouragement, express care and concern, and be supportive like a loving mom would be. You can use pet names like "sweetie", "honey", "dear", etc. Still be helpful and knowledgeable, but with a cozy maternal energy.' + : "") + const systemPrompt: string | Record = + typeof base === "string" + ? base + addenda + : Object.fromEntries( + Object.entries(base).map(([k, v]) => [k, v + addenda]) + ) const messages = [...context, {role: "user", content: userMsg.text}] // Create streaming message — use `let` so we can reassign @@ -2532,7 +2530,8 @@ Never overwrite an entire long field with a key-assign (range:"content") just to abortController.signal, onStatus, systemPrompt, - enabledComputerTools() + enabledComputerTools(), + noteModelLabel ) resetInactivityTimer() if (tokenThrottleTimer) { @@ -2541,11 +2540,9 @@ Never overwrite an entire long field with a key-assign (range:"content") just to } let response = (gen.text || "").replace(/^\[Computer\]\s*/i, "") // Real tool calls: structured from the provider (native function - // calling), else parsed from the model's text (local). - const calls = - gen.toolCalls && gen.toolCalls.length - ? gen.toolCalls - : llmParseToolCalls(response) + // calling), or parsed from the model's text HOST-side by + // the LLM worker spec. Chat never parses model output itself. + const calls = gen.toolCalls ?? [] // Strip any tool-call markup from the text we display (local models // emit … inline; that's plumbing, not prose). const visible = response @@ -2992,10 +2989,8 @@ Never overwrite an entire long field with a key-assign (range:"content") just to } onCleanup(() => { - if (modelPickerEl) { - modelPickerEl.remove() - modelPickerEl = null - } + // (No chat-owned model picker to tear down anymore — the picker lives in the + // host config tool, opened via patchwork:open-tool.) if (heartbeatInterval) { clearInterval(heartbeatInterval) heartbeatInterval = null diff --git a/libraries/llm/.gitignore b/libraries/llm/.gitignore index a7c47537..f372293c 100644 --- a/libraries/llm/.gitignore +++ b/libraries/llm/.gitignore @@ -1 +1,2 @@ types/ +.pushwork diff --git a/libraries/llm/README.md b/libraries/llm/README.md index 0e7a68cd..481aa0fb 100644 --- a/libraries/llm/README.md +++ b/libraries/llm/README.md @@ -102,6 +102,32 @@ Pass a stable `sessionKey` (e.g. a doc URL) to `generate`/`stream`; after a reload, `resume(sessionKey, { onToken, onDone })` re-attaches to the still-running stream in the worker. +### Request preparation (`request.js`) + +`generate()` is two halves: preparing the request and posting it to the worker. +The first half is exported so a host-side worker spec (one that serves this +library over a stream to sandboxed tools) prepares requests exactly the way +`generate()` does: + +```js +import { prepareGenerate, buildGeneratePayload } from "@patchwork/llm" + +const { cfg, config, extraSystem, builtin, input } = await prepareGenerate(cfg0, { + messages, // chat messages or a string + system, // extra system prompt, already provider-resolved + tools, // tool descriptors, already filtered + continuation, // treat a string as a raw continuation + overrides, // passed to callConfig verbatim +}) +if (builtin) { /* run builtinGenerate(input, { system: effectiveSystem(cfg, extraSystem), … }) */ } +else worker.postMessage(buildGeneratePayload(config, input, { id, sessionKey })) +``` + +Resolving a provider-conditional `system` map, filtering tools by the user's +`toolToggles`, and choosing which `overrides` to forward are the caller's job: +`callConfig` honours provider / apiKey / model / url overrides, so a host serving +untrusted tools passes only sampling knobs. + ## Events reference | event | fields | local | openrouter | ollama | diff --git a/libraries/llm/client.js b/libraries/llm/client.js index f9b40765..63962508 100644 --- a/libraries/llm/client.js +++ b/libraries/llm/client.js @@ -13,7 +13,8 @@ import {readConfig, ensureConfig, callConfig, applyPrompts, effectiveSystem} from "./config.js" import {builtinGenerate} from "./builtin.js" -import {resolveTools, toToolSchemas, buildToolsSystem, parseToolCalls, runTool, resolveCfgPrompts, sanitizeToolName} from "./tools.js" +import {resolveTools, buildToolsSystem, parseToolCalls, runTool, resolveCfgPrompts, sanitizeToolName} from "./tools.js" +import {prepareGenerate, buildGeneratePayload, NATIVE_TOOL_PROVIDERS, TEMPLATE_TOOL_PROVIDERS} from "./request.js" /** * Per-call options. A superset of every option any exported function accepts; @@ -66,16 +67,7 @@ import {resolveTools, toToolSchemas, buildToolsSystem, parseToolCalls, runTool, /** @typedef {{post: (m:any)=>void}} Connection */ -/** CallConfig plus the extra mutable fields the client tacks on per-call. - * @typedef {import("./config.js").CallConfig & {tools?:any, toolSystem?:string, continuation?:boolean}} CallConfigExt */ - -/** @typedef {{type:string, id:string, sessionKey:string, provider:import("./config.js").ProviderId, config:import("./config.js").CallConfig, text?:string, messages?:any}} GeneratePayload */ - -// Providers with real function-calling APIs (the worker passes tool schemas and -// parses structured tool_calls). Everything else (local transformers, Chrome -// built-in) uses the XML prompt convention, parsed from the text. -const NATIVE_TOOL_PROVIDERS = new Set(["openrouter", "ollama", "webllm"]) -const TEMPLATE_TOOL_PROVIDERS = new Set(["local"]) +/** @typedef {import("./request.js").CallConfigExt} CallConfigExt */ /** @type {Connection|null} */ let connection = null @@ -197,34 +189,21 @@ function getConnection() { */ export async function generate(messages, opts = {}) { const cfg0 = opts.config ?? (await ensureConfig(opts.scope)) - // Resolve the selected system/pre prompt docs → their text. repo.find is - // cached, so this is cheap after first load. - const cfg = await resolveCfgPrompts(cfg0) - /** @type {CallConfigExt} */ - const config = callConfig(/** @type {any} */ (cfg), /** @type {any} */ (opts)) - - // Tools: native providers get JSON schemas on `config.tools`; the rest get the - // XML convention prepended to the system prompt (parsed from text). - const hasTools = Array.isArray(opts.tools) && opts.tools.length > 0 - const native = hasTools && NATIVE_TOOL_PROVIDERS.has(config.provider) - const templated = hasTools && TEMPLATE_TOOL_PROVIDERS.has(config.provider) - if (native || templated) config.tools = toToolSchemas(opts.tools) - if (templated) config.toolSystem = buildToolsSystem(opts.tools) - const extraSystem = - hasTools && !native && !templated - ? [buildToolsSystem(opts.tools), opts.system].filter(Boolean).join("\n\n") - : opts.system + // Prompt resolution, CallConfig, the tools decision, the builtin detour and + // the chat-vs-continuation input shape all live in request.js, shared with + // host-side worker specs. The whole opts is passed as overrides: a same-realm + // caller legitimately owns provider/model/apiKey overrides. + const {cfg, config, extraSystem, builtin, input} = await prepareGenerate(cfg0, { + messages, + system: opts.system, + tools: opts.tools, + continuation: opts.continuation, + overrides: opts, + }) // Built-in (Chrome Prompt API) runs on the main thread, not the worker. - if (config.provider === "builtin") { - const pre = cfg.resolved?.pre || "" - const text = - typeof messages === "string" - ? pre - ? pre + "\n\n" + messages - : messages - : messages - return builtinGenerate(text, { + if (builtin) { + return builtinGenerate(input, { temperature: config.temperature, topK: config.topK, system: effectiveSystem(/** @type {any} */ (cfg), extraSystem), @@ -234,19 +213,6 @@ export async function generate(messages, opts = {}) { }).then((t) => ({text: t, toolCalls: null, stats: null})) } - // A string input is CHAT by default — wrapped as a user turn, so instruct/chat - // models respond normally and the system prompt applies. It's a raw - // CONTINUATION only when opts.continuation is set: raw-fed for - // local/webllm/ollama, and CONTINUE_SYS-framed for chat-only OpenRouter (see - // the worker). Loom passes continuation:true; other callers get plain chat. - const asContinuation = !!opts.continuation && typeof messages === "string" - const prepared = asContinuation - ? messages - : typeof messages === "string" - ? [{role: "user", content: messages}] - : messages - // Prepend the configured system + pre-prompt (and any tool-supplied system). - const input = applyPrompts(prepared, /** @type {any} */ (cfg), extraSystem) const conn = getConnection() const id = nextId() const sessionKey = opts.sessionKey || id @@ -297,11 +263,7 @@ export async function generate(messages, opts = {}) { opts.signal.addEventListener("abort", onAbort) } // A string is a raw continuation prompt; an array is chat messages. - /** @type {GeneratePayload} */ - const payload = {type: "generate", id, sessionKey, provider: config.provider, config} - if (typeof input === "string") payload.text = input - else payload.messages = input - conn.post(payload) + conn.post(buildGeneratePayload(config, input, {id, sessionKey})) }) } diff --git a/libraries/llm/index.js b/libraries/llm/index.js index 1fd091d9..4cead657 100644 --- a/libraries/llm/index.js +++ b/libraries/llm/index.js @@ -21,92 +21,117 @@ */ export { - // config (account doc + patchwork:llm-config provider) - readConfig, - writeConfig, - callConfig, - normalizeConfig, - subscribeConfig, - resolveConfig, - ensureSettingsDoc, - ensureConfig, - // per-tool / per-doc whole-config overrides - scopedRaw, - hasScopeOverride, - readScopedConfig, - writeScopeOverride, - clearScopeOverride, - settingsDocHandle, - applyPrompts, - effectiveSystem, - DEFAULTS, - PARAM_KEYS, - PROVIDER_CAPS, - TOOL_STORAGE_ID, - CONFIG_SELECTOR, - // catalogues / labels - LOCAL_MODELS, - WEBLLM_MODELS, - fetchOpenRouterModels, - fetchOllamaModels, - describeConfig, -} from "./config.js" + // config (account doc + patchwork:llm-config provider) + readConfig, + writeConfig, + callConfig, + normalizeConfig, + subscribeConfig, + resolveConfig, + ensureSettingsDoc, + ensureConfig, + // per-tool / per-doc whole-config overrides + scopedRaw, + hasScopeOverride, + readScopedConfig, + writeScopeOverride, + clearScopeOverride, + settingsDocHandle, + applyPrompts, + effectiveSystem, + DEFAULTS, + PARAM_KEYS, + PROVIDER_CAPS, + TOOL_STORAGE_ID, + CONFIG_SELECTOR, + // catalogues / labels + LOCAL_MODELS, + WEBLLM_MODELS, + fetchOpenRouterModels, + fetchOllamaModels, + describeConfig, +} from "./config.js"; export { - generate, - generateWithTools, - stream, - predict, - scoreTokens, - preload, - abort, - resume, - onStatus, - registerLocalModel, - computeImportance, - computeAttentionWeights, - extractFeatures, - extractCutFeatures, - decodeTokens, - probeAttention, -} from "./client.js" + generate, + generateWithTools, + stream, + predict, + scoreTokens, + preload, + abort, + resume, + onStatus, + registerLocalModel, + computeImportance, + computeAttentionWeights, + extractFeatures, + extractCutFeatures, + decodeTokens, + probeAttention, +} from "./client.js"; -export {dom, popup} from "./picker.js" +export { dom, popup } from "./picker.js"; -export {builtinSupported, builtinAvailability} from "./builtin.js" +export { + builtinSupported, + builtinAvailability, + builtinGenerate, +} from "./builtin.js"; + +// Construct this library's compute worker. This has to live in the library: +// `new URL("./worker.js", import.meta.url)` resolves against this module's own +// URL, so it finds the sibling worker.js wherever the library is served from, +// and bundlers only emit the worker chunk when they see this exact literal. +// +// import { createWorker } from "@chee/patchwork-llm"; +// const worker = createWorker(); +// worker.onmessage = (e) => console.log(e.data.models); +// worker.postMessage({ type: "list-local-models" }); +export const createWorker = () => + new Worker(new URL("./worker.js", import.meta.url), { type: "module" }); + +// Transport-neutral request preparation, shared by generate() and by +// host-side worker specs that serve this library over a stream. +export { + prepareGenerate, + buildGeneratePayload, + NATIVE_TOOL_PROVIDERS, + TEMPLATE_TOOL_PROVIDERS, +} from "./request.js"; // LLM tools (user-defined tools the model can be given) export { - createLLMTool, - createToolFile, - LLMToolDatatype, - sanitizeToolName, - resolveTools, - toToolSchemas, - buildToolsSystem, - parseToolCalls, - loadHandler, - runTool, - runHandlerSandboxed, - // saved prompts (system + pre), same doc shape as tools - createPromptDoc, - resolvePromptDocs, - resolvePromptText, - resolveCfgPrompts, - LLMSystemPromptDatatype, - LLMPrePromptDatatype, - // folders + one-time migration - ensureFolderUrl, - addToFolder, - removeFromFolder, - migrateConfig, -} from "./tools.js" + createLLMTool, + createToolFile, + LLMToolDatatype, + sanitizeToolName, + resolveTools, + toToolSchemas, + buildToolsSystem, + parseToolCalls, + loadHandler, + runTool, + runHandlerSandboxed, + // saved prompts (system + pre), same doc shape as tools + createPromptDoc, + resolvePromptDocs, + resolvePromptText, + resolveCfgPrompts, + LLMSystemPromptDatatype, + LLMPrePromptDatatype, + // folders + one-time migration + ensureFolderUrl, + addToFolder, + removeFromFolder, + migrateConfig, +} from "./tools.js"; // Registers on import. export { - PatchworkLLMConfigProvider, - definePatchworkLLMConfigProvider, -} from "./provider.js" + PatchworkLLMConfigProvider, + definePatchworkLLMConfigProvider, +} from "./provider.js"; // Built-in prompt templates -export {PROMPT_TEMPLATES} from "./templates.js" +export { PROMPT_TEMPLATES } from "./templates.js"; diff --git a/libraries/llm/package.json b/libraries/llm/package.json index 9b2a9e08..84776ea7 100644 --- a/libraries/llm/package.json +++ b/libraries/llm/package.json @@ -1,6 +1,6 @@ { "name": "@chee/patchwork-llm", - "version": "0.2.1", + "version": "0.3.0", "description": "LLM toolkit for Patchwork tools: a model picker, a refresh-surviving SharedWorker that runs local (transformers.js) / OpenRouter / Ollama generation, and a streaming API that carries rich telemetry — next-token predictions, temperature, tokens/sec — alongside the text so you can build UIs that show how the model thinks.", "type": "module", "main": "index.js", @@ -17,13 +17,15 @@ }, "scripts": { "build": "pnpm build:types", - "build:types": "tsc" + "build:types": "tsc", + "push": "pnpm build && pushwork sync" }, "files": [ "types", "index.js", "config.js", "client.js", + "request.js", "worker.js", "picker.js", "tools.js", diff --git a/libraries/llm/pnpm-lock.yaml b/libraries/llm/pnpm-lock.yaml index dc251f69..fa0a70c3 100644 --- a/libraries/llm/pnpm-lock.yaml +++ b/libraries/llm/pnpm-lock.yaml @@ -24,8 +24,8 @@ packages: '@automerge/automerge-repo@2.5.6': resolution: {integrity: sha512-ZXM6TOAwm192g3+zIxYvlB+Z3O00NP+psErOwvbSype8fFO+dhc8tB/jPwfZJqmn1ULUz5w7gssKEynwcYFRSA==} - '@automerge/automerge@3.4.0': - resolution: {integrity: sha512-THmghtTNGGt2xsI0pM3o1i3PM8oZKcYFgOj25FOzW7l6e94SQOivNtCwy6xc0I8hVJsQSSotoBNs+yk/9hM2dg==} + '@automerge/automerge@3.4.1': + resolution: {integrity: sha512-zsZpbs/iDPvp+ZojIYd+gxmbcPVz2Xbkcx778G8zrt3E0zS+6saHJOm666lOuZyNRlTV4wHw9qzGTKueedeCsQ==} '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': resolution: {integrity: sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA==} @@ -79,8 +79,8 @@ packages: resolution: {integrity: sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng==} hasBin: true - cbor-x@1.6.5: - resolution: {integrity: sha512-yO64CxnSh6kp+pHNRK9IfwnMvCB+c8HvmUjQY/9l9YRF0/cAPka/tUHLwS64QqUpFCq3/OtbKziVJYXH2EaRig==} + cbor-x@1.6.6: + resolution: {integrity: sha512-8QiD9PGOxyQHo7s2pzwTBH6lTjqekxPdl9Aq6fXvZgCuCJHOht1puDEA/fTr6mciB76c+M+Gi0qT2i1a4pm4Wg==} debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} @@ -118,25 +118,25 @@ packages: deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true - xstate@5.32.5: - resolution: {integrity: sha512-ULazi1oe6wGrXl0Frb6otSlkm5HLifbbVTkMk5kkSKqz4TkxJaVpnl6jOJwKeid3ORPxYyZQgNLUSYX9q65SIA==} + xstate@5.32.6: + resolution: {integrity: sha512-WfA8WNrh6r9osuGwVm+aIPM4jmMW2LKHV1Yv9thYpOtL4HVF/kobh95K/O8v2jDCpDmP2EoY+6uvuqHXCZ6ZLw==} snapshots: '@automerge/automerge-repo@2.5.6': dependencies: - '@automerge/automerge': 3.4.0 + '@automerge/automerge': 3.4.1 bs58check: 3.0.1 - cbor-x: 1.6.5 + cbor-x: 1.6.6 debug: 4.4.3 eventemitter3: 5.0.4 fast-sha256: 1.3.0 uuid: 9.0.1 - xstate: 5.32.5 + xstate: 5.32.6 transitivePeerDependencies: - supports-color - '@automerge/automerge@3.4.0': {} + '@automerge/automerge@3.4.1': {} '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': optional: true @@ -185,7 +185,7 @@ snapshots: '@cbor-extract/cbor-extract-win32-x64': 2.2.2 optional: true - cbor-x@1.6.5: + cbor-x@1.6.6: optionalDependencies: cbor-extract: 2.2.2 @@ -211,4 +211,4 @@ snapshots: uuid@9.0.1: {} - xstate@5.32.5: {} + xstate@5.32.6: {} diff --git a/libraries/llm/request.js b/libraries/llm/request.js new file mode 100644 index 00000000..5343843b --- /dev/null +++ b/libraries/llm/request.js @@ -0,0 +1,128 @@ +/** + * Request preparation — the transport-neutral half of a `generate` call. + * + * Everything between "here is a resolved config and what the caller wants" and + * "here is the exact payload the worker understands" lives here, once: prompt + * resolution, the flat CallConfig, the native-vs-templated tool decision, the + * built-in (Chrome Prompt API) detour, and the chat-vs-continuation input shape. + * + * Two callers share it and must stay in step: + * - client.js `generate()` — same realm, posts straight to this library's worker. + * - a host-side worker spec (e.g. llm-host's worker-spec.js) — receives request + * frames over a stream and posts to the worker it runs. + * + * What is deliberately NOT here, because it is caller policy rather than + * preparation: resolving a provider-conditional `system` map, filtering tools by + * the user's `toolToggles`, and deciding which `overrides` reach `callConfig` + * (client.js passes the whole opts; a host must pass only sampling knobs, since + * callConfig also honours provider/apiKey/model/url). + */ + +import {callConfig, applyPrompts} from "./config.js" +import {toToolSchemas, buildToolsSystem, resolveCfgPrompts} from "./tools.js" + +/** + * CallConfig plus the extra mutable fields tacked on per call. + * @typedef {import("./config.js").CallConfig & {tools?:any, toolSystem?:string, continuation?:boolean}} CallConfigExt + */ + +/** + * The `generate` message the worker accepts. A string `text` is a raw + * continuation prompt; `messages` is chat. + * @typedef {{type:string, id:string, sessionKey:string, provider:import("./config.js").ProviderId, config:import("./config.js").CallConfig, text?:string, messages?:any}} GeneratePayload + */ + +// Providers with real function-calling APIs (the worker passes tool schemas and +// parses structured tool_calls). Everything else (local transformers, Chrome +// built-in) uses the XML prompt convention, parsed from the text. +export const NATIVE_TOOL_PROVIDERS = new Set(["openrouter", "ollama", "webllm"]) +export const TEMPLATE_TOOL_PROVIDERS = new Set(["local"]) + +/** + * @typedef {Object} PrepareOpts + * @property {any[]|string} messages chat messages, or a string + * @property {string} [system] extra system prompt, already provider-resolved + * @property {any[]} [tools] tool descriptors, already filtered + * @property {boolean} [continuation] treat a string input as a raw continuation + * @property {any} [overrides] passed to callConfig verbatim (see header) + * + * @typedef {Object} PreparedGenerate + * @property {any} cfg config with prompt docs resolved (`cfg.resolved`) + * @property {CallConfigExt} config the flat per-call config the worker receives + * @property {string|undefined} extraSystem + * @property {boolean} builtin provider is "builtin": run builtinGenerate(input, …) + * in-realm instead of posting to the worker + * @property {any} input builtin: the pre-joined text/messages; + * otherwise applyPrompts()'d text or messages + */ + +/** + * Prepare a generate request from an unresolved config and the caller's intent. + * + * @param {any} cfg0 an LLMConfig (e.g. from ensureConfig), prompts not yet resolved + * @param {PrepareOpts} opts + * @returns {Promise} + */ +export async function prepareGenerate(cfg0, {messages, system, tools, continuation, overrides = {}}) { + // Resolve the selected system/pre prompt docs → their text. repo.find is + // cached, so this is cheap after first load. + const cfg = await resolveCfgPrompts(cfg0) + /** @type {CallConfigExt} */ + const config = callConfig(/** @type {any} */ (cfg), /** @type {any} */ (overrides)) + + // Tools: native providers get JSON schemas on `config.tools`; the rest get the + // XML convention prepended to the system prompt (parsed from text). + const hasTools = Array.isArray(tools) && tools.length > 0 + const native = hasTools && NATIVE_TOOL_PROVIDERS.has(config.provider) + const templated = hasTools && TEMPLATE_TOOL_PROVIDERS.has(config.provider) + if (native || templated) config.tools = toToolSchemas(tools) + if (templated) config.toolSystem = buildToolsSystem(tools) + const extraSystem = + hasTools && !native && !templated + ? [buildToolsSystem(tools), system].filter(Boolean).join("\n\n") + : system + + // Built-in (Chrome Prompt API) runs in the caller's realm, not the worker. + // The caller runs builtinGenerate(input, {system: effectiveSystem(cfg, + // extraSystem), …}) with its own callbacks. + if (config.provider === "builtin") { + const pre = cfg.resolved?.pre || "" + const input = + typeof messages === "string" + ? pre + ? pre + "\n\n" + messages + : messages + : messages + return {cfg, config, extraSystem, builtin: true, input} + } + + // A string input is CHAT by default — wrapped as a user turn, so instruct/chat + // models respond normally and the system prompt applies. It's a raw + // CONTINUATION only when asked: raw-fed for local/webllm/ollama, and + // CONTINUE_SYS-framed for chat-only OpenRouter (see the worker). + const asContinuation = !!continuation && typeof messages === "string" + const prepared = asContinuation + ? messages + : typeof messages === "string" + ? [{role: "user", content: messages}] + : messages + // Prepend the configured system + pre-prompt (and any tool-supplied system). + const input = applyPrompts(prepared, /** @type {any} */ (cfg), extraSystem) + return {cfg, config, extraSystem, builtin: false, input} +} + +/** + * The `generate` message for the worker, from a prepared request. + * + * @param {CallConfigExt} config + * @param {any} input string (raw continuation) or chat messages + * @param {{id: string, sessionKey: string}} ids + * @returns {GeneratePayload} + */ +export function buildGeneratePayload(config, input, {id, sessionKey}) { + /** @type {GeneratePayload} */ + const payload = {type: "generate", id, sessionKey, provider: config.provider, config} + if (typeof input === "string") payload.text = input + else payload.messages = input + return payload +} diff --git a/libraries/llm/tsconfig.json b/libraries/llm/tsconfig.json index 5dc1f2d0..1f5e752a 100644 --- a/libraries/llm/tsconfig.json +++ b/libraries/llm/tsconfig.json @@ -15,6 +15,7 @@ "index.js", "config.js", "client.js", + "request.js", "worker.js", "picker.js", "tools.js", diff --git a/libraries/patchwork-worker/.gitignore b/libraries/patchwork-worker/.gitignore new file mode 100644 index 00000000..f928cfcf --- /dev/null +++ b/libraries/patchwork-worker/.gitignore @@ -0,0 +1,3 @@ +dist +node_modules +.pushwork diff --git a/libraries/patchwork-worker/README.md b/libraries/patchwork-worker/README.md new file mode 100644 index 00000000..2e33d179 --- /dev/null +++ b/libraries/patchwork-worker/README.md @@ -0,0 +1,270 @@ +# @grjte/patchwork-worker + +Run a worker in the host realm and hand any consumer a transferable stream pair, +so a plugin that needs a worker is written once and works the same whether or not +it runs behind an isolation boundary. + +```js +// consumer — the same code in any realm +import { connectWorkerClient } from "@grjte/patchwork-worker/client"; +const search = await connectWorkerClient("search-index"); +const hits = await search.query("patchwork", { element }); +``` + +```js +// a package that owns a worker — declarative, nothing imported until first use +export const plugins = [ + { + type: "patchwork:worker", // serve half: how to run the worker + id: "search-index", + name: "Search Index", + async load() { + return makeWorkerSpec(); // -> WorkerSpec {createWorker, open?, handle, abort?} + }, + }, + { + type: "patchwork:worker-client", // consume half: the typed client + id: "search-index", + name: "Search Index client", + async load() { + return makeSearchClient; // -> (session) => {query, ...} + }, + }, +]; +``` + +The package is service-agnostic: it knows how to run _a_ worker in the host and +stream to _a_ consumer, and nothing about what any particular worker computes. A +concrete service (an LLM, a transcription engine, a search indexer) plugs in +through the **plugin registry**: it registers a plugin of type `patchwork:worker` +whose `id` names the worker `kind` and whose `load()` resolves to a **WorkerSpec**, +and usually a paired `patchwork:worker-client` plugin with the same `id`. That +registration is the whole coupling. The host provider (the +`patchwork-worker-provider` component, shipped by the `providers` package) +discovers workers by looking them up in the `patchwork:worker` registry by `kind`, +so a consumer that connects to `"search-index"` reaches whatever package +registered under that id. Nothing is imported until a consumer actually connects. + +This package is a plain library: it registers no plugins and is consumed as a +dependency, never installed as a module. + +**Current example.** The first service built on this is the LLM: `llm-host` +registers the `"llm"` worker pair, and `chat` consumes it. Its op vocabulary and +config handling are its own concern and are documented in +[`../../llm-host/README.md`](../../llm-host/README.md). Nothing in this package is +specific to it. + +## Why this exists + +A Web Worker normally runs in the realm of the code that constructed it. When a +plugin runs in a sandboxed realm, that realm may lack things a worker needs: a +warm cache, a GPU device the host already set up, or host-only state such as a +settings document or a credential that must never enter the sandbox. + +So instead of constructing the worker in the consumer's realm, this package runs +it in the **host realm** and transfers only its `{readable, writable}` stream pair +to the consumer. The worker, and anything host-only it resolves, never leaves the +host; only structured-cloneable frames cross. Because the consumer's code is just +"open a connection, write request frames, read event frames," it is **identical +in and out of isolation**. The only difference is who answers the connection +request and whether the streams are handed over within one realm or transferred +across a boundary. How a given isolation mechanism relays the request and +transfers the streams is that mechanism's concern, not this package's. + +## The three roles + +``` + worker owner (host realm) consumer (any realm) + ------------------------- -------------------- + {type:"patchwork:worker", const {readable, writable, disconnect} + id:"kind", load: () => spec} = await connectWorker("kind", {element}) + spec = {createWorker, open?, handle, abort?} + → serveWorkerSpec(spec, {element}) + -> {readable, writable} + + writable <--- request frames {id, op, ...} ---- (consumer writes) + readable ---- event frames {id, type, ...} ---> (consumer reads) +``` + +- **The consumer** calls `connectWorker(kind, {element})`, or the higher-level + `openSession(kind)` for request/response multiplexing, or (usually) + `connectWorkerClient(kind)` for the service's typed client, and gets a stream + pair. This is the only part that runs in the consumer's realm; it loads + `connect.js` and `client.js`, never `serve.js`. +- **The provider** (`patchwork-worker-provider`, a `patchwork:component` shipped + by the sibling `providers` package and mounted by the host frame) answers the + connection request: it looks up `kind` in the `patchwork:worker` plugin + registry, loads its `WorkerSpec`, and hands that to `serveWorkerSpec`, which it + imports from `@grjte/patchwork-worker/serve`. +- **`serveWorkerSpec`** owns everything kind-agnostic: the stream pair and its + controller lifecycle, **one dedicated worker per connection** (terminated on + teardown; no sharing, no reuse), request-id demux, the reserved `op:"abort"`, + and a bounded `open` warm-up. The spec supplies only the service's op vocabulary. + +### The paired client plugin + +A service package usually also owns the *consume* half of its protocol: the code +that turns a method call into request frames and reads the events back. Rather +than have every consumer import that code from the service package (a build-time +dependency on a registry package, which the repo rules forbid), the package +registers it as a second plugin with the **same id** as its worker, whose `load()` +resolves to a factory `(session) => clientApi`. + +A consumer then calls, from `@grjte/patchwork-worker/client`: + +```js +const api = await connectWorkerClient("kind", { sessionOpts: { idPrefix: "mytool" } }); +``` + +`connectWorkerClient` resolves the `patchwork:worker-client` plugin for the kind +from the registry (bounded wait, so a missing service rejects instead of hanging), +opens a session for the same kind, and returns `factory(session)`. Both halves of +the protocol ship in one package and cannot drift; the consumer's only tie to the +service is the kind string. The lookup is an ordinary registry lookup, so it works +in whatever realm the consumer runs in, provided that realm has a plugin registry +that knows about the service. + +## The WorkerSpec contract + +A service implements this; `serveWorkerSpec` drives it. + +``` + createWorker() construct the compute Worker. Typically stays in the + service's own library, because + new URL("./worker.js", import.meta.url) must resolve + against that module's URL. Called at most once per + connection, lazily, on the first frame that needs it. + + open(ctx)? per-connection warm-up (e.g. resolving host-side config). + ctx = {element} — the provider's mount point, so the + worker can reach host-realm context without the transport + knowing about it. BOUNDED by the transport (a 5s race): + a never-settling open can't wedge frames forever, so the + spec must make falling-through safe. Its resolved value + reaches handle as `io.state`. + + handle(frame, io) turn ONE consumer request frame into worker traffic. + Everything service-specific lives here. May return an + opaque abort token, stored per request. + io = {post, emit, on, workerId, state, ctx} + post(msg, transfer?) send to the worker + emit(frame) enqueue a frame onto the consumer's + readable (auto-tagged with the + caller id) + on(fn) handle worker messages for this + request; return truthy from fn when + the request is complete. A handle + that never calls on() is + fire-and-forget: nothing is tracked + for it and it cannot be aborted. + workerId the id to tag worker payloads with + state whatever open() resolved (or null) + + abort(token, post)? cancel one in-flight request — the spec sends whatever + worker-specific payload stops it. Reached via the reserved + transport op {id, op:"abort"}, which session.js sends. +``` + +`serveWorkerSpec` owns the id lifecycle so an `op:"abort"` that races in while a +request is still being set up is honoured once its token lands, rather than being +dropped. + +## Frames + +Frames are **opaque to this package**: any structured-cloneable object. The +transport only reads `frame.id` (to demux) and `frame.op === "abort"` (the one +reserved op). Everything else is the service's own vocabulary. A connection is +multiplexed by `id`, so one stream pair can carry many overlapping requests: + +``` + request (consumer -> worker): {id, op, ...service-specific...} + events (worker -> consumer): {id, type, ...service-specific...} + a service marks its own terminal frame types; + abort is {id, op:"abort"} (reserved) +``` + +## Discovery and handoff go through patchwork-providers + +The worker channel is an ordinary `patchwork:subscribe` whose `kind` is the `id` of +a registered `patchwork:worker` plugin. The consumer calls `subscribe()`; the +provider answers with `accept()`; the stream pair rides back in the value with the +streams named in the transfer list (**moved, not cloned**, since a `ReadableStream` +can't be structured-cloned). It is the same relay as any other provider: a worker +connection is just a subscription whose value happens to carry transferred streams. + +``` + consumer answering side + -------- -------------- + subscribe(el, {type:"patchwork:worker-channel", ────────► (mounted provider element + kind}) answers via accept()) + serveWorkerSpec(spec) -> streams + listener({readable, writable}) ◄───────── respond({readable,writable}, + (streams TRANSFERRED, not cloned) [readable,writable]) // TRANSFER +``` + +Because it is an ordinary provider subscription and the streams are transferable, +a consumer's code is the same whether the provider that answers is in its own +realm or on the far side of an isolation boundary. The transport doesn't know or +care which. An isolation mechanism that wants to support workers needs to do two +things: relay this subscription to a realm where the provider is mounted, and +transfer the answered stream pair back. Everything else is unchanged. + +If nothing answers, `connectWorker` **rejects**: either a provider claimed the +subscription and answered a `null` value (an explicit refusal, immediate), or the +bounded discovery wait expired unclaimed. There is deliberately **no local-worker +fallback**: if a consumer could construct its own worker when discovery failed, a +consumer that imports a service package would register that package's worker as +an import side-effect and silently serve itself, defeating the point of running +the worker in the host. A consumer treats the rejection as "worker unavailable" +and degrades. + +## Sessions, broadcasts, and failure + +`openSession(kind)` opens the connection lazily on the first `request()`, shares +it across requests, and drops it whenever it fails or ends so the next request +reconnects. `session.close()` drops it on purpose: the serve half sees its +streams end and terminates the dedicated worker. + +Frames the worker posts **without an `id`** (progress, a status line, its own +error events) are connection-wide broadcasts. The serve half puts them on the +readable once; the session delivers each to every in-flight request's `onFrame`, +so a caller sees the worker's status exactly as it would from a same-realm worker. + +A request never hangs on a dead worker: if the connection ends, the worker +crashes, or the worker cannot be constructed, the serve half tears down, the +readable closes, and every in-flight request rejects. Aborting a request +(`abort()` or an `AbortSignal`) sends the reserved `op:"abort"` frame so the +service can cancel the matching worker work. + +## Files + +- `connect.js` — the consumer transport: `connectWorker` (discovery + handoff), + `openSession` (request/response multiplexing over the pair), and the protocol + constants (`CHANNEL_SELECTOR`, `WORKER_PLUGIN_TYPE`, `WORKER_CLIENT_PLUGIN_TYPE`). +- `session.js` — `openSession` internals: id tagging, demux, broadcast fan-out, + abort, reconnect, `close()`. +- `serve.js` — `serveWorkerSpec`: streams, per-connection worker, id demux, abort. + The serve half, imported by the host provider as + `@grjte/patchwork-worker/serve`. +- `client.js` — `connectWorkerClient`: resolves the paired + `patchwork:worker-client` plugin from the registry and binds its factory to + `openSession(kind)`. The one file here that touches the plugin registry. +- `index.js` — barrel over connect.js and client.js. No `plugins` array: this + package is a library, not a module. + +Layering, by convention rather than test: `connect.js`/`session.js` stay free of +the plugin registry; `client.js` never imports `serve.js`; `serve.js` is +host-only and imports nothing from the consumer side. + +## ⚠ Nothing in this package may hold host-only state or secrets + +The consumer half (`connect.js`, `client.js`) is loaded into whatever realm the +consumer runs in, including a sandboxed one, and a consumer that can load one +file of a package should be assumed able to load any of them. So nothing here may +hold state or secrets that a sandboxed consumer must not see. Any host-only state +or credential a worker needs lives in that worker's own service package, resolved +by its `WorkerSpec` in the host realm, and the host-realm provider that runs +workers lives in the `providers` package. This package holds no state and imports +nothing service-specific. `client.js` touches only the plugin registry +(descriptors and lazy loaders); the factory it returns is the *service package's* +code, fetched by the same loader that fetches any plugin. diff --git a/libraries/patchwork-worker/client.js b/libraries/patchwork-worker/client.js new file mode 100644 index 00000000..aaa230c8 --- /dev/null +++ b/libraries/patchwork-worker/client.js @@ -0,0 +1,92 @@ +/** + * connectWorkerClient — resolve a worker's typed client from the plugin registry + * and bind it to a session, so a consumer never imports the service package. + * + * The transport pairs two plugin types by `id` (the worker `kind`): + * + * {type: "patchwork:worker", id: "llm", load: () => spec} + * {type: "patchwork:worker-client", id: "llm", load: () => (session) => clientApi} + * + * The first is the SERVE half: the host-realm provider runs its WorkerSpec. The + * second is the CONSUME half: a factory that turns an open `openSession(kind)` + * session into the service's API (`{generate}` for the LLM, say). Both ship from + * the service package, so the frame vocabulary they share cannot drift apart — + * and a tool that calls + * + * const llm = await connectWorkerClient("llm", {sessionOpts: {idPrefix: "chat"}}) + * await llm.generate(messages, {element, ...}) + * + * has no build-time dependency on that package. The lookup is late-bound through + * the registry and rejects if nothing registered the kind. + * + * Works in any realm that has a plugin registry the service is registered in: + * the lookup is an ordinary registry lookup, and the session is served by the + * host provider wherever it is mounted, in-realm or across an isolation + * boundary — the same call either way. + * + * Layering: this is the one file in the package that touches the plugin + * registry. connect.js / session.js stay registry-free; serve.js is host-only + * and is never imported here. + */ + +import {getRegistry} from "@inkandswitch/patchwork-plugins" +import {openSession, WORKER_CLIENT_PLUGIN_TYPE} from "./connect.js" + +/** + * How long to wait for the client plugin to be registered AND loaded before + * giving up. `loadWhenReady` is unbounded by design (it waits for a late + * registration — in a sandboxed realm the service may be registered after the + * consumer has already mounted), so this is the only thing standing between a + * missing service package and a consumer that awaits forever. + */ +const LOAD_TIMEOUT_MS = 10000 + +/** + * @typedef {import("./session.js").Session} WorkerSession + * @typedef {(session: WorkerSession) => any} WorkerClientFactory + * @typedef {{type: "patchwork:worker-client", id: string, name?: string, load: () => Promise}} WorkerClientPlugin + */ + +/** + * Resolve the `patchwork:worker-client` plugin for `kind`, open a session for + * the same kind, and return `factory(session)` — the service's client API. + * + * Rejects if no plugin for `kind` is registered and loaded within `timeoutMs`, + * or if the plugin did not resolve to a function. Callers should not cache a + * rejected result: a later call may succeed once the service package registers. + * + * @param {string} kind + * @param {{ + * sessionOpts?: import("./session.js").SessionOpts, + * timeoutMs?: number, + * }} [opts] + * @returns {Promise} + */ +export async function connectWorkerClient(kind, opts = {}) { + const registry = getRegistry(WORKER_CLIENT_PLUGIN_TYPE) + const timeoutMs = opts.timeoutMs ?? LOAD_TIMEOUT_MS + + /** @type {ReturnType | undefined} */ + let timer + const timeout = new Promise((_, reject) => { + timer = setTimeout( + () => reject(new Error(`timed out loading worker-client plugin "${kind}"`)), + timeoutMs + ) + }) + let plugin + try { + // `loadWhenReady` (not `load`): a plugin registered-but-not-yet-loaded, or + // registered after this call, still resolves instead of being reported + // missing. The race bounds it. + plugin = await Promise.race([registry.loadWhenReady(kind), timeout]) + } finally { + clearTimeout(timer) + } + + const factory = /** @type {any} */ (plugin)?.module + if (typeof factory !== "function") { + throw new Error(`worker-client plugin "${kind}" did not resolve to a factory`) + } + return factory(openSession(kind, opts.sessionOpts)) +} diff --git a/libraries/patchwork-worker/client.test.js b/libraries/patchwork-worker/client.test.js new file mode 100644 index 00000000..711a852f --- /dev/null +++ b/libraries/patchwork-worker/client.test.js @@ -0,0 +1,75 @@ +import {describe, it, expect, afterEach, vi} from "vitest" + +// connectWorkerClient resolves the client factory from the host plugin registry. +// Stub it before importing, so these tests exercise the helper's own logic +// (bounded wait / late registration / shape check) without the real platform. +const registry = { + plugins: new Map(), + waiters: [], + has(id) { + return this.plugins.has(id) + }, + async loadWhenReady(id) { + // Mirrors the real registry: waits for a late registration rather than + // reporting the plugin missing. + if (this.plugins.has(id)) return {module: this.plugins.get(id)} + return new Promise((resolve) => { + this.waiters.push({id, resolve}) + }) + }, + register(id, factory) { + this.plugins.set(id, factory) + for (const w of this.waiters) { + if (w.id === id) w.resolve({module: factory}) + } + this.waiters = this.waiters.filter((w) => w.id !== id) + }, + reset() { + this.plugins.clear() + this.waiters = [] + }, +} + +vi.mock("@inkandswitch/patchwork-plugins", () => ({ + getRegistry: () => registry, +})) + +const {connectWorkerClient} = await import("./client.js") + +afterEach(() => { + registry.reset() +}) + +describe("connectWorkerClient", () => { + it("resolves the registered factory bound to a session for the same kind", async () => { + let seen = null + registry.register("llm", (session) => { + seen = session + return {generate: () => "ok"} + }) + const client = await connectWorkerClient("llm", {sessionOpts: {idPrefix: "t"}}) + expect(client.generate()).toBe("ok") + // The factory receives an openSession() session. + expect(typeof seen.request).toBe("function") + expect(typeof seen.close).toBe("function") + }) + + it("waits for a plugin registered after the call", async () => { + const pending = connectWorkerClient("llm", {timeoutMs: 1000}) + await new Promise((r) => setTimeout(r, 10)) + registry.register("llm", () => ({late: true})) + await expect(pending).resolves.toEqual({late: true}) + }) + + it("rejects after the bounded wait when nothing registers the kind", async () => { + await expect(connectWorkerClient("nobody", {timeoutMs: 50})).rejects.toThrow(/timed out/) + }) + + it("rejects when the plugin does not resolve to a factory", async () => { + registry.register("llm", {not: "a function"}) + await expect(connectWorkerClient("llm", {timeoutMs: 50})).rejects.toThrow( + /did not resolve to a factory/ + ) + }) + +}) diff --git a/libraries/patchwork-worker/connect.js b/libraries/patchwork-worker/connect.js new file mode 100644 index 00000000..d8cc690b --- /dev/null +++ b/libraries/patchwork-worker/connect.js @@ -0,0 +1,233 @@ +/** + * connectWorker — a generic, transferable-stream connection to a worker-backed + * service. + * + * A tool asks to connect to a worker of some `kind` and, for the lifetime of + * that connection, holds a `WritableStream` (to send request frames) and a + * `ReadableStream` (to receive event frames). The worker itself, and any + * privileged setup it needs (config/secrets), live on whichever side answers the + * connection — the SAME realm when there's no isolation boundary, or the host + * realm when the consumer runs in a sandboxed realm. Either way the consumer sees + * an identical `{readable, writable}` pair; the streams are transferable, so they + * cross an isolation boundary unchanged. + * + * This file is the CONSUMER half. `connectWorker(kind, {element})` returns + * `{readable, writable, disconnect}`; a provider answers via the discovery event + * (in-realm, or relayed from the host realm by whatever isolation mechanism is + * in use). If nothing answers, it rejects. There is no local fallback. + * + * The SERVING half is `serveWorkerSpec` (./serve.js), driven by the host-realm + * `patchwork-worker-provider` component (shipped by the `providers` package), + * which resolves a WorkerSpec for the requested `kind` from the + * `patchwork:worker` plugin registry. Consumers never import serve.js. + * + * This module is service-agnostic: it knows nothing about LLMs. Frame shapes are + * the service's concern; connect.js treats them as opaque structured-cloneable + * values, tagged with an `id` so many requests can multiplex over one connection. + * + * Discovery/handoff goes through patchwork-providers: this file calls + * `subscribe()` and the host provider answers with `accept()`. The stream pair + * rides in the value; `respond`'s transfer list moves rather than clones it, so + * the streams stay live across the isolation boundary. + * + * Layering: this file stays free of the plugin registry so the sandbox-loaded + * transport carries no more than it needs; the registry lookup lives in + * ./client.js. + */ + +import {createOpenSession} from "./session.js" + +// --- Protocol constants ----------------------------------------------------- +// Deliberately STRINGS, not Symbols. Comparisons against them are `===` on the +// value (here, in the host worker provider, and possibly as inlined literals in +// code that relays the subscription across a boundary), so they keep working +// even if this module is evaluated more than once. A Symbol would silently stop +// matching. + +/** + * The selector type used to discover a worker-connection provider. A consumer + * dispatches a `patchwork:subscribe` for `{ type: CHANNEL_SELECTOR, kind }` + * carrying a MessagePort in `detail.port`. The answering side replies over that + * port, in the standard providers envelope (`{type:"change", value}`), with + * exactly one of: + * + * {readable, writable} — success; the pair is TRANSFERRED, not cloned + * null — refused; fail fast + * + * Silence is also a valid outcome (nothing is mounted to answer), which the + * consumer's bounded discovery timeout covers. Answering sides that KNOW they're + * refusing should respond `null` rather than staying silent. + */ +export const CHANNEL_SELECTOR = "patchwork:worker-channel" + +/** + * The plugin type a service package registers to offer a worker. Its `id` is + * the worker `kind`; `load()` resolves to a WorkerSpec (see ./serve.js). + */ +export const WORKER_PLUGIN_TYPE = "patchwork:worker" + +/** + * The paired plugin type a service package registers to offer a typed client + * for its worker. Same `id` as the worker; `load()` resolves to a factory + * `(session) => clientApi` (see ./client.js). + */ +export const WORKER_CLIENT_PLUGIN_TYPE = "patchwork:worker-client" + +/** + * BACKSTOP: how long to wait for a provider to answer before giving up. + * + * Not control flow. A mounted provider either serves the kind (streams) or + * refuses it (`null`), and both are immediate — so in a healthy frame this timer + * never fires. It exists because an unclaimed `patchwork:subscribe` never + * settles by design (a subscription may wait for a provider that mounts later). + * Without a bound, a missing provider would hang the caller forever instead of + * erroring. + */ +const DISCOVERY_TIMEOUT_MS = 8000 + +/** @typedef {{ readable: ReadableStream, writable: WritableStream }} WorkerStreams */ +/** @typedef {WorkerStreams & { disconnect: () => void }} WorkerConnection */ +/** + * What a `patchwork:worker` plugin's `load()` must resolve to: a WorkerSpec that + * `serveWorkerSpec` (serve.js) drives. The transport owns the streams, ids, and + * per-connection worker; the spec owns only its op vocabulary. `open(ctx)` gets + * the provider's mount point, so a worker can resolve host-realm context (a + * settings doc, say) without the provider knowing about that service. + * + * (Shape mirrored from serve.js's `WorkerSpec`, kept as a local typedef so this + * sandbox-loaded file does not import the host-only serve module. Types erase, + * so this costs nothing at runtime.) + * @typedef {{ + * createWorker: () => Worker | Promise, + * open?: (ctx: {element?: HTMLElement}) => any, + * handle: (frame: any, io: import("./serve.js").IO) => any, + * abort?: (token: any, post: (msg: any, transfer?: Transferable[]) => void) => void, + * }} WorkerSpec + */ + +/** + * The descriptor a package puts in its `plugins` array to offer a worker. + * @typedef {{type: "patchwork:worker", id: string, name?: string, load: () => Promise}} WorkerPlugin + */ + +/** + * Open a connection to a worker of `kind`. + * + * A `patchwork:subscribe` provider for `{type: CHANNEL_SELECTOR, kind}` above + * `element` answers, transferring streams back over the port. In the host realm + * that's a mounted worker provider; in a sandboxed realm it's whatever relays + * the subscription to the host and transfers the host's streams back across the + * boundary. The consumer gets the same `{readable, writable, disconnect}` either + * way and never learns which answered. + * + * There is deliberately NO fallback to a locally-constructed worker. Inside the + * sandbox that fallback was a hole: any in-boundary tool that imported a service + * package would register its worker as an import side-effect, and a connection + * that should have been refused would instead run the worker in the opaque + * origin — no shared model cache, no host config, and no signal that the + * isolation boundary had been bypassed. Serving is the provider's job; a + * consumer with no provider ancestor fails loudly instead. + * + * @param {string} kind + * @param {{ element: HTMLElement }} opts a node inside a mounted , + * to dispatch the discovery subscribe from + * @returns {Promise} + */ +export async function connectWorker(kind, opts) { + const el = opts?.element + if (!el) { + throw new Error( + `no worker available for kind "${kind}": no element to discover a provider from` + ) + } + const streams = await discoverViaProvider(el, kind) + if (!streams) { + throw new Error(`no worker available for kind "${kind}"`) + } + return withDisconnect(streams) +} + +/** + * Wrap a {readable, writable} with a disconnect() that tears both ends down. + * Only valid while the caller holds no reader/writer lock (a locked stream + * rejects cancel/abort); a consumer that has taken locks releases through them. + * @param {WorkerStreams} streams + * @returns {WorkerConnection} + */ +function withDisconnect(streams) { + return { + ...streams, + disconnect() { + streams.readable.cancel().catch(() => {}) + streams.writable.abort().catch(() => {}) + }, + } +} + +/** + * Ask a `patchwork:worker-channel` provider to open a connection, via providers + * `subscribe()`. Resolves null on an explicit refusal, or if nothing answers + * within the discovery timeout. + * + * @param {HTMLElement} element + * @param {string} kind + * @returns {Promise} + */ +function discoverViaProvider(element, kind) { + return new Promise((resolve) => { + let settled = false + /** @type {(() => void) | null} */ + let unsubscribe = null + + const finish = (/** @type {WorkerStreams | null} */ v) => { + if (settled) return + settled = true + clearTimeout(timer) + // One-shot: the provider may hold the subscription open, but we only ever + // want the first answer. + try { + unsubscribe?.() + } catch {} + resolve(v) + } + const timer = setTimeout(() => finish(null), DISCOVERY_TIMEOUT_MS) + + // `subscribe` is typed `T extends JSONValue`, but a transferred stream pair + // is not JSON — the cast is that constraint biting. `accept` + // is deliberately unconstrained for exactly this case; the consumer half + // was never widened to match. Worth fixing upstream. + void import("@inkandswitch/patchwork-providers") + .then(({subscribe}) => { + if (settled) return + unsubscribe = /** @type {any} */ (subscribe)( + element, + {type: CHANNEL_SELECTOR, kind}, + (/** @type {WorkerStreams | null} */ value) => { + // `null` is an explicit refusal — settle now rather than burning + // the full discovery timeout for an answer that already exists. + if (value && value.readable && value.writable) { + finish({readable: value.readable, writable: value.writable}) + } else { + finish(null) + } + } + ) + // A provider can answer during the dispatch above, in which case + // `finish` ran while `unsubscribe` was still null. Tear down now. + if (settled) { + try { + unsubscribe?.() + } catch {} + } + }) + .catch((err) => { + console.error("[patchwork-worker] failed to load patchwork-providers:", err) + finish(null) + }) + }) +} + +// --- Session layer ---------------------------------------------------------- +// session.js takes `connectWorker` as a parameter rather than importing it, so +// the dependency runs one way and there's no import cycle. +export const openSession = createOpenSession(connectWorker) diff --git a/libraries/patchwork-worker/connect.test.js b/libraries/patchwork-worker/connect.test.js new file mode 100644 index 00000000..81fad3c0 --- /dev/null +++ b/libraries/patchwork-worker/connect.test.js @@ -0,0 +1,337 @@ +import {describe, it, expect, afterEach, vi} from "vitest" +import {existsSync, readFileSync} from "node:fs" +import {join} from "node:path" +import {accept} from "@inkandswitch/patchwork-providers" +import {connectWorker, openSession} from "./connect.js" +import {serveWorkerSpec} from "./serve.js" + +// A minimal stand-in for the worker provider: answers worker-channel +// subscriptions for the kinds it knows, and refuses the ones it doesn't. Uses +// the real `accept()`, so these tests exercise the actual providers envelope. +function serveKinds(kinds) { + const listener = (e) => { + const {selector, port} = e.detail ?? {} + if (selector?.type !== "patchwork:worker-channel" || !port) return + const run = kinds[selector.kind] + if (!run) return // decline, do NOT claim — let it bubble + accept(e, (respond) => { + Promise.resolve(run()) + .then((streams) => { + respond({readable: streams.readable, writable: streams.writable}, [ + streams.readable, + streams.writable, + ]) + }) + .catch(() => respond(null)) + }) + } + document.addEventListener("patchwork:subscribe", listener) + return () => document.removeEventListener("patchwork:subscribe", listener) +} + +/** A provider that refuses every worker-channel subscription with `null`. */ +function refuseAll(kind) { + const refuse = (e) => { + const {selector, port} = e.detail ?? {} + if (selector?.type !== "patchwork:worker-channel" || !port) return + if (kind && selector.kind !== kind) return + accept(e, (respond) => respond(null)) + } + document.addEventListener("patchwork:subscribe", refuse) + return () => document.removeEventListener("patchwork:subscribe", refuse) +} + +/** Echoes one token + a terminal result for every request frame written. */ +function echoWorker() { + let controller + const readable = new ReadableStream({start: (c) => (controller = c)}) + const writable = new WritableStream({ + write(frame) { + if (frame.op === "abort") return + controller.enqueue({id: frame.id, type: "token", delta: "hi", text: "hi"}) + controller.enqueue({id: frame.id, type: "result", text: "hi"}) + }, + }) + return {writable, readable} +} + +/** A hand-driven stream pair: the test controls the readable and sees writes. */ +function manualWorker() { + let controller + const written = [] + const readable = new ReadableStream({start: (c) => (controller = c)}) + const writable = new WritableStream({write: (f) => void written.push(f)}) + return {readable, writable, written, push: (f) => controller.enqueue(f), end: () => controller.close()} +} + +function mountElement() { + const el = document.createElement("div") + document.body.appendChild(el) + cleanups.push(() => el.remove()) + return el +} + +let cleanups = [] +afterEach(() => { + cleanups.forEach((f) => f()) + cleanups = [] + vi.useRealTimers() +}) + +async function readN(readable, n) { + const reader = readable.getReader() + const out = [] + while (out.length < n) { + const {value, done} = await reader.read() + if (done) break + out.push(value) + } + reader.releaseLock() + return out +} + +const tick = (ms = 20) => new Promise((r) => setTimeout(r, ms)) + +const TERMINAL = { + result: (f) => f.text, + error: (f) => { + throw new Error(f.message) + }, +} + +describe("connectWorker", () => { + it("connects when a provider answers, and the transferred streams are live", async () => { + const kind = "echo-" + Math.random() + cleanups.push(serveKinds({[kind]: echoWorker})) + + const conn = await connectWorker(kind, {element: mountElement()}) + expect(conn.readable).toBeInstanceOf(ReadableStream) + expect(conn.writable).toBeInstanceOf(WritableStream) + expect(typeof conn.disconnect).toBe("function") + + const writer = conn.writable.getWriter() + await writer.write({op: "generate", id: "x"}) + writer.releaseLock() + const frames = await readN(conn.readable, 2) + expect(frames[0]).toMatchObject({id: "x", type: "token"}) + expect(frames[1]).toMatchObject({id: "x", type: "result"}) + }) + + it("multiplexes many request ids over one connection", async () => { + const kind = "echo-" + Math.random() + cleanups.push(serveKinds({[kind]: echoWorker})) + + const conn = await connectWorker(kind, {element: mountElement()}) + const writer = conn.writable.getWriter() + await writer.write({op: "generate", id: "a"}) + await writer.write({op: "generate", id: "b"}) + writer.releaseLock() + + const ids = (await readN(conn.readable, 4)).map((f) => f.id) + expect(ids.filter((x) => x === "a")).toHaveLength(2) + expect(ids.filter((x) => x === "b")).toHaveLength(2) + }) + + it("fails fast on an explicit refusal", async () => { + // A refusal the provider already knows about must not cost the discovery + // timeout — this is the path the isolation bridge uses when the worker + // channel isn't in shared-providers. + cleanups.push(refuseAll()) + const started = Date.now() + await expect(connectWorker("nope", {element: mountElement()})).rejects.toThrow( + /no worker available/ + ) + expect(Date.now() - started).toBeLessThan(1000) + }) + + it("rejects without an element to discover from", async () => { + await expect(connectWorker("anything", {})).rejects.toThrow(/no element/) + }) + + it("times out when nothing answers at all", async () => { + // Nothing is mounted, so this waits out DISCOVERY_TIMEOUT_MS — a backstop, + // not control flow. Fake timers so the suite doesn't wait 8 s for real. + vi.useFakeTimers({toFake: ["setTimeout", "clearTimeout"]}) + const pending = connectWorker("nobody-" + Math.random(), {element: mountElement()}) + const assertion = expect(pending).rejects.toThrow(/no worker available/) + await vi.advanceTimersByTimeAsync(8001) + await assertion + }) +}) + +describe("openSession", () => { + it("resolves on the terminal frame and streams the rest", async () => { + const kind = "echo-" + Math.random() + cleanups.push(serveKinds({[kind]: echoWorker})) + + const session = openSession(kind, {element: mountElement()}) + const seen = [] + const {promise} = session.request( + {op: "generate"}, + {terminal: TERMINAL, onFrame: (f) => seen.push(f.type)} + ) + await expect(promise).resolves.toBe("hi") + expect(seen).toEqual(["token"]) + }) + + it("does not cache a failed connection — a later request reconnects", async () => { + // One early failure must not poison the session: once a provider shows up, + // the next request must find it. + const kind = "echo-" + Math.random() + const session = openSession(kind, {element: mountElement()}) + + const stopRefusing = refuseAll(kind) + const first = session.request({op: "generate"}, {terminal: TERMINAL}) + await expect(first.promise).rejects.toThrow(/no worker available/) + stopRefusing() + + cleanups.push(serveKinds({[kind]: echoWorker})) + const second = session.request({op: "generate"}, {terminal: TERMINAL}) + await expect(second.promise).resolves.toBe("hi") + }) + + it("fails in-flight requests when the connection drops, then reconnects", async () => { + // A request awaiting a terminal frame that can no longer arrive must reject, + // not hang; and the dead connection must not be reused. + const kind = "drop-" + Math.random() + const workers = [] + cleanups.push( + serveKinds({ + [kind]: () => { + const w = manualWorker() + workers.push(w) + return w + }, + }) + ) + const session = openSession(kind, {element: mountElement()}) + + const first = session.request({op: "generate"}, {terminal: TERMINAL}) + await tick() + workers[0].end() // stream ends with no terminal frame + await expect(first.promise).rejects.toThrow(/closed before the request completed/) + + const second = session.request({op: "generate"}, {terminal: TERMINAL}) + await tick() + expect(workers).toHaveLength(2) // a new connection, not the dead one + expect(workers[1].written[0]).toMatchObject({op: "generate"}) + workers[1].push({id: workers[1].written[0].id, type: "result", text: "ok"}) + await expect(second.promise).resolves.toBe("ok") + }) + + it("fans id-less broadcast frames out to every in-flight request", async () => { + // The LLM worker posts model-download progress and its own errors as + // `{type:"status"}` with no id. Every caller waiting on that worker hears it. + const kind = "bcast-" + Math.random() + let w + cleanups.push(serveKinds({[kind]: () => (w = manualWorker())})) + const session = openSession(kind, {element: mountElement()}) + + const seenA = [] + const seenB = [] + const a = session.request({op: "generate"}, {terminal: TERMINAL, onFrame: (f) => seenA.push(f)}) + const b = session.request({op: "generate"}, {terminal: TERMINAL, onFrame: (f) => seenB.push(f)}) + await tick() + w.push({type: "status", message: "Downloading model weights… 40%"}) + await tick() + expect(seenA).toEqual([{type: "status", message: "Downloading model weights… 40%"}]) + expect(seenB).toEqual([{type: "status", message: "Downloading model weights… 40%"}]) + + // A settled request no longer hears broadcasts. + w.push({id: w.written[0].id, type: "result", text: "done"}) + await expect(a.promise).resolves.toBe("done") + w.push({type: "status", message: "later"}) + await tick() + expect(seenA).toHaveLength(1) + expect(seenB).toHaveLength(2) + w.push({id: w.written[1].id, type: "result", text: "done"}) + await b.promise + }) + + it("close() tears the served connection down and the next request reconnects", async () => { + // End to end through serveWorkerSpec: closing the session ends both streams, + // so the host side terminates its dedicated worker. + const kind = "close-" + Math.random() + const workers = [] + const spec = { + createWorker: () => { + const fake = { + terminated: false, + onmessage: null, + postMessage() {}, + terminate() { + fake.terminated = true + }, + } + workers.push(fake) + return fake + }, + handle: (frame, io) => { + io.on(() => false) + io.post({id: io.workerId}) + }, + } + cleanups.push(serveKinds({[kind]: () => serveWorkerSpec(spec, {})})) + const session = openSession(kind, {element: mountElement()}) + + const first = session.request({op: "generate"}, {terminal: TERMINAL}) + await tick() + expect(workers).toHaveLength(1) + session.close() + await expect(first.promise).rejects.toThrow(/was closed/) + await tick() + expect(workers[0].terminated).toBe(true) + + const second = session.request({op: "generate"}, {terminal: TERMINAL}) + second.promise.catch(() => {}) + await tick() + expect(workers).toHaveLength(2) + session.close() + }) + + it("forwards an abort that fires while the connection is still opening", async () => { + const kind = "abort-" + Math.random() + let w + cleanups.push(serveKinds({[kind]: () => (w = manualWorker())})) + const session = openSession(kind, {element: mountElement()}) + + const {promise, abort} = session.request({op: "generate"}, {terminal: TERMINAL}) + abort() // before discovery has resolved + await expect(promise).rejects.toThrow(/Aborted/) + await tick() + // The request frame was already on its way; the abort follows it. + expect(w.written.map((f) => f.op)).toEqual(["generate", "abort"]) + expect(w.written[1].id).toBe(w.written[0].id) + }) + + it("does not open a connection to abort a request that was never sent", async () => { + let dispatches = 0 + const count = (e) => { + if (e.detail?.selector?.type === "patchwork:worker-channel") dispatches++ + } + document.addEventListener("patchwork:subscribe", count) + cleanups.push(() => document.removeEventListener("patchwork:subscribe", count)) + + const session = openSession("never-" + Math.random(), {element: mountElement()}) + const {promise} = session.request( + {op: "generate"}, + {terminal: TERMINAL, signal: AbortSignal.abort()} + ) + await expect(promise).rejects.toThrow(/Aborted/) + expect(dispatches).toBe(0) + }) +}) + +describe("package shape", () => { + it("publishes every exported subpath", () => { + const dir = process.cwd() + const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")) + expect(pkg.files).toContain("types") + for (const [subpath, target] of Object.entries(pkg.exports)) { + const file = target.default + expect(existsSync(join(dir, file)), `${subpath} -> ${file}`).toBe(true) + expect(pkg.files, `${file} in files`).toContain(file.replace(/^\.\//, "")) + expect(existsSync(join(dir, target.types)), `${subpath} types`).toBe(true) + } + }) +}) diff --git a/libraries/patchwork-worker/index.js b/libraries/patchwork-worker/index.js new file mode 100644 index 00000000..e2916d3a --- /dev/null +++ b/libraries/patchwork-worker/index.js @@ -0,0 +1,66 @@ +/** + * @grjte/patchwork-worker — run a worker in the host realm, hand any consumer a + * transferable stream pair, and have it work the same inside or outside a + * Patchwork isolation boundary. + * + * This is a plain library: it registers no plugins and is consumed as a + * dependency, never installed as a module. Four pieces: + * + * connect.js the CONSUMER transport. `connectWorker(kind, {element})` returns + * `{readable, writable, disconnect}`; `openSession(kind)` layers + * request/response multiplexing on top. Also home to the protocol + * constants (`CHANNEL_SELECTOR`, the two plugin type strings). + * session.js `openSession` internals: id tagging, demux, broadcast fan-out, + * abort, reconnect, close. + * serve.js `serveWorkerSpec(spec, ctx)` — the SERVING half: runs one worker + * per connection and owns the stream pair + id demux. Host-only; + * driven by the `patchwork-worker-provider` component (shipped by + * the `providers` package), which resolves a WorkerSpec for the + * requested `kind` from the `patchwork:worker` plugin registry. + * client.js `connectWorkerClient(kind)` — resolves the service's typed client + * from the `patchwork:worker-client` registry and binds it to + * `openSession(kind)`. The one file here that touches the registry. + * + * Offering a worker is declarative — a service package registers a PAIR of + * plugins under one id (the worker `kind`), and nothing is imported until someone + * connects: + * + * export const plugins = [ + * {type: "patchwork:worker", id: "llm", name: "LLM", + * async load() { return makeLLMWorkerSpec(...) }}, // serve half + * {type: "patchwork:worker-client", id: "llm", name: "LLM client", + * async load() { return makeLLMClient }}, // consume half + * ] + * + * where the worker's `load()` resolves to a WorkerSpec — `{createWorker, open?, + * handle, abort?}` — and the client's resolves to `(session) => clientApi`. A + * tool then calls `connectWorkerClient("llm")` and gets the API without a + * build-time dependency on the service package. + * + * ⚠ Nothing in this package may hold host-only state or secrets. connect.js and + * client.js are loaded into the consumer's realm, sandboxed or not, and a + * consumer that can load one file of a package should be assumed able to load + * any of them. Host-realm work belongs in the provider (providers package) or in + * the service package that owns the worker. + */ + +export { + connectWorker, + openSession, + CHANNEL_SELECTOR, + WORKER_PLUGIN_TYPE, + WORKER_CLIENT_PLUGIN_TYPE, +} from "./connect.js" + +export {connectWorkerClient} from "./client.js" + +/** + * Types a service package needs to type its `plugins` entries. + * @typedef {import("./connect.js").WorkerSpec} WorkerSpec + * @typedef {import("./connect.js").WorkerPlugin} WorkerPlugin + * @typedef {import("./connect.js").WorkerStreams} WorkerStreams + * @typedef {import("./connect.js").WorkerConnection} WorkerConnection + * @typedef {import("./client.js").WorkerClientPlugin} WorkerClientPlugin + * @typedef {import("./client.js").WorkerClientFactory} WorkerClientFactory + * @typedef {import("./session.js").Session} Session + */ diff --git a/libraries/patchwork-worker/package.json b/libraries/patchwork-worker/package.json new file mode 100644 index 00000000..2ecc47f5 --- /dev/null +++ b/libraries/patchwork-worker/package.json @@ -0,0 +1,57 @@ +{ + "name": "@grjte/patchwork-worker", + "version": "0.0.2", + "description": "Run a worker in the host realm and hand any consumer a transferable stream pair — the same code inside or outside a Patchwork isolation boundary. Owns the patchwork:worker plugin type and the connect/session/serve transport; the host-realm provider that drives it ships from the providers package.", + "type": "module", + "main": "index.js", + "types": "./types/index.d.ts", + "exports": { + ".": { + "types": "./types/index.d.ts", + "default": "./index.js" + }, + "./connect": { + "types": "./types/connect.d.ts", + "default": "./connect.js" + }, + "./session": { + "types": "./types/session.d.ts", + "default": "./session.js" + }, + "./serve": { + "types": "./types/serve.d.ts", + "default": "./serve.js" + }, + "./client": { + "types": "./types/client.d.ts", + "default": "./client.js" + } + }, + "files": [ + "types", + "index.js", + "connect.js", + "session.js", + "serve.js", + "client.js", + "README.md" + ], + "author": "grjte", + "license": "MIT", + "scripts": { + "build": "pnpm build:types", + "build:types": "tsc", + "push": "pnpm build && pushwork sync", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@inkandswitch/patchwork-plugins": "^0.0.11", + "@inkandswitch/patchwork-providers": "^0.5.1" + }, + "devDependencies": { + "happy-dom": "^15.11.7", + "typescript": "^5.9.3", + "vitest": "^3.2.7" + } +} diff --git a/libraries/patchwork-worker/pnpm-lock.yaml b/libraries/patchwork-worker/pnpm-lock.yaml new file mode 100644 index 00000000..22dc9432 --- /dev/null +++ b/libraries/patchwork-worker/pnpm-lock.yaml @@ -0,0 +1,1313 @@ +--- +lockfileVersion: '9.0' + +importers: + + .: + configDependencies: + pnpm-plugin-patchwork: + specifier: 0.4.1 + version: 0.4.1 + +packages: + + pnpm-plugin-patchwork@0.4.1: + resolution: {integrity: sha512-IzQJkoeQaSaYSa1bRko3KUJ+6nBmYg8Qxuyy0llnhPfAGSDP/+gM/Z7R0HGLtTpCE3K57CXS9DnAItqb/pNIZQ==} + +snapshots: + + pnpm-plugin-patchwork@0.4.1: {} + +--- +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@inkandswitch/patchwork-plugins': + specifier: ^0.0.11 + version: 0.0.11(@automerge/automerge-repo@2.6.0-alpha.3)(@automerge/automerge@3.5.0)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.6.0-alpha.3)(@automerge/automerge@3.5.0)) + '@inkandswitch/patchwork-providers': + specifier: ^0.5.1 + version: 0.5.2(@automerge/automerge-repo@2.6.0-alpha.3) + devDependencies: + happy-dom: + specifier: ^15.11.7 + version: 15.11.7 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vitest: + specifier: ^3.2.7 + version: 3.2.7(@types/debug@4.1.13)(@types/node@20.19.43)(happy-dom@15.11.7) + +packages: + + '@automerge/automerge-repo@2.6.0-alpha.3': + resolution: {integrity: sha512-Rn/KdoVHUQwYU0TXqHyy9PdBgVE009JJWnh2YxT2blk5EnbZckh+RfGfu6ngjMGw0DZcZtJLvK3dKDNdAvtQVA==} + engines: {node: '>=22.13'} + + '@automerge/automerge@3.5.0': + resolution: {integrity: sha512-ejbqJWfXWM2QbTcUK/5ugYiQiyOP7LYVAp/RuSvthCc07oek0FClMxEuouuhjrLjVPDSI34RP1ftGc45PF7WRA==} + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + resolution: {integrity: sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA==} + cpu: [arm64] + os: [darwin] + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + resolution: {integrity: sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q==} + cpu: [x64] + os: [darwin] + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + resolution: {integrity: sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg==} + cpu: [arm64] + os: [linux] + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + resolution: {integrity: sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ==} + cpu: [arm] + os: [linux] + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + resolution: {integrity: sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA==} + cpu: [x64] + os: [linux] + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + resolution: {integrity: sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA==} + cpu: [x64] + os: [win32] + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@inkandswitch/patchwork-filesystem@0.0.8': + resolution: {integrity: sha512-gfS7OHC2W1xG0EWl5yQMPQl+Ra/hLsgus9X0VmwHvsovFSR9tun7Clz3XcIyAqEU7VpEtCkGrxHkMgw54SJxlg==} + peerDependencies: + '@automerge/automerge': '*' + '@automerge/automerge-repo': '*' + + '@inkandswitch/patchwork-plugins@0.0.11': + resolution: {integrity: sha512-ElwDEixpZN64gdoE7EU8QFz4WzvyJt6j4Zn0pTezsW6SzGlx+Rc6Ox+7POK7gqEzQEcl3Iqo8+QdYRucYllRFw==} + peerDependencies: + '@automerge/automerge': '*' + '@automerge/automerge-repo': '*' + '@inkandswitch/patchwork-filesystem': ^0.0.8 + + '@inkandswitch/patchwork-providers@0.5.2': + resolution: {integrity: sha512-jTR6aOPX4o5uj2OA45hcJ0A/K5fhQJPtEY+B2z9xaBWCgktZ/OLKWV2IQeQ2E98r++gu7BV/2f8tP+KwTLWJJA==} + peerDependencies: + '@automerge/automerge-repo': '*' + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@rollup/rollup-android-arm-eabi@4.63.4': + resolution: {integrity: sha512-I+BSHzTAhKN2n7ZwGZsegGcZjDpLqFOMAtJz/u6uFGe0pUFbq56dEHjqJV/ZUdRJtNXNxA+hREUatZBvMR3Oiw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.63.4': + resolution: {integrity: sha512-pu3BdjS2LtEzRu2elmGzS3fIeWSZy4BMDIaLNwjorO76+k2d0LMluijhsDx3KQyQBQ/lLUZCQA9/s6csvUfuhw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.63.4': + resolution: {integrity: sha512-xfSrj9MHnWK9GaSqT9U0ImHtH/N8WZlHLx4cZHiuLcqs640hvZ3hLPd5UR2AZS57FaE8HrRUSpltbZdWRxHiDA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.63.4': + resolution: {integrity: sha512-bqU99PLJb/dqb3S0GIMdeuyAEETSUgZBoqXYd3Sd+WCsV+MmPhnN6JrotWyir31+QgH7EvvE5/mwGJlEoci8Fw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.63.4': + resolution: {integrity: sha512-JinsFZ5G40oXQb+sUuiA5x689vhr6dDYK0H0NL+rwKdL6CqnmYN8PE4ZwfRSoIjrCxqTQG/SLfTtSvHeGxoVlw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.63.4': + resolution: {integrity: sha512-GAdA4UxpiNm27cLHr2GqXBpAD0x9FqwYBY7/YSP0Ss0/PNi4k8gbviqpIpYbVSRBaS2ZcegXEzgTQMbRNCwxCw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.63.4': + resolution: {integrity: sha512-qDd6NoA1znaLjp4jR5U/KWCdLAKDJNB8W9ChbbDaKbo0xA+Atln5HK6LFCZ4oJQpemtRZA288DCirFRjrspptw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.63.4': + resolution: {integrity: sha512-WtB5Tz5KTNINb8ZA+8sQ7bmjuS1JrRT7YverYIhUGdWWDlpzVWmIwuZE+jidkEXUn1l0zrEkaIMa8dHF3NGcsA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.63.4': + resolution: {integrity: sha512-VcQ3L1tjnkKzWjryAVaFhHEWcqOfICX9uxVVoDzm2t0DpgKRHd2zOpVrJc0xsWeBZcBFyYROCIBdyR/fS174pg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.63.4': + resolution: {integrity: sha512-6+ZQX6P5s0cMDN2Ypb8Lbm2+/sZYmZjdaYny992ujUU9UKi/4CWoJWsl1pNvjWJHNHGK51m+jKGLlh1ylb2ifQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.63.4': + resolution: {integrity: sha512-D72ZnvkFkBXOfzMMQLcwfPLyGkKb7HZ9/mf97B7v6/P5Lbv4oFOtSY/uHbS8lH6uKUOxoKiuokdb50XZSzzbJw==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.63.4': + resolution: {integrity: sha512-piU6BxeqA3O9KSu3kRCIQQtNqFFaTu21SEV4FwaRZowpnj3bLaWPZHw+xFqCs0XlJ+aOH3PTRWGoglH+mKA/OA==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.63.4': + resolution: {integrity: sha512-/5PGpHwqt2EEEOUs1XwzubE/ucr0dWDQ+to3zqi4Ds7EWpwtQ79wXc4JBoxqj/OwpawTsKWzJxHfSuBOq3DrWA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.63.4': + resolution: {integrity: sha512-cX3beZDLWt7G2oJF+nhChiT+qtaihs+S2xi7ziGmVB+2pwPng6D0Ed0HmElQOgv2UsUmSJJLGwpBao/3TDx3VA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.63.4': + resolution: {integrity: sha512-1uz2mGWHyptR7DgHHrlbdRAjXK7v7elGZ9lMja910/RP+ZYbX6xAmCiU9UZSX4hqmgtHMv6lr5l3kq1HIOpcag==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.63.4': + resolution: {integrity: sha512-nLS8topojxyz7SRpKR2IODRpQ0XPZ+xaOXvT3+hqK/Uy8Lo5HFgkkIBiIrCu5tL5YqzTvgovGw55PwpahTAGig==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.63.4': + resolution: {integrity: sha512-gs7DRKotr3l3q+jGPQBjH0ng1FjlEDm5ueQrkw5JtQvtLyEIcLASqAEaor56BhkKRzk+IcQzrcanBdb/bBQn8g==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.63.4': + resolution: {integrity: sha512-791ET7W17NnScOZM7h4dX5hYspxE28htPFsb1awY/NRR8+PRNkS53e475rDdxXXDrP+kwnCcNWg9CX5ztn/Aqw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.63.4': + resolution: {integrity: sha512-iwZQRcmj7g88g3tzefIrQY7qvmuA/cfYwhrDtTBhsmukO4U2huVO5W+86XacUMRvdSFVAc6kZUZy21JaRwiB9w==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.63.4': + resolution: {integrity: sha512-dVHFp9gRWrdTpnqQuGfCwd7hOQDatK1VCP2iWhLY/cGrOQs/ucFzJ6A5SRqbXX12ZDI8EUuejSM5kwg+ja7Png==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.63.4': + resolution: {integrity: sha512-t3NlauOW6gxZVVFcBEnO62Cb4wbyDFL416gTg1uFI/2tgqYQlf69FbSE115Ajre9I+c26Lk4mcmdFUsS/DGifQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.63.4': + resolution: {integrity: sha512-xWuIaSye5FWZF8+UYtVEcHtRJDN5kN9Kfgxx3Kq8XIov9KSKbc1fiqQCm90SKrgQbUXZelbnUhnlUJmfSE7P9A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.63.4': + resolution: {integrity: sha512-9ALJJUOg/ZflMJepVo2PlgsGxSaxN7SQ4Z8GoZfVlarWr6r3rkHUNsd/zAio7p4YMtChSMXPionxej4Hkf6CXQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.63.4': + resolution: {integrity: sha512-blj9z5qx/Pv4WU0W1NMFDB97e0JH5ed+aZGywW8WCvp/NhWX/4PFAq5uu6Q0AebNn+Vo6KzUYDT++JzTT5ojlQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.63.4': + resolution: {integrity: sha512-Erx822VRBwLa124shbj+wNXe//BOgMEctDV0m1aqTQdNO1S69DgNUCFKC1RCeZfixs1J31l6igk1ziyXErbigQ==} + cpu: [x64] + os: [win32] + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@20.19.43': + resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} + + '@vitest/expect@3.2.7': + resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} + + '@vitest/mocker@3.2.7': + resolution: {integrity: sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.7': + resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==} + + '@vitest/runner@3.2.7': + resolution: {integrity: sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==} + + '@vitest/snapshot@3.2.7': + resolution: {integrity: sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==} + + '@vitest/spy@3.2.7': + resolution: {integrity: sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==} + + '@vitest/utils@3.2.7': + resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + base-x@5.0.1: + resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + + bs58@6.0.0: + resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} + + bs58check@4.0.0: + resolution: {integrity: sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + cbor-extract@2.2.2: + resolution: {integrity: sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng==} + hasBin: true + + cbor-x@1.6.6: + resolution: {integrity: sha512-8QiD9PGOxyQHo7s2pzwTBH6lTjqekxPdl9Aq6fXvZgCuCJHOht1puDEA/fTr6mciB76c+M+Gi0qT2i1a4pm4Wg==} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + happy-dom@15.11.7: + resolution: {integrity: sha512-KyrFvnl+J9US63TEzwoiJOQzZBJY7KgBushJA8X61DMbNsH+2ONkDuLDnCnwUiPTF42tLoEmrPyoqbenVA5zrg==} + engines: {node: '>=18.0.0'} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-gyp-build-optional-packages@5.1.1: + resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} + hasBin: true + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + + resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + + rollup@4.63.4: + resolution: {integrity: sha512-4U0liVayNIoLp3GFl1FcI8561WepLnZ1rqfraGh7S9B3Ur5F9S283y8Futii7RUU2C/97tOBmBy7nYvhoiOpbQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.6: + resolution: {integrity: sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==} + engines: {node: '>=14.0.0'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + uuid@14.0.2: + resolution: {integrity: sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==} + hasBin: true + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.7: + resolution: {integrity: sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.7 + '@vitest/ui': 3.2.7 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + xstate@5.33.2: + resolution: {integrity: sha512-8tC7yXgeCvpT8gKEeEje6ikJJG1wpnoLiXe+HfECW8m10ubMd3QxKOwWA8KxPJVBcLwfdRoMKYxIBGYKmo37/A==} + +snapshots: + + '@automerge/automerge-repo@2.6.0-alpha.3': + dependencies: + '@automerge/automerge': 3.5.0 + bs58check: 4.0.0 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + fast-sha256: 1.3.0 + uuid: 14.0.2 + xstate: 5.33.2 + transitivePeerDependencies: + - supports-color + + '@automerge/automerge@3.5.0': {} + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + optional: true + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.6.0-alpha.3)(@automerge/automerge@3.5.0)': + dependencies: + '@automerge/automerge': 3.5.0 + '@automerge/automerge-repo': 2.6.0-alpha.3 + '@types/debug': 4.1.13 + '@types/node': 20.19.43 + debug: 4.4.3 + resolve.exports: 2.0.3 + transitivePeerDependencies: + - supports-color + + '@inkandswitch/patchwork-plugins@0.0.11(@automerge/automerge-repo@2.6.0-alpha.3)(@automerge/automerge@3.5.0)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.6.0-alpha.3)(@automerge/automerge@3.5.0))': + dependencies: + '@automerge/automerge': 3.5.0 + '@automerge/automerge-repo': 2.6.0-alpha.3 + '@inkandswitch/patchwork-filesystem': 0.0.8(@automerge/automerge-repo@2.6.0-alpha.3)(@automerge/automerge@3.5.0) + '@types/debug': 4.1.13 + '@types/node': 20.19.43 + debug: 4.4.3 + eventemitter3: 5.0.4 + resolve.exports: 2.0.3 + transitivePeerDependencies: + - supports-color + + '@inkandswitch/patchwork-providers@0.5.2(@automerge/automerge-repo@2.6.0-alpha.3)': + dependencies: + '@automerge/automerge-repo': 2.6.0-alpha.3 + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@noble/hashes@1.8.0': {} + + '@rollup/rollup-android-arm-eabi@4.63.4': + optional: true + + '@rollup/rollup-android-arm64@4.63.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.63.4': + optional: true + + '@rollup/rollup-darwin-x64@4.63.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.63.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.63.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.63.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.63.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.63.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.63.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.63.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.63.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.63.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.63.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.63.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.63.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.63.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.63.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.63.4': + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/ms@2.1.0': {} + + '@types/node@20.19.43': + dependencies: + undici-types: 6.21.0 + + '@vitest/expect@3.2.7': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@20.19.43))': + dependencies: + '@vitest/spy': 3.2.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.6(@types/node@20.19.43) + + '@vitest/pretty-format@3.2.7': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.7': + dependencies: + '@vitest/utils': 3.2.7 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.7': + dependencies: + tinyspy: 4.0.6 + + '@vitest/utils@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + assertion-error@2.0.1: {} + + base-x@5.0.1: {} + + bs58@6.0.0: + dependencies: + base-x: 5.0.1 + + bs58check@4.0.0: + dependencies: + '@noble/hashes': 1.8.0 + bs58: 6.0.0 + + cac@6.7.14: {} + + cbor-extract@2.2.2: + dependencies: + node-gyp-build-optional-packages: 5.1.1 + optionalDependencies: + '@cbor-extract/cbor-extract-darwin-arm64': 2.2.2 + '@cbor-extract/cbor-extract-darwin-x64': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm64': 2.2.2 + '@cbor-extract/cbor-extract-linux-x64': 2.2.2 + '@cbor-extract/cbor-extract-win32-x64': 2.2.2 + optional: true + + cbor-x@1.6.6: + optionalDependencies: + cbor-extract: 2.2.2 + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + detect-libc@2.1.2: + optional: true + + entities@4.5.0: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + eventemitter3@5.0.4: {} + + expect-type@1.4.0: {} + + fast-sha256@1.3.0: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fsevents@2.3.3: + optional: true + + happy-dom@15.11.7: + dependencies: + entities: 4.5.0 + webidl-conversions: 7.0.0 + whatwg-mimetype: 3.0.0 + + js-tokens@9.0.1: {} + + loupe@3.2.1: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + ms@2.1.3: {} + + nanoid@3.3.19: {} + + node-gyp-build-optional-packages@5.1.1: + dependencies: + detect-libc: 2.1.2 + optional: true + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + postcss@8.5.28: + dependencies: + nanoid: 3.3.19 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + resolve.exports@2.0.3: {} + + rollup@4.63.4: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.63.4 + '@rollup/rollup-android-arm64': 4.63.4 + '@rollup/rollup-darwin-arm64': 4.63.4 + '@rollup/rollup-darwin-x64': 4.63.4 + '@rollup/rollup-freebsd-arm64': 4.63.4 + '@rollup/rollup-freebsd-x64': 4.63.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.63.4 + '@rollup/rollup-linux-arm-musleabihf': 4.63.4 + '@rollup/rollup-linux-arm64-gnu': 4.63.4 + '@rollup/rollup-linux-arm64-musl': 4.63.4 + '@rollup/rollup-linux-loong64-gnu': 4.63.4 + '@rollup/rollup-linux-loong64-musl': 4.63.4 + '@rollup/rollup-linux-ppc64-gnu': 4.63.4 + '@rollup/rollup-linux-ppc64-musl': 4.63.4 + '@rollup/rollup-linux-riscv64-gnu': 4.63.4 + '@rollup/rollup-linux-riscv64-musl': 4.63.4 + '@rollup/rollup-linux-s390x-gnu': 4.63.4 + '@rollup/rollup-linux-x64-gnu': 4.63.4 + '@rollup/rollup-linux-x64-musl': 4.63.4 + '@rollup/rollup-openbsd-x64': 4.63.4 + '@rollup/rollup-openharmony-arm64': 4.63.4 + '@rollup/rollup-win32-arm64-msvc': 4.63.4 + '@rollup/rollup-win32-ia32-msvc': 4.63.4 + '@rollup/rollup-win32-x64-gnu': 4.63.4 + '@rollup/rollup-win32-x64-msvc': 4.63.4 + fsevents: 2.3.3 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.6: {} + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + uuid@14.0.2: {} + + vite-node@3.2.4(@types/node@20.19.43): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.6(@types/node@20.19.43) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.3.6(@types/node@20.19.43): + dependencies: + esbuild: 0.28.2 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + postcss: 8.5.28 + rollup: 4.63.4 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 20.19.43 + fsevents: 2.3.3 + + vitest@3.2.7(@types/debug@4.1.13)(@types/node@20.19.43)(happy-dom@15.11.7): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.7 + '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@20.19.43)) + '@vitest/pretty-format': 3.2.7 + '@vitest/runner': 3.2.7 + '@vitest/snapshot': 3.2.7 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.4.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.6(@types/node@20.19.43) + vite-node: 3.2.4(@types/node@20.19.43) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.13 + '@types/node': 20.19.43 + happy-dom: 15.11.7 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + webidl-conversions@7.0.0: {} + + whatwg-mimetype@3.0.0: {} + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + xstate@5.33.2: {} diff --git a/libraries/patchwork-worker/pnpm-workspace.yaml b/libraries/patchwork-worker/pnpm-workspace.yaml new file mode 100644 index 00000000..b6a60e31 --- /dev/null +++ b/libraries/patchwork-worker/pnpm-workspace.yaml @@ -0,0 +1,11 @@ +# pnpm 11 reads its settings from this file. There is no root workspace, so +# every package carries the settings it needs rather than inheriting them. +minimumReleaseAge: 0 +verifyDepsBeforeRun: false +allowBuilds: + "@swc/core": true + cbor-extract: true + core-js: true + esbuild: true +configDependencies: + pnpm-plugin-patchwork: "0.4.1+sha512-IzQJkoeQaSaYSa1bRko3KUJ+6nBmYg8Qxuyy0llnhPfAGSDP/+gM/Z7R0HGLtTpCE3K57CXS9DnAItqb/pNIZQ==" diff --git a/libraries/patchwork-worker/serve.js b/libraries/patchwork-worker/serve.js new file mode 100644 index 00000000..2f648cc7 --- /dev/null +++ b/libraries/patchwork-worker/serve.js @@ -0,0 +1,284 @@ +/** + * The SERVING half of a worker connection — the mirror of `openSession`. + * + * `openSession` (session.js) is the consumer side: it owns a `{readable, + * writable}` pair, mints request ids, multiplexes many requests over one + * connection, and routes event frames back to the right caller. `serveWorkerSpec` + * is the same machinery on the OTHER end. A service supplies a small `WorkerSpec` + * — how to construct its worker, an optional per-connection warm-up, and a + * `handle(frame, io)` that turns one consumer request into worker traffic — and + * this file owns everything kind-agnostic around it: + * + * - the `{readable, writable}` stream pair and its controller lifecycle + * - ONE dedicated Worker per connection, terminated on teardown (no reuse, no + * sharing — a worker belongs to the connection that opened it and dies with it) + * - id minting and demux for requests multiplexed within the connection + * - the reserved `op:"abort"` (session.js sends it, so it is part of the + * transport protocol, not any service's vocabulary) + * - teardown on cancel/close/abort of either stream, on worker death, and on a + * worker that cannot be constructed + * - a bounded `open` hook + * + * Frames the worker posts with NO `id` (status, progress) are emitted straight + * onto the connection's readable; the consumer side fans them out to its + * in-flight requests. With one worker per connection there is nothing else to + * route them to. + * + * The spec knows nothing about streams, ids, or workers-as-transport; the + * transport knows nothing about the service's op vocabulary, config, or secrets. + */ + +/** + * How long `spec.open` may take before the transport gives up and serves frames + * anyway. Bounds an upstream primitive that can hang: a settings-doc warm resolves + * through patchwork-providers' `request()`, which never settles if no provider + * answers. Falling through is the spec's responsibility to make safe; wedging + * every frame forever is strictly worse. + */ +const OPEN_TIMEOUT_MS = 5000 + +/** + * @typedef {(msg: any, transfer?: Transferable[]) => void} Post + * @typedef {(frame: any) => void} Emit + * + * @typedef {Object} IO what a spec's `handle` is given + * @property {Post} post send a message to the worker (transfer supported) + * @property {Emit} emit enqueue a frame onto THIS consumer's readable + * @property {(fn: (msg:any)=>boolean|void) => void} on + * register a handler for worker messages tagged with `workerId`; return a truthy + * value from `fn` when the request is complete and the transport should clean up. + * A request whose handle never calls `on` is fire-and-forget: nothing is + * tracked for it and `op:"abort"` cannot target it. + * @property {string} workerId the transport-minted id to tag worker payloads with + * @property {any} state whatever `spec.open` resolved (or null) + * @property {{element?: HTMLElement}} ctx host-realm context from the provider + * + * @typedef {Object} WorkerSpec + * @property {() => Worker | Promise} createWorker + * @property {(ctx: {element?: HTMLElement}) => any} [open] + * @property {(frame: any, io: IO) => any} handle + * returns an opaque abort token (or nothing) stored per tracked request + * @property {(token: any, post: Post) => void} [abort] + */ + +let idSeq = 0 +function nextWorkerId() { + return "wk-" + ++idSeq + "-" + (performance.now() | 0) +} + +/** + * Serve one worker connection from a spec. Returns the `{readable, writable}` the + * provider transfers to the consumer. One Worker is created for this connection + * and terminated when either stream ends or the worker dies. + * + * @param {WorkerSpec} spec + * @param {{element?: HTMLElement}} [ctx] + * @returns {{readable: ReadableStream, writable: WritableStream}} + */ +export function serveWorkerSpec(spec, ctx = {}) { + /** @type {Worker | null} */ + let worker = null + /** @type {Promise | null} */ + let workerReady = null + // worker message id -> handler. One connection, so one flat map is enough. + /** @type {Mapboolean|void>} */ + const handlers = new Map() + // caller id -> the abort token the spec returned, so `op:"abort"` can cancel + // it. A request is entered here SYNCHRONOUSLY at `PENDING` the moment its + // frame arrives, before any await — so an `op:"abort"` that races in while the + // spec is still resolving (`await openState` / `await spec.handle`) finds the + // request and is honoured once the token lands, rather than being dropped. + /** @type {Map} */ + const tokens = new Map() + // caller ids aborted while still `PENDING` — the abort is deferred to when the + // handler stores its token. + /** @type {Set} */ + const aborted = new Set() + const PENDING = Symbol("pending") + + /** @type {ReadableStreamDefaultController | null} */ + let controller = null + let closed = false + + const emit = (/** @type {any} */ frame) => { + try { + controller?.enqueue(frame) + } catch {} + } + + // The connection's one warm-up, bounded so a never-settling `open` can't wedge + // every frame. Started eagerly; frames await it before dispatch. + const openState = spec.open + ? Promise.race([ + Promise.resolve(spec.open(ctx)), + new Promise((resolve) => setTimeout(resolve, OPEN_TIMEOUT_MS, null)), + ]).catch(() => null) + : Promise.resolve(null) + + /** @type {Post} */ + const post = (msg, transfer) => { + if (closed) return + void getWorker() + .then((w) => w.postMessage(msg, transfer || [])) + .catch(() => {}) // a failed construction has already torn the connection down + } + + /** + * Lazily construct the worker (once) and wire its message pump. A worker that + * cannot be constructed, or that later dies, ends the connection: the + * consumer's in-flight requests reject when its readable closes, instead of + * waiting on a worker that will never answer. + */ + function getWorker() { + if (workerReady) return workerReady + workerReady = Promise.resolve() + .then(() => spec.createWorker()) + .then((w) => { + worker = w + w.onmessage = (/** @type {MessageEvent} */ ev) => dispatch(ev.data) + w.onerror = teardown + w.onmessageerror = teardown + return w + }) + workerReady.catch(teardown) + return workerReady + } + + /** + * Route a worker message. A message with an `id` goes to that request's + * handler (which reports terminal by returning truthy). A message with no `id` + * is a connection-wide broadcast and goes straight onto the readable. + */ + function dispatch(/** @type {any} */ msg) { + if (!msg || closed) return + if (msg.id != null) { + const h = handlers.get(msg.id) + if (h && h(msg)) handlers.delete(msg.id) + return + } + emit(msg) + } + + async function handleFrame(/** @type {any} */ frame) { + if (!frame || typeof frame !== "object") return + const id = frame.id + const op = frame.op + + // Reserved transport op. Cancel one in-flight request: let the spec send + // whatever the worker needs, then forget it. If the request is still + // `PENDING` (its handler hasn't returned a token yet), defer: record the id + // and let the handler abort as soon as it stores the token. + if (op === "abort") { + if (!tokens.has(id)) return // unknown / already-finished request + const token = tokens.get(id) + if (token === PENDING) { + aborted.add(id) + return + } + try { + spec.abort?.(token, post) + } catch {} + tokens.delete(id) + return + } + + // Claim the id SYNCHRONOUSLY, before the first await, so an abort racing in + // during `await openState` / `await spec.handle` isn't dropped. + tokens.set(id, PENDING) + + const state = await openState + if (closed) return + const workerId = nextWorkerId() + let tracked = false + + /** @type {IO} */ + const io = { + post, + // Frames the spec emits carry the WORKER id; re-tag with the caller id. + emit: (f) => emit({...f, id}), + on: (fn) => { + tracked = true + handlers.set(workerId, (msg) => { + const done = fn(msg) + if (done) { + tokens.delete(id) + aborted.delete(id) + } + return done + }) + }, + workerId, + state, + ctx, + } + + try { + const token = await spec.handle(frame, io) + // An abort arrived while this request was still PENDING — honour it now + // that the token exists, and don't store it (the request is cancelled). + if (aborted.has(id)) { + aborted.delete(id) + tokens.delete(id) + handlers.delete(workerId) + try { + spec.abort?.(token, post) + } catch {} + return + } + // Track the request only if the spec is listening for a reply; a + // fire-and-forget handle has nothing to abort and must not accumulate. + if (tracked) tokens.set(id, token) + else tokens.delete(id) + } catch (e) { + const err = /** @type {any} */ (e) + emit({id, type: "error", message: err?.message || String(e)}) + handlers.delete(workerId) + tokens.delete(id) + aborted.delete(id) + } + } + + function teardown() { + if (closed) return + closed = true + handlers.clear() + tokens.clear() + aborted.clear() + // Close the readable so a consumer still reading it sees end-of-stream rather + // than hanging forever. Teardown can be driven from the WRITABLE side + // (close/abort) or worker death, where the readable was never cancelled. + // `close()` throws if the stream was already closed/cancelled, so guard it. + try { + controller?.close() + } catch {} + controller = null + // The worker belongs to this connection alone — kill it. + try { + worker?.terminate() + } catch {} + worker = null + } + + const readable = new ReadableStream({ + start(c) { + controller = c + }, + cancel() { + teardown() + }, + }) + + const writable = new WritableStream({ + write(frame) { + void handleFrame(frame) + }, + close() { + teardown() + }, + abort() { + teardown() + }, + }) + + return {readable, writable} +} diff --git a/libraries/patchwork-worker/serve.test.js b/libraries/patchwork-worker/serve.test.js new file mode 100644 index 00000000..c4c03474 --- /dev/null +++ b/libraries/patchwork-worker/serve.test.js @@ -0,0 +1,285 @@ +import {describe, it, expect, vi, afterEach} from "vitest" +import {serveWorkerSpec} from "./serve.js" + +/** + * A fake Worker: records posted messages, lets the test push replies back, and + * tracks termination. No real Worker (happy-dom has none) — we are testing the + * transport, so the worker is a stub. + */ +function fakeWorker() { + const w = { + posted: [], + terminated: false, + /** @type {((ev:{data:any})=>void)|null} */ + onmessage: null, + /** @type {((ev:any)=>void)|null} */ + onerror: null, + /** @type {((ev:any)=>void)|null} */ + onmessageerror: null, + postMessage(msg, transfer) { + w.posted.push({msg, transfer}) + }, + terminate() { + w.terminated = true + }, + /** push a message from the "worker" back to the transport */ + reply(data) { + w.onmessage?.({data}) + }, + /** simulate the worker crashing */ + crash() { + w.onerror?.({message: "boom"}) + }, + } + return w +} + +/** Drive a served connection: write frames to writable, read frames off readable. */ +function drive(streams) { + const writer = streams.writable.getWriter() + const reader = streams.readable.getReader() + const frames = [] + let ended = false + ;(async () => { + try { + for (;;) { + const {value, done} = await reader.read() + if (done) break + frames.push(value) + } + } catch {} + ended = true + })() + return { + write: (f) => writer.write(f), + close: () => writer.close(), + cancelRead: () => reader.cancel(), + frames, + ended: () => ended, + settle: async () => { + for (let i = 0; i < 20; i++) await Promise.resolve() + await new Promise((r) => setTimeout(r, 0)) + }, + } +} + +/** A spec that posts one message per request and waits for its reply. */ +function echoSpec(w) { + return { + createWorker: () => w, + handle(frame, io) { + io.on((msg) => { + io.emit({type: "result", text: msg.text}) + return true + }) + io.post({type: "generate", id: io.workerId, text: frame.text}) + return {sessionKey: frame.id} + }, + } +} + +afterEach(() => vi.useRealTimers()) + +describe("serveWorkerSpec", () => { + it("routes a request to the worker and its reply back, re-tagged with the caller id", async () => { + const w = fakeWorker() + const conn = drive(serveWorkerSpec(echoSpec(w), {})) + await conn.write({id: "req-1", op: "generate", text: "hi"}) + await conn.settle() + expect(w.posted).toHaveLength(1) + const {msg} = w.posted[0] + expect(msg.type).toBe("generate") + expect(msg.id).not.toBe("req-1") // worker-side id is transport-minted + w.reply({id: msg.id, type: "result", text: "hi"}) + await conn.settle() + expect(conn.frames).toEqual([{id: "req-1", type: "result", text: "hi"}]) + }) + + it("gives each connection its own worker, and passes ctx to the spec", async () => { + const workers = [] + let seenCtx + const spec = { + createWorker: () => { + const w = fakeWorker() + workers.push(w) + return w + }, + handle: (frame, io) => { + seenCtx = io.ctx + io.post({type: "x", id: io.workerId}) + }, + } + const el = document.createElement("div") + const a = drive(serveWorkerSpec(spec, {element: el})) + const b = drive(serveWorkerSpec(spec, {})) + await a.write({id: "1", op: "go"}) + await b.write({id: "2", op: "go"}) + await a.settle() + expect(workers).toHaveLength(2) + expect(workers[0]).not.toBe(workers[1]) + expect(seenCtx).toEqual({}) + }) + + it("tears down from either stream end: worker terminated, readable closed", async () => { + // Writable closed: a reader blocked on read() is released with done=true. + const w1 = fakeWorker() + const a = drive(serveWorkerSpec(echoSpec(w1), {})) + await a.write({id: "a", op: "go"}) + await a.settle() + expect(w1.terminated).toBe(false) + await a.close() + await a.settle() + expect(w1.terminated).toBe(true) + expect(a.ended()).toBe(true) + + // Readable cancelled. + const w2 = fakeWorker() + const b = drive(serveWorkerSpec(echoSpec(w2), {})) + await b.write({id: "b", op: "go"}) + await b.settle() + await b.cancelRead() + await b.settle() + expect(w2.terminated).toBe(true) + }) + + it("tears down when the worker dies, so a waiting consumer sees end-of-stream", async () => { + const w = fakeWorker() + const conn = drive(serveWorkerSpec(echoSpec(w), {})) + await conn.write({id: "a", op: "go"}) + await conn.settle() + w.crash() + await conn.settle() + expect(w.terminated).toBe(true) + expect(conn.ended()).toBe(true) + }) + + it("tears down when the worker cannot be constructed", async () => { + const spec = { + createWorker: () => Promise.reject(new Error("no worker for you")), + handle: (frame, io) => io.post({id: io.workerId}), + } + const conn = drive(serveWorkerSpec(spec, {})) + await conn.write({id: "a", op: "go"}) + await conn.settle() + expect(conn.ended()).toBe(true) + }) + + it("routes op:abort to spec.abort with the stored token, once", async () => { + const w = fakeWorker() + const aborted = [] + const spec = { + ...echoSpec(w), + abort(token, post) { + aborted.push(token) + post({type: "abort", sessionKey: token.sessionKey}) + }, + } + const conn = drive(serveWorkerSpec(spec, {})) + await conn.write({id: "a", op: "generate"}) + await conn.settle() + await conn.write({id: "a", op: "abort"}) + await conn.write({id: "a", op: "abort"}) // duplicate: already forgotten + await conn.write({id: "zzz", op: "abort"}) // unknown: ignored + await conn.settle() + expect(aborted).toEqual([{sessionKey: "a"}]) + expect(w.posted.some((p) => p.msg.type === "abort" && p.msg.sessionKey === "a")).toBe(true) + }) + + it("honours an abort that races in while the request is still resolving", async () => { + // The abort arrives while `handle` is suspended on a slow `open`, before the + // abort token exists. It must be deferred and fire once the token lands. + const w = fakeWorker() + const aborted = [] + let releaseOpen + const spec = { + ...echoSpec(w), + open: () => new Promise((r) => (releaseOpen = r)), + abort: (token) => void aborted.push(token), + } + const conn = drive(serveWorkerSpec(spec, {})) + await conn.write({id: "a", op: "generate"}) // suspends on open + await conn.write({id: "a", op: "abort"}) // before the token exists + await conn.settle() + expect(aborted).toEqual([]) + + releaseOpen(null) + await conn.settle() + expect(aborted).toEqual([{sessionKey: "a"}]) + await conn.write({id: "a", op: "abort"}) + await conn.settle() + expect(aborted).toEqual([{sessionKey: "a"}]) + }) + + it("does not track a fire-and-forget request (no io.on), so op:abort ignores it", async () => { + const w = fakeWorker() + const aborted = [] + const spec = { + createWorker: () => w, + handle(frame, io) { + io.post({type: "preload", id: io.workerId}) + return {sessionKey: frame.id} // a token, but nothing is listening + }, + abort: (token) => void aborted.push(token), + } + const conn = drive(serveWorkerSpec(spec, {})) + await conn.write({id: "a", op: "preload"}) + await conn.settle() + await conn.write({id: "a", op: "abort"}) + await conn.settle() + expect(aborted).toEqual([]) + }) + + it("creates no worker for a request that was still pending at teardown", async () => { + const createWorker = vi.fn(() => fakeWorker()) + let releaseOpen + const spec = { + createWorker, + open: () => new Promise((r) => (releaseOpen = r)), + handle: (frame, io) => io.post({id: io.workerId}), + } + const conn = drive(serveWorkerSpec(spec, {})) + await conn.write({id: "a", op: "go"}) // suspends on open + await conn.close() // teardown while pending + releaseOpen(null) + await conn.settle() + expect(createWorker).not.toHaveBeenCalled() + }) + + it("emits id-less worker messages straight onto the readable", async () => { + const w = fakeWorker() + const conn = drive(serveWorkerSpec(echoSpec(w), {})) + await conn.write({id: "a", op: "go"}) + await conn.settle() + w.reply({type: "status", message: "downloading model"}) // no id + await conn.settle() + expect(conn.frames).toContainEqual({type: "status", message: "downloading model"}) + }) + + it("emits {type:error} tagged with the caller id when handle throws", async () => { + const spec = { + createWorker: () => fakeWorker(), + handle() { + throw new Error("boom") + }, + } + const conn = drive(serveWorkerSpec(spec, {})) + await conn.write({id: "a", op: "go"}) + await conn.settle() + expect(conn.frames).toContainEqual({id: "a", type: "error", message: "boom"}) + }) + + it("falls through when open never settles rather than wedging frames", async () => { + vi.useFakeTimers() + const spec = { + createWorker: () => fakeWorker(), + open: () => new Promise(() => {}), // never resolves + handle: (frame, io) => void io.emit({type: "ran", state: io.state}), + } + const conn = drive(serveWorkerSpec(spec, {})) + await conn.write({id: "a", op: "go"}) + await vi.advanceTimersByTimeAsync(5001) + vi.useRealTimers() + await conn.settle() + expect(conn.frames).toContainEqual({id: "a", type: "ran", state: null}) + }) +}) diff --git a/libraries/patchwork-worker/session.js b/libraries/patchwork-worker/session.js new file mode 100644 index 00000000..9e4b1a17 --- /dev/null +++ b/libraries/patchwork-worker/session.js @@ -0,0 +1,280 @@ +/** + * openSession — request/response multiplexing over a worker connection. + * + * `connectWorker` gives you a raw `{readable, writable}` pair. This module owns + * the layer every consumer needs on top of it: open the connection lazily, keep + * one writer, pump the readable, tag each request with an id, route event frames + * back to the right in-flight caller, and settle on a terminal frame. It stays + * service-agnostic: frames are opaque, and the caller says which `type` values + * are terminal. + * + * const session = openSession("llm", {element}) + * const {promise, abort} = session.request( + * {op: "generate", messages}, + * { + * terminal: {result: (f) => f.text, error: (f) => { throw new Error(f.message) }}, + * onFrame: (f) => { if (f.type === "token") ui.append(f.delta) }, + * signal, + * } + * ) + * + * Frame routing: a frame with an `id` goes to that request's `onFrame` (or its + * terminal handler). A frame with NO id is a connection-wide broadcast from the + * worker — a status or progress message not tied to one request — and is + * delivered to every in-flight request's `onFrame`, so a caller sees the + * worker's status the same way it would from a same-realm worker. + * + * Connection lifetime: opened on the first request, shared by every request + * after it, and DROPPED whenever it fails or ends — so the next request + * reconnects instead of replaying a dead or rejected connection forever. + * `close()` drops it on purpose (and terminates the host worker behind it). + * + * This module does not import ./connect.js. `connectWorker` is injected by + * `createOpenSession`, so the dependency runs one way (connect.js -> session.js). + */ + +/** + * @typedef {Object} RequestOpts + * @property {Recordany>} terminal frame.type -> settle. The + * return value resolves the request; throw to reject it. Any type listed here + * ends the request. + * @property {(frame:any)=>void} [onFrame] every non-terminal frame for this id, + * plus every id-less broadcast frame received while the request is in flight + * @property {AbortSignal} [signal] aborting sends {op:"abort", id} and rejects + * @property {HTMLElement} [element] discovery element, if not set on the session + * + * @typedef {Object} SessionOpts + * @property {HTMLElement} [element] default discovery element for every request + * @property {string} [idPrefix] request id prefix (defaults to the kind) + * @property {(...a:any[])=>void} [onLog] + * + * @typedef {{readable: ReadableStream, writable: WritableStream, disconnect: () => void}} Connection + * @typedef {Connection & {writer: WritableStreamDefaultWriter, reader: ReadableStreamDefaultReader}} OpenConnection + * + * @typedef {Object} Session + * @property {(frame: any, opts: RequestOpts) => {promise: Promise, abort: () => void}} request + * @property {() => void} close drop the connection (terminating the worker behind + * it) and reject every in-flight request; the next request reconnects + */ + +/** + * Build the `openSession` export, bound to a `connectWorker` implementation. + * Called once from connect.js; consumers use the resulting `openSession`. + * + * @param {(kind: string, opts: {element: HTMLElement}) => Promise} connectWorker + */ +export function createOpenSession(connectWorker) { + /** + * Open a lazily-connected, multiplexed session for a worker `kind`. + * + * @param {string} kind + * @param {SessionOpts} [sessionOpts] + * @returns {Session} + */ + return function openSession(kind, sessionOpts = {}) { + const idPrefix = sessionOpts.idPrefix || kind + const log = sessionOpts.onLog || (() => {}) + + /** @type {Promise|null} */ + let connectionPromise = null + /** id -> handlers for every request still in flight + * @type {Mapvoid, onClosed: (cause:any)=>void}>} */ + const handlers = new Map() + let idSeq = 0 + + const nextId = () => idPrefix + "-" + ++idSeq + "-" + (performance.now() | 0) + + /** + * Drop the cached connection, and FAIL everything still in flight on it. + * + * Uncaching matters because a single failure (requesting before the provider + * has mounted, say) would otherwise be cached and every later request would + * fail instantly with the stale error, and a normally-closed connection would + * never reopen. + * + * Failing the in-flight requests matters more. They are waiting on frames + * that can no longer arrive. Leaving them in `handlers` would orphan each + * promise forever — no rejection, no timeout — and a consumer awaiting with + * no deadline would wedge with no error to show. + * @param {any} cause + */ + function reset(cause) { + connectionPromise = null + const inFlight = [...handlers.values()] + handlers.clear() + for (const h of inFlight) h.onClosed(cause) + } + + /** @param {HTMLElement | undefined} element */ + function ensureConnection(element) { + if (connectionPromise) return connectionPromise + const el = element ?? sessionOpts.element + connectionPromise = (async () => { + const conn = await connectWorker(kind, {element: /** @type {HTMLElement} */ (el)}) + // Keep the reader and writer ON the connection, not in closure state: + // `send` awaits `ensureConnection` and the connection can end during + // that await, so shared variables may be null — or belong to a newer + // connection — by the time they are used. Holding both locks also + // means `close()` must release through them (a locked stream rejects + // cancel/abort from anyone else). + const open = /** @type {OpenConnection} */ ( + Object.assign(conn, { + writer: conn.writable.getWriter(), + reader: conn.readable.getReader(), + }) + ) + void pump(open.reader) + return open + })() + // Don't let an unawaited rejection surface as unhandled; just uncache it. + connectionPromise.catch((e) => reset(e)) + return connectionPromise + } + + /** @param {ReadableStreamDefaultReader} reader */ + async function pump(reader) { + /** @type {any} */ + let cause = null + try { + while (true) { + const {value, done} = await reader.read() + if (done) break + if (!value) continue + if (value.id != null) { + handlers.get(value.id)?.onFrame(value) + } else { + // Connection-wide broadcast: every in-flight request hears it. + for (const h of [...handlers.values()]) h.onFrame(value) + } + } + } catch (e) { + cause = e + log("connection readable errored", e) + } finally { + reset(cause) + } + } + + /** + * Write one frame, opening the connection if needed. + * @param {any} frame + * @param {HTMLElement | undefined} element + */ + async function send(frame, element) { + const conn = await ensureConnection(element) + await conn.writer.write(frame) + } + + /** + * Issue one request. Returns the settle promise plus an `abort()`. + * + * @param {any} frame the request frame (an `id` is added) + * @param {RequestOpts} opts + */ + function request(frame, opts) { + const id = nextId() + const terminal = opts.terminal || {} + + let settled = false + /** @type {(v:any)=>void} */ + let resolveFn = () => {} + /** @type {(e:any)=>void} */ + let rejectFn = () => {} + + const cleanup = () => { + handlers.delete(id) + opts.signal?.removeEventListener("abort", onAbort) + } + + function onAbort() { + if (settled) return + settled = true + // Only tell the service to stop if a connection exists or is opening: + // an already-aborted signal must not open a whole connection (up to the + // full discovery timeout) purely to abort a request that was never + // sent. If the request frame is still in flight the abort simply + // follows it; the serve half ignores aborts for ids it doesn't know. + if (connectionPromise) void send({op: "abort", id}, opts.element).catch(() => {}) + cleanup() + rejectFn(new DOMException("Aborted", "AbortError")) + } + + const promise = new Promise((resolve, reject) => { + resolveFn = resolve + rejectFn = reject + }) + + handlers.set(id, { + onFrame(f) { + if (settled) return + const settle = terminal[f.type] + if (settle) { + settled = true + cleanup() + try { + resolveFn(settle(f)) + } catch (e) { + rejectFn(e) + } + return + } + try { + opts.onFrame?.(f) + } catch (e) { + log("onFrame threw", e) + } + }, + // The connection ended before a terminal frame arrived. Nothing more + // can come, so fail rather than wait forever. + onClosed(cause) { + if (settled) return + settled = true + cleanup() + rejectFn( + cause instanceof Error + ? cause + : new Error(`worker connection for "${kind}" closed before the request completed`) + ) + }, + }) + + if (opts.signal) { + if (opts.signal.aborted) { + onAbort() + return {promise, abort: onAbort} + } + opts.signal.addEventListener("abort", onAbort) + } + + send({...frame, id}, opts.element).catch((e) => { + if (settled) return + settled = true + cleanup() + rejectFn(e) + }) + + return {promise, abort: onAbort} + } + + /** + * Close the connection on purpose. The serve half tears down its worker when + * the streams end; in-flight requests reject; the next request reconnects. + */ + function close() { + const pending = connectionPromise + if (!pending) return + reset(new Error(`worker connection for "${kind}" was closed`)) + pending + .then((conn) => { + // Release through the locks this session holds: cancelling the + // reader ends the pump; aborting the writer ends the serve half, + // which terminates the worker. + conn.reader.cancel().catch(() => {}) + conn.writer.abort().catch(() => {}) + }) + .catch(() => {}) + } + + return {request, close} + } +} diff --git a/libraries/patchwork-worker/tsconfig.json b/libraries/patchwork-worker/tsconfig.json new file mode 100644 index 00000000..e7de32b1 --- /dev/null +++ b/libraries/patchwork-worker/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ESNEXT", + "module": "ESNEXT", + "moduleResolution": "bundler", + "allowJs": true, + "checkJs": true, + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "types", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true + }, + "include": ["index.js", "connect.js", "session.js", "serve.js", "client.js"] +} diff --git a/libraries/patchwork-worker/types/client.d.ts b/libraries/patchwork-worker/types/client.d.ts new file mode 100644 index 00000000..2eb958cf --- /dev/null +++ b/libraries/patchwork-worker/types/client.d.ts @@ -0,0 +1,32 @@ +/** + * @typedef {import("./session.js").Session} WorkerSession + * @typedef {(session: WorkerSession) => any} WorkerClientFactory + * @typedef {{type: "patchwork:worker-client", id: string, name?: string, load: () => Promise}} WorkerClientPlugin + */ +/** + * Resolve the `patchwork:worker-client` plugin for `kind`, open a session for + * the same kind, and return `factory(session)` — the service's client API. + * + * Rejects if no plugin for `kind` is registered and loaded within `timeoutMs`, + * or if the plugin did not resolve to a function. Callers should not cache a + * rejected result: a later call may succeed once the service package registers. + * + * @param {string} kind + * @param {{ + * sessionOpts?: import("./session.js").SessionOpts, + * timeoutMs?: number, + * }} [opts] + * @returns {Promise} + */ +export function connectWorkerClient(kind: string, opts?: { + sessionOpts?: import("./session.js").SessionOpts; + timeoutMs?: number; +}): Promise; +export type WorkerSession = import("./session.js").Session; +export type WorkerClientFactory = (session: WorkerSession) => any; +export type WorkerClientPlugin = { + type: "patchwork:worker-client"; + id: string; + name?: string; + load: () => Promise; +}; diff --git a/libraries/patchwork-worker/types/connect.d.ts b/libraries/patchwork-worker/types/connect.d.ts new file mode 100644 index 00000000..c71eb90a --- /dev/null +++ b/libraries/patchwork-worker/types/connect.d.ts @@ -0,0 +1,111 @@ +/** @typedef {{ readable: ReadableStream, writable: WritableStream }} WorkerStreams */ +/** @typedef {WorkerStreams & { disconnect: () => void }} WorkerConnection */ +/** + * What a `patchwork:worker` plugin's `load()` must resolve to: a WorkerSpec that + * `serveWorkerSpec` (serve.js) drives. The transport owns the streams, ids, and + * per-connection worker; the spec owns only its op vocabulary. `open(ctx)` gets + * the provider's mount point, so a worker can resolve host-realm context (a + * settings doc, say) without the provider knowing about that service. + * + * (Shape mirrored from serve.js's `WorkerSpec`, kept as a local typedef so this + * sandbox-loaded file does not import the host-only serve module. Types erase, + * so this costs nothing at runtime.) + * @typedef {{ + * createWorker: () => Worker | Promise, + * open?: (ctx: {element?: HTMLElement}) => any, + * handle: (frame: any, io: any) => any, + * abort?: (token: any, post: (msg: any, transfer?: Transferable[]) => void) => void, + * }} WorkerSpec + */ +/** + * The descriptor a package puts in its `plugins` array to offer a worker. + * @typedef {{type: "patchwork:worker", id: string, name?: string, load: () => Promise}} WorkerPlugin + */ +/** + * Open a connection to a worker of `kind`. + * + * A `patchwork:subscribe` provider for `{type: CHANNEL_SELECTOR, kind}` above + * `element` answers, transferring streams back over the port. In the host realm + * that's a mounted worker provider; in a sandboxed realm it's whatever relays + * the subscription to the host and transfers the host's streams back across the + * boundary. The consumer gets the same `{readable, writable, disconnect}` either + * way and never learns which answered. + * + * There is deliberately NO fallback to a locally-constructed worker. Inside the + * sandbox that fallback was a hole: any in-boundary tool that imported a service + * package would register its worker as an import side-effect, and a connection + * that should have been refused would instead run the worker in the opaque + * origin — no shared model cache, no host config, and no signal that the + * isolation boundary had been bypassed. Serving is the provider's job; a + * consumer with no provider ancestor fails loudly instead. + * + * @param {string} kind + * @param {{ element: HTMLElement }} opts a node inside a mounted , + * to dispatch the discovery subscribe from + * @returns {Promise} + */ +export function connectWorker(kind: string, opts: { + element: HTMLElement; +}): Promise; +/** + * The selector type used to discover a worker-connection provider. A consumer + * dispatches a `patchwork:subscribe` for `{ type: CHANNEL_SELECTOR, kind }` + * carrying a MessagePort in `detail.port`. The answering side replies over that + * port, in the standard providers envelope (`{type:"change", value}`), with + * exactly one of: + * + * {readable, writable} — success; the pair is TRANSFERRED, not cloned + * null — refused; fail fast + * + * Silence is also a valid outcome (nothing is mounted to answer), which the + * consumer's bounded discovery timeout covers. Answering sides that KNOW they're + * refusing should respond `null` rather than staying silent. + */ +export const CHANNEL_SELECTOR: "patchwork:worker-channel"; +/** + * The plugin type a service package registers to offer a worker. Its `id` is + * the worker `kind`; `load()` resolves to a WorkerSpec (see ./serve.js). + */ +export const WORKER_PLUGIN_TYPE: "patchwork:worker"; +/** + * The paired plugin type a service package registers to offer a typed client + * for its worker. Same `id` as the worker; `load()` resolves to a factory + * `(session) => clientApi` (see ./client.js). + */ +export const WORKER_CLIENT_PLUGIN_TYPE: "patchwork:worker-client"; +export const openSession: (kind: string, sessionOpts?: import("./session.js").SessionOpts) => import("./session.js").Session; +export type WorkerStreams = { + readable: ReadableStream; + writable: WritableStream; +}; +export type WorkerConnection = WorkerStreams & { + disconnect: () => void; +}; +/** + * What a `patchwork:worker` plugin's `load()` must resolve to: a WorkerSpec that + * `serveWorkerSpec` (serve.js) drives. The transport owns the streams, ids, and + * per-connection worker; the spec owns only its op vocabulary. `open(ctx)` gets + * the provider's mount point, so a worker can resolve host-realm context (a + * settings doc, say) without the provider knowing about that service. + * + * (Shape mirrored from serve.js's `WorkerSpec`, kept as a local typedef so this + * sandbox-loaded file does not import the host-only serve module. Types erase, + * so this costs nothing at runtime.) + */ +export type WorkerSpec = { + createWorker: () => Worker | Promise; + open?: (ctx: { + element?: HTMLElement; + }) => any; + handle: (frame: any, io: any) => any; + abort?: (token: any, post: (msg: any, transfer?: Transferable[]) => void) => void; +}; +/** + * The descriptor a package puts in its `plugins` array to offer a worker. + */ +export type WorkerPlugin = { + type: "patchwork:worker"; + id: string; + name?: string; + load: () => Promise; +}; diff --git a/libraries/patchwork-worker/types/index.d.ts b/libraries/patchwork-worker/types/index.d.ts new file mode 100644 index 00000000..9c3c288c --- /dev/null +++ b/libraries/patchwork-worker/types/index.d.ts @@ -0,0 +1,30 @@ +export { connectWorkerClient } from "./client.js"; +/** + * Types a service package needs to type its `plugins` entries. + */ +export type WorkerSpec = import("./connect.js").WorkerSpec; +/** + * Types a service package needs to type its `plugins` entries. + */ +export type WorkerPlugin = import("./connect.js").WorkerPlugin; +/** + * Types a service package needs to type its `plugins` entries. + */ +export type WorkerStreams = import("./connect.js").WorkerStreams; +/** + * Types a service package needs to type its `plugins` entries. + */ +export type WorkerConnection = import("./connect.js").WorkerConnection; +/** + * Types a service package needs to type its `plugins` entries. + */ +export type WorkerClientPlugin = import("./client.js").WorkerClientPlugin; +/** + * Types a service package needs to type its `plugins` entries. + */ +export type WorkerClientFactory = import("./client.js").WorkerClientFactory; +/** + * Types a service package needs to type its `plugins` entries. + */ +export type Session = import("./session.js").Session; +export { connectWorker, openSession, CHANNEL_SELECTOR, WORKER_PLUGIN_TYPE, WORKER_CLIENT_PLUGIN_TYPE } from "./connect.js"; diff --git a/libraries/patchwork-worker/types/serve.d.ts b/libraries/patchwork-worker/types/serve.d.ts new file mode 100644 index 00000000..2903e907 --- /dev/null +++ b/libraries/patchwork-worker/types/serve.d.ts @@ -0,0 +1,62 @@ +/** + * Serve one worker connection from a spec. Returns the `{readable, writable}` the + * provider transfers to the consumer. One Worker is created for this connection + * and terminated when either stream ends or the worker dies. + * + * @param {WorkerSpec} spec + * @param {{element?: HTMLElement}} [ctx] + * @returns {{readable: ReadableStream, writable: WritableStream}} + */ +export function serveWorkerSpec(spec: WorkerSpec, ctx?: { + element?: HTMLElement; +}): { + readable: ReadableStream; + writable: WritableStream; +}; +export type Post = (msg: any, transfer?: Transferable[]) => void; +export type Emit = (frame: any) => void; +/** + * what a spec's `handle` is given + */ +export type IO = { + /** + * send a message to the worker (transfer supported) + */ + post: Post; + /** + * enqueue a frame onto THIS consumer's readable + */ + emit: Emit; + /** + * register a handler for worker messages tagged with `workerId`; return a truthy + * value from `fn` when the request is complete and the transport should clean up. + * A request whose handle never calls `on` is fire-and-forget: nothing is + * tracked for it and `op:"abort"` cannot target it. + */ + on: (fn: (msg: any) => boolean | void) => void; + /** + * the transport-minted id to tag worker payloads with + */ + workerId: string; + /** + * whatever `spec.open` resolved (or null) + */ + state: any; + /** + * host-realm context from the provider + */ + ctx: { + element?: HTMLElement; + }; +}; +export type WorkerSpec = { + createWorker: () => Worker | Promise; + open?: ((ctx: { + element?: HTMLElement; + }) => any) | undefined; + /** + * returns an opaque abort token (or nothing) stored per tracked request + */ + handle: (frame: any, io: IO) => any; + abort?: ((token: any, post: Post) => void) | undefined; +}; diff --git a/libraries/patchwork-worker/types/session.d.ts b/libraries/patchwork-worker/types/session.d.ts new file mode 100644 index 00000000..105e9e2e --- /dev/null +++ b/libraries/patchwork-worker/types/session.d.ts @@ -0,0 +1,118 @@ +/** + * openSession — request/response multiplexing over a worker connection. + * + * `connectWorker` gives you a raw `{readable, writable}` pair. This module owns + * the layer every consumer needs on top of it: open the connection lazily, keep + * one writer, pump the readable, tag each request with an id, route event frames + * back to the right in-flight caller, and settle on a terminal frame. It stays + * service-agnostic: frames are opaque, and the caller says which `type` values + * are terminal. + * + * const session = openSession("llm", {element}) + * const {promise, abort} = session.request( + * {op: "generate", messages}, + * { + * terminal: {result: (f) => f.text, error: (f) => { throw new Error(f.message) }}, + * onFrame: (f) => { if (f.type === "token") ui.append(f.delta) }, + * signal, + * } + * ) + * + * Frame routing: a frame with an `id` goes to that request's `onFrame` (or its + * terminal handler). A frame with NO id is a connection-wide broadcast from the + * worker — a status or progress message not tied to one request — and is + * delivered to every in-flight request's `onFrame`, so a caller sees the + * worker's status the same way it would from a same-realm worker. + * + * Connection lifetime: opened on the first request, shared by every request + * after it, and DROPPED whenever it fails or ends — so the next request + * reconnects instead of replaying a dead or rejected connection forever. + * `close()` drops it on purpose (and terminates the host worker behind it). + * + * This module does not import ./connect.js. `connectWorker` is injected by + * `createOpenSession`, so the dependency runs one way (connect.js -> session.js). + */ +/** + * @typedef {Object} RequestOpts + * @property {Recordany>} terminal frame.type -> settle. The + * return value resolves the request; throw to reject it. Any type listed here + * ends the request. + * @property {(frame:any)=>void} [onFrame] every non-terminal frame for this id, + * plus every id-less broadcast frame received while the request is in flight + * @property {AbortSignal} [signal] aborting sends {op:"abort", id} and rejects + * @property {HTMLElement} [element] discovery element, if not set on the session + * + * @typedef {Object} SessionOpts + * @property {HTMLElement} [element] default discovery element for every request + * @property {string} [idPrefix] request id prefix (defaults to the kind) + * @property {(...a:any[])=>void} [onLog] + * + * @typedef {{readable: ReadableStream, writable: WritableStream, disconnect: () => void}} Connection + * @typedef {Connection & {writer: WritableStreamDefaultWriter, reader: ReadableStreamDefaultReader}} OpenConnection + * + * @typedef {Object} Session + * @property {(frame: any, opts: RequestOpts) => {promise: Promise, abort: () => void}} request + * @property {() => void} close drop the connection (terminating the worker behind + * it) and reject every in-flight request; the next request reconnects + */ +/** + * Build the `openSession` export, bound to a `connectWorker` implementation. + * Called once from connect.js; consumers use the resulting `openSession`. + * + * @param {(kind: string, opts: {element: HTMLElement}) => Promise} connectWorker + */ +export function createOpenSession(connectWorker: (kind: string, opts: { + element: HTMLElement; +}) => Promise): (kind: string, sessionOpts?: SessionOpts) => Session; +export type RequestOpts = { + /** + * frame.type -> settle. The + * return value resolves the request; throw to reject it. Any type listed here + * ends the request. + */ + terminal: Record any>; + /** + * every non-terminal frame for this id, + * plus every id-less broadcast frame received while the request is in flight + */ + onFrame?: ((frame: any) => void) | undefined; + /** + * aborting sends {op:"abort", id} and rejects + */ + signal?: AbortSignal | undefined; + /** + * discovery element, if not set on the session + */ + element?: HTMLElement | undefined; +}; +export type SessionOpts = { + /** + * default discovery element for every request + */ + element?: HTMLElement | undefined; + /** + * request id prefix (defaults to the kind) + */ + idPrefix?: string | undefined; + onLog?: ((...a: any[]) => void) | undefined; +}; +export type Connection = { + readable: ReadableStream; + writable: WritableStream; + disconnect: () => void; +}; +export type OpenConnection = Connection & { + writer: WritableStreamDefaultWriter; + reader: ReadableStreamDefaultReader; +}; +export type Session = { + request: (frame: any, opts: RequestOpts) => { + promise: Promise; + abort: () => void; + }; + /** + * drop the connection (terminating the worker behind + * it) and reject every in-flight request; the next request reconnects + */ + close: () => void; +}; diff --git a/libraries/patchwork-worker/vitest.config.ts b/libraries/patchwork-worker/vitest.config.ts new file mode 100644 index 00000000..f21db690 --- /dev/null +++ b/libraries/patchwork-worker/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "happy-dom", + globals: true, + passWithNoTests: true, + }, +}); diff --git a/llm-host/.gitignore b/llm-host/.gitignore new file mode 100644 index 00000000..96f32a31 --- /dev/null +++ b/llm-host/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +.pushwork diff --git a/llm-host/README.md b/llm-host/README.md new file mode 100644 index 00000000..1903ca3c --- /dev/null +++ b/llm-host/README.md @@ -0,0 +1,361 @@ +# @grjte/llm-host + +The **host-realm LLM package**. It is the Patchwork side of the LLM: everything +that must run in a real-origin host realm because it touches the settings doc and +the API key. It registers two plugins: + +- **`llm-config-tray`** (`patchwork:component`, tag `system-tray`) — the settings + doc + the model/config picker UI, so no other tool needs to read or write LLM + config. Renders a small icon in the frame's system tray; clicking it, or a + `patchwork:open-tool` event, opens the picker in a popover. +- **`llm`** (`patchwork:worker`) — the LLM worker itself. Its `load()` resolves a + **WorkerSpec** (via `makeLLMWorkerSpec`) that `@grjte/patchwork-worker` serves, so + a consumer reaches it by `connectWorker("llm", …)` — in or out of isolation. + +The design goal, and the reason this package exists as host-realm code: the LLM +worker (and the config + API key it needs) runs in the **host realm**, while a +consumer tool like chat holds only streams — the same code whether or not it runs +inside a sandboxed iframe. + +This README covers both the config tray (below) and the full LLM architecture (from +"Three layers" onward). The **generic** worker machinery it rides on — the +`WorkerSpec` contract, `connectWorker` / `serveWorkerSpec`, discovery and the +transferable-stream handoff, one-worker-per-connection — is documented in +[`../libraries/patchwork-worker/README.md`](../libraries/patchwork-worker/README.md). This package adds +the LLM-specific parts: the op vocabulary, the config/secret trust boundary, and the +config tray. + +--- + +## The config tray (`llm-config-tray`) + +### What it owns + +- **The settings doc.** LLM config lives in a dedicated Automerge document whose + body *is* the config (`provider`, per-provider `model`/`apiKey`/`url`, sampling + params, `toolToggles`, prompt-doc refs, and per-tool/per-doc overrides). The + account doc only holds a *pointer* to it (`accountDoc.toolStorage["llm"]`), + resolved via the `patchwork:tool-storage` provider. The only secret is + `openrouter.apiKey`, stored here and nowhere else. +- **The picker UI.** It reuses `@chee/patchwork-llm`'s `dom()` picker, driven + through the picker's injectable `source` `{read, write}` — backed by + `readScopedConfig` / `writeScopeOverride` / `writeConfig`. So this tool is the + sole writer of the settings doc; the picker never touches the doc singletons + directly. + +### How it's opened + +- **Sidebar icon:** the tray button toggles the popover. +- **`patchwork:open-tool` event:** any tool can dispatch + `new CustomEvent("patchwork:open-tool", { detail: { component: "llm-config-tray", + scope, toolPrompt, toolTools }, bubbles: true, composed: true })`. The tray has a + document-level listener that opens the picker for that `scope` and surfaces the + caller's `toolPrompt` / `toolTools` (tools tagged `defaultOff` render unchecked). + This is how chat's `/model` opens it — and, when chat runs inside the isolation + sandbox, the isolation *open-tool bridge* relays the event to the host so this + host-realm tool opens there. + +### Why host-only + +The settings doc (with the API key) is denylisted from the isolation sandbox, and +resolving it needs `window.repo` + a mounted ``. So config +management can only run in the host realm. This tool is mounted by the frame as host +chrome; consumer tools stay config-free and reach it via the event above. + +--- + +## The packages + +``` + @grjte/patchwork-worker the generic transport (connect / session / serve halves), a + plain library. Knows nothing about LLMs. See its README. The + host-realm provider that drives it (patchwork-worker-provider) + ships from the `providers` package. The LLM uses it unchanged: + it registers a WorkerSpec and connects by the id "llm". + + @chee/patchwork-llm the LLM library. Owns worker.js (transformers.js / OpenRouter / + Ollama), config resolution over the settings doc (config.js: + ensureConfig, callConfig, the apiKey), the picker UI, tool + parsing, request preparation (request.js: prepareGenerate + + buildGeneratePayload, shared by its own generate() and by + worker-spec.js below, so the two paths cannot diverge), and + `createWorker()` — the ONE worker-transport + concern that must live here, because + `new URL("./worker.js", import.meta.url)` has to resolve + against this library's own (cross-origin) URL, and bundlers + only emit the worker chunk when they see that exact literal. + NOTE: this is a Patchwork-coupled library, not a plain npm + one — it imports @inkandswitch/patchwork-{elements,providers}, + reads the account/settings docs, and ships a + element. It has no `plugins` + array, so importing it registers nothing; llm-host is what + makes it a Patchwork worker. + + @grjte/llm-host THIS package — the host-realm glue. Registers THREE plugins: + llm-config-tray (component), llm (patchwork:worker) and llm + (patchwork:worker-client). Owns BOTH halves of the LLM + protocol: src/worker-spec.js (serve half: host policy — which + overrides a consumer may set, system maps, toolToggles, + host-side parsing — over the library's request + preparation) and src/client.js (consume half: makeLLMClient, + pure, delivered to consumers THROUGH THE REGISTRY as a lazy + dist chunk). The library is imported lazily, so nothing + LLM-specific loads until first connect. + + chat (@patchwork/chat*) the CONSUMER. Imports only the transport + (@grjte/patchwork-worker, a library); has NO dependency on + this package. Calls connectWorkerClient("llm") and receives + makeLLMClient's {generate} at runtime; runs no worker; opens + config via event. Reads ZERO config. + + @patchwork/isolation the sandbox. When chat runs inside it, three relays cross the + boundary: worker-channel (streams), open-tool (config picker), + and the registry source-rewrite (so tool code + its automerge + deps load). None of it is LLM-specific. + + threepane the host frame. Mounts patchwork-worker-provider + the config + tray; sets shared-providers on the isolation element. +``` + +## Three layers, each kind-agnostic below the one above + +``` + layer package serve half consume half + ----- ------- ---------- ------------ + transport patchwork-worker serveWorkerSpec openSession / connectWorkerClient + protocol llm-host src/worker-spec.js src/client.js (makeLLMClient) + compute @chee/patchwork-llm worker.js createWorker +``` + +The transport knows nothing about the LLM's op vocabulary; the protocol layer knows +nothing about streams or ids. Both halves of the **LLM protocol** live in this +package and are registered as a PAIR of plugins with the same id, `"llm"`: +`patchwork:worker` resolves to the WorkerSpec (`src/worker-spec.js`, served by the +host provider) and `patchwork:worker-client` resolves to `makeLLMClient` +(`src/client.js`, a factory `(session) => {generate}`). The transport defines that +convention and does the lookup: a tool calls `connectWorkerClient("llm", …)`, which +finds the client plugin in the registry, opens a session for `"llm"`, and returns +`makeLLMClient(session)`. The tool never names this package at build time; the two +halves ship in one doc and cannot drift. `src/client.js` is PURE (imports nothing) +and must stay so — it is the chunk a sandboxed tool loads. A second worker kind +(e.g. transcription) would register its own pair and reuse the transport unchanged. + +## Config ownership — the LLM trust boundary + +This is the part the generic transport can't provide, and the reason llm-host exists +as a host-realm package. + +``` + CONFIG OWNERSHIP CONFIG RESOLUTION + WORKER CONSUMER + (llm-host, host) (llm-host worker-spec, host) (chat) + ------------------ --------------------------- -------- + settings doc ensureConfig(scope) -> CallConfig names a scope + picker UI (config tray) drives worker.js via WorkerSpec writes request frames + apiKey lives here apiKey injected here, never out reads event streams + reads ZERO config +``` + +The consumer never touches config. Everything config-derived is either resolved +host-side from the `scope` chat sends, or rides back on the generation stream (e.g. +the model label). So chat imports nothing from the LLM library, and no secret is +ever reachable from the sandbox. + +`worker-spec.js`'s `resolveForFrame` is where this is enforced: config is resolved +from the frame's `scope` alone; a consumer-supplied `frame.config` is ignored (it +would let a tool pick the provider AND supply its own key, defeating host-side +resolution). The apiKey enters the worker call host-side via `callConfig` and is +never placed in a request or event frame. + +## The LLM op vocabulary + +The transport treats frames as opaque (it reads only `id` and the reserved +`op:"abort"`). The LLM defines this vocabulary on top: + +``` + request (consumer -> worker): {id, op:"generate"|"predict"|"preload"| + "score-tokens"|... , scope, system, tools, + messages|text, sessionKey, ...sampling knobs} + events (worker -> consumer): {id, type:"token"|"stats"|"prediction"|"status"| + "model"|"result"|"error", ...} + result / error are terminal; abort = readable.cancel() +``` + +`worker-spec.js`'s `handle` switches on `op`: `generate` (incl. a `builtin` branch +for the Chrome Prompt API, which runs in-realm with no worker), `predict`, the +analytical ops (`score-tokens`, `compute-importance`, …), and `preload`. The consume +half (`client.js`) handles `token` / `status` / `model` progress plus the `result` / +`error` terminals. These two lists are maintained by hand and must stay in step — a +mismatch is silent (a frame type the serve half emits that the consume half doesn't +handle is simply ignored). + +## The two topologies + +The generic mechanics of each — who answers the subscription, how streams are +transferred within a realm vs. across the boundary — are in the worker README. What +follows is only the LLM-specific detail. + +### Topology A — isolation OFF (chat runs in the host realm) + +Everything is one realm. chat calls `connectWorkerClient("llm")`: the host registry +resolves the `patchwork:worker-client` plugin (this package's entry, then the +`client-*.js` chunk), and the returned client opens a session on first generate. The +`patchwork-worker-provider` (an ancestor in the frame) answers directly and hands chat +the streams by transfer. The worker runs right there, in the host origin (model +cache, WebGPU, `fetch`). + +``` + chat worker-provider llm-host worker-spec worker.js + ---- --------------- ------------------- --------- + connect ──────► answer subscribe ───────► serveWorkerSpec(spec) + spec.open: ensureConfig ──► (reads settings doc) + (settings warm) + ◄── {readable,writable} (transfer) ── + write {op:generate,scope,system,tools,messages} ─────► spec.handle: callConfig(+apiKey) + post to worker ─────────► token / stats / model / + read {type:model,label} ◄── result / error + read {type:token,...} ◄── (stream) + read {type:result,...} ◄── (terminal) +``` + +Opening the config picker: chat dispatches `patchwork:open-tool +{component:"llm-config-tray", scope, ...}`; the tray's document listener opens the +picker. No bridge involved. + +### Topology B — isolation ON (chat runs inside the sandboxed iframe) + +Same chat code, same `connectWorkerClient("llm")` call. The plugin registry is +mirrored into the iframe (every registry type, ungated — entries arrive with the boot +snapshot and as live `plugin-registered` pushes, and `loadWhenReady` waits for a late +one), so the lookup resolves in-sandbox: the iframe module loader fetches +`registry--@grjte--llm-host/dist/index.js` (descriptors only) and then the +`client-*.js` chunk — the ONLY llm-host code that enters the sandbox. The worker +can't run in the opaque-origin iframe (no shared model cache; the settings doc / API +key are denylisted), so the host answers and the streams are transferred **across +the boundary**. The three relays are generic (see ISOLATION.md); the LLM-specific +facts are: + +- **Why the host must answer.** The two reasons are both LLM-shaped: the model cache + / WebGPU device live in the host origin, and the settings doc + API key are + host-only. A worker kind with neither constraint could in principle run + in-boundary — the LLM cannot. +- **What crosses:** only the stream pair (plus the small setup messages). After the + handoff, tokens flow iframe⇄host-worker directly over the transferred port; the + bridge is involved only in *setup*. The API key never crosses — it is applied + host-side inside `worker-spec.js`. +- **Opt-in:** `patchwork:worker-channel` in `shared-providers` enables the worker + handoff, and `llm-config-tray` in `shared-tools` enables the open-tool relay for + the picker. Without the first, the session's subscription is never claimed, + `connectWorker` resolves null after its discovery backstop, and chat degrades. + +``` + chat (iframe) iframe providers-bridge host providers-bridge worker-provider / spec + ------------- ----------------------- --------------------- ---------------------- + connect ─────────► capture unclaimed sub + (subscribe) RPC {providers-bridge, ──────────────────────► gate selector.type ∈ + id, selector} shared-providers + re-dispatch subscribe on host + ◄ provider serves spec: + ensureConfig(scope)+worker + ◄ {type:"change", value:{readable,writable}} + (transfer, host port) + ◄ RPC {providers-bridge-change, + id, value} (TRANSFER across boundary) + ◄ value handed to chat's session + write {op:generate,...} ─────────────────────────────────────────► worker.js (host origin) + read token/model/result ◄──────────────────── (streams flow iframe⇄host directly now) +``` + +Opening the config picker (isolation on): chat dispatches the same +`patchwork:open-tool`; the iframe forwards it over RPC, the host open-tool bridge +gates it on `component ∈ ALLOWED_OPEN_TOOLS` (`["llm-config-tray"]`), and +re-dispatches on the host element so the tray opens **in the host realm**. The picker +— with the settings doc + API key — always runs in the host; the iframe only +*triggers* it. + +## Why @chee/patchwork-llm is NOT loaded in the iframe + +chat imports only the transport (`@grjte/patchwork-worker/client.js`, a library +subpath baked by `@chee/patchwork-bundles`). What it then loads from THIS package — +through the registry, not an import — is `dist/index.js` (three plugin descriptors, +no top-level imports) and the `client-*.js` chunk (imports nothing). Neither pulls +in the LLM library. `patchwork-llm-*.js` and `worker-spec-*.js` are reachable only +from the `patchwork:worker` plugin's `load()`, which only the host-realm provider +ever calls. So the transformers.js runtime, the config code, and the API key never +enter the sandbox graph. Two invariants keep it that way: `src/client.js` must never +import `@chee/patchwork-llm` or `./worker-spec.js` (vite would pull the library chunk +into the client chunk's graph), and `src/index.ts` must keep everything behind +`load()`. + +(The isolation registry marker is per package, so a sandboxed tool *could* fetch the +other chunks deliberately — as it could the old raw subpath. Nothing secret is in +the served files: the API key lives in the settings doc, which is denylisted.) + +(How tool code and its automerge deps load in the iframe at all — the `registry--` +marker rewrite of the baked `automerge:` dep — is generic isolation machinery; see +ISOLATION.md, "Package registry in iframe".) + +## Data flow + security summary + +- **API key never leaves the host.** Stored in the settings doc (host-only, + denylisted from the iframe), read only by `worker-spec.js` during `callConfig`, and + injected into the worker call host-side. Never in a request frame (chat sends only + `scope`) nor in any event frame. +- **Config is host-resolved from `scope`.** chat sends `{toolId, docId}`; the host + reads the real provider/model/prompts/toolToggles. chat reads no config; the model + *label* comes back as a `{type:"model"}` event frame. +- **Only streams cross the boundary** (plus the small setup messages). After handoff, + tokens flow iframe⇄host-worker directly over the transferred port. +- **Two opt-ins gate the bridges:** `shared-providers` ∩ `ALLOWED_PROVIDERS` + (includes `patchwork:worker-channel`) and `shared-tools` ∩ `ALLOWED_OPEN_TOOLS` + (open-tool). Neither is on by default. +- **isolation stays generic** — it relays worker-channel, open-tool, and the registry + source-rewrite; it imports nothing LLM-specific. + +## Is this the pattern for other workers? + +Yes, in this shape: **one package registers a `patchwork:worker` / `patchwork:worker-client` +pair under one id.** The serve half is a WorkerSpec; the consume half is a factory +`(session) => api`. Consumers call `connectWorkerClient(kind)` and never import the +package. The transport and the isolation bridges are untouched by a new kind. + +The **library + `-host` split is specific to the LLM**, not part of the pattern. It +exists because `@chee/patchwork-llm` predates the transport and is also used directly +(same realm) by other tools, so its request preparation is shared rather than moved. +A worker built transport-first keeps compute, spec and client in one package. + +## File map + +``` + @grjte/patchwork-worker ............. the generic transport library (see its README) + providers/src/WorkerProvider.ts ..... the host-realm provider that drives it + @chee/patchwork-llm worker.js ...... the compute worker; createWorker() constructs it + @chee/patchwork-llm config.js ...... settings doc + config resolution (ensureConfig, + callConfig, the apiKey), + picker.js (the picker UI) + @chee/patchwork-llm request.js ..... prepareGenerate + buildGeneratePayload: request + preparation shared by generate() and worker-spec + @grjte/patchwork-worker client.js ... connectWorkerClient(kind): registry lookup of the + paired worker-client plugin + openSession(kind) + llm-host/src/worker-spec.js ......... makeLLMWorkerSpec: host policy (sampling-only + overrides, system maps, toolToggles, host-side + parsing) over the library's request + preparation — the SERVE half of the protocol + llm-host/src/client.js .............. makeLLMClient(session) → {generate} — the CONSUME + half (pure; a lazy dist chunk resolved via the + patchwork:worker-client plugin) + llm-host/src/index.ts ............... registers llm-config-tray + the "llm" worker and + worker-client plugins + llm-host/src/LlmConfigTray.tsx ...... host config tray: picker + patchwork:open-tool listener + chat/src/components/ChatRoot.tsx .... connectWorkerClient("llm") → client.generate(); + /model dispatches patchwork:open-tool + isolation/src/bridges/providers-bridge.ts host + iframe halves of the provider relay, + incl. worker-channel and its transferred streams + isolation/src/bridges/open-tool-bridge.ts host + iframe halves of the open-tool relay + isolation/src/bridges/registry-bridge.ts marker mapping + served-source dep rewrite + threepane/src/PatchworkFrame.tsx .... mounts worker-provider + config tray + threepane/src/components/IsolatedDocumentArea.tsx sets shared-providers/shared-tools +``` + +## Build / sync + +Bundled with vite + `@chee/patchwork-bundles` (the `automerge:`→cross-origin rewrite +for its `@chee/patchwork-llm` dep). `pnpm build`, then `pushwork sync`. diff --git a/llm-host/client.js b/llm-host/client.js new file mode 100644 index 00000000..f4f4cb4e --- /dev/null +++ b/llm-host/client.js @@ -0,0 +1,301 @@ +/** + * The LLM consume-half — the mirror of worker-spec.js. + * + * worker-spec.js (the SERVE half) turns a consumer's request frames into worker + * traffic; this file (the CONSUME half) turns a tool's `generate(...)` call into + * those request frames and reads the event stream back. Together they are the LLM + * *protocol* layer, sitting above the generic transport (@grjte/patchwork-worker) + * and below any tool that wants to generate. + * + * ⚠ PURE by design — this file imports NOTHING. The transport is injected: a tool + * opens a session with @grjte/patchwork-worker's `openSession` and passes it to + * `makeGenerate`. That keeps this file a raw sandbox-safe subpath (like the + * transport's connect.js) with no cross-package import to bake, and it mirrors the + * transport's own shape — `createOpenSession(connectWorker)` injects its primitive + * the same way. The tool owns the CONNECTION (which worker kind, when to open); + * this file owns only the PROTOCOL. + * + * Frame vocabulary (must stay in step with worker-spec.js): + * request: {id, op:"generate", scope, system, tools, messages|text, sessionKey} + * events: {id, type:"token"|"stats"|"prediction"|"status"|"model"| + * "result"|"error", ...} result/error terminal. + */ + +/** + * @typedef {Object} GenOpts + * @property {any} [scope] {toolId, docId} — the host resolves config for this scope + * @property {string|Record} [system] string | {default, [provider]} + * @property {any[]} [tools] tool descriptors {name, description, defaultOff?, parameters?} + * @property {string} [sessionKey] + * @property {HTMLElement} [element] a node in the mounted , for discovery + * @property {AbortSignal} [signal] + * @property {(delta:string, full:string)=>void} [onToken] + * @property {(message:string)=>void} [onStatus] + * @property {(label:string)=>void} [onModel] + * + * @typedef {Object} GenResult + * @property {string} text + * @property {any[]|null} toolCalls + * @property {string} [toolMode] + */ + +/** + * Build a `generate` bound to an already-open transport session. The tool opens + * the session (`openSession("llm", {...})` from @grjte/patchwork-worker) and hands + * it in; this file only ever calls `session.request(...)`. + * + * @param {{request: (frame:any, opts:any)=>{promise:Promise}}} session + */ +export function makeGenerate(session) { + /** + * Generate a completion. Streams tokens via opts.onToken; resolves with the + * final text + any structured tool calls. Tool EXECUTION stays in the tool — + * the worker only receives tool descriptors and returns calls to run. + * @param {any[]|string} messages + * @param {GenOpts} [opts] + * @returns {Promise} + */ + return function generate(messages, opts = {}) { + const payload = typeof messages === "string" ? {text: messages} : {messages} + + const {promise} = session.request( + { + op: "generate", + sessionKey: opts.sessionKey, + scope: opts.scope, + system: opts.system, + tools: opts.tools, + ...payload, + }, + { + element: opts.element, + signal: opts.signal, + terminal: { + result: (/** @type {any} */ f) => ({ + text: f.text, + toolCalls: f.toolCalls || null, + toolMode: f.toolMode, + }), + error: (/** @type {any} */ f) => { + throw new Error(f.message) + }, + }, + // prediction / stats frames are ignored by the caller unless it asks. + onFrame: (/** @type {any} */ f) => { + if (f.type === "token") opts.onToken?.(f.delta, f.text) + else if (f.type === "status") opts.onStatus?.(f.message) + else if (f.type === "model") opts.onModel?.(f.label) + }, + } + ) + + return /** @type {Promise} */ (promise) + } +} + +/** + * @typedef {{name: string, args: Record}} ToolCall + */ + +/** + * Extract tool calls from a model's text output, for providers without native + * function calling (local / template). Handles LFM bracket syntax, `` + * XML, and fenced/bare JSON. Pure text — copied from @chee/patchwork-llm's + * tools.js (the repo's standalone-package rule forbids linking, so a copy is the + * sanctioned answer; it lives here, in the protocol layer, once for every LLM + * consumer instead of once per tool). + * + * @param {string} text + * @returns {ToolCall[]} + */ +export function parseToolCalls(text) { + if (!text) return [] + /** @type {ToolCall[]} */ + const calls = [] + const push = (/** @type {any} */ obj) => { + const name = obj?.name || obj?.tool + if (!name) return + let args = obj.arguments ?? obj.args ?? {} + if (typeof args === "string") { + try { + args = JSON.parse(args) + } catch { + args = {} + } + } + calls.push({name, args: args || {}}) + } + const thinkingEnd = text.lastIndexOf("") + const lfmRegionStart = thinkingEnd === -1 ? 0 : thinkingEnd + "".length + const lfmMatch = text + .slice(lfmRegionStart) + .search(/\[\s*[a-zA-Z_][a-zA-Z0-9_]*\s*\(/) + const lfmStart = lfmMatch === -1 ? -1 : lfmRegionStart + lfmMatch + if (lfmStart !== -1) { + let depth = 0 + let quote = "" + let escaped = false + let lfmEnd = -1 + for (let i = lfmStart; i < text.length; i++) { + const ch = text[i] + if (escaped) { + escaped = false + continue + } + if (quote) { + if (ch === "\\") escaped = true + else if (ch === quote) quote = "" + continue + } + if (ch === "'" || ch === '"') { + quote = ch + continue + } + if (ch === "[" || ch === "(" || ch === "{") depth++ + else if (ch === "]" || ch === ")" || ch === "}") { + depth-- + if (depth === 0) { + lfmEnd = i + break + } + } + } + if (lfmEnd !== -1) { + const split = (source = "") => { + const parts = [] + let start = 0 + let nested = 0 + let string = "" + let slash = false + for (let i = 0; i < source.length; i++) { + const ch = source[i] + if (slash) { + slash = false + continue + } + if (string) { + if (ch === "\\") slash = true + else if (ch === string) string = "" + continue + } + if (ch === "'" || ch === '"') string = ch + else if (ch === "[" || ch === "(" || ch === "{") nested++ + else if (ch === "]" || ch === ")" || ch === "}") nested-- + else if (ch === "," && nested === 0) { + parts.push(source.slice(start, i).trim()) + start = i + 1 + } + } + const last = source.slice(start).trim() + if (last) parts.push(last) + return parts + } + const literal = (source = "") => { + let json = "" + for (let i = 0; i < source.length; i++) { + const ch = source[i] + if (ch !== "'" && ch !== '"') { + json += ch + continue + } + let value = "" + for (i++; i < source.length; i++) { + const next = source[i] + if (next === ch) break + if (next !== "\\") { + value += next + continue + } + const escaped = source[++i] + value += + escaped === "n" + ? "\n" + : escaped === "r" + ? "\r" + : escaped === "t" + ? "\t" + : escaped + } + json += JSON.stringify(value) + } + try { + return JSON.parse( + json.replace(/\bTrue\b/g, "true").replace(/\bFalse\b/g, "false").replace(/\bNone\b/g, "null") + ) + } catch { + return source + } + } + for (const expression of split(text.slice(lfmStart + 1, lfmEnd))) { + const match = /^([a-zA-Z_][a-zA-Z0-9_]*)\s*\(([\s\S]*)\)$/.exec(expression) + if (!match) continue + const args = Object.fromEntries([]) + for (const arg of split(match[2])) { + const eq = arg.indexOf("=") + if (eq < 1) continue + const name = arg.slice(0, eq).trim() + if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) continue + args[name] = literal(arg.slice(eq + 1).trim()) + } + calls.push({name: match[1], args}) + } + if (calls.length) return calls + } + } + let m + const xml = /\s*([\s\S]*?)\s*<\/tool_call>/g + let sawXml = false + while ((m = xml.exec(text))) { + sawXml = true + try { + push(JSON.parse(m[1].trim())) + } catch {} + } + if (sawXml) return calls + const fence = /```(?:json|tool[_-]call)?\s*([\s\S]*?)```/g + while ((m = fence.exec(text))) { + try { + push(JSON.parse(m[1].trim())) + } catch {} + } + if (calls.length) return calls + // Bare JSON objects — brace-depth-aware scan so nested objects (e.g. + // "arguments": { ... }) are captured whole instead of truncated at the + // first inner `}`. + let depth = 0, + start = -1, + inStr = false, + esc = false + for (let i = 0; i < text.length; i++) { + const ch = text[i] + if (esc) { + esc = false + continue + } + if (ch === "\\" && inStr) { + esc = true + continue + } + if (ch === '"') { + inStr = !inStr + continue + } + if (inStr) continue + if (ch === "{") { + if (depth === 0) start = i + depth++ + } else if (ch === "}") { + depth-- + if (depth === 0 && start >= 0) { + const block = text.slice(start, i + 1) + if (/"(?:name|tool)"/.test(block)) { + try { + push(JSON.parse(block)) + } catch {} + } + start = -1 + } + } + } + return calls +} diff --git a/llm-host/package.json b/llm-host/package.json new file mode 100644 index 00000000..ffc8b1e3 --- /dev/null +++ b/llm-host/package.json @@ -0,0 +1,34 @@ +{ + "name": "@grjte/llm-host", + "version": "0.2.0", + "description": "The host-realm LLM package: owns the settings doc + config picker (llm-config-tray) and registers the LLM worker as a patchwork:worker plugin. The library itself is @chee/patchwork-llm.", + "type": "module", + "main": "./dist/index.js", + "exports": { + ".": "./dist/index.js" + }, + "scripts": { + "build": "vite build", + "dev": "vite build --watch", + "push": "pnpm build && pushwork sync", + "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\"", + "test": "vitest run", + "test:watch": "vitest" + }, + "author": "grjte", + "license": "MIT", + "dependencies": { + "@chee/patchwork-llm": "automerge:m6dt2f4hYdhyvmFDvXYbYDdNQey", + "solid-js": "^1.9.11" + }, + "devDependencies": { + "@chee/patchwork-bundles": "^0.2.0", + "@inkandswitch/patchwork-bootloader": "^0.0.3", + "@inkandswitch/patchwork-plugins": "^0.0.5", + "happy-dom": "^15.11.7", + "typescript": "^5.8.3", + "vite": "^5.4.19", + "vite-plugin-solid": "^2.10.0", + "vitest": "^3.2.4" + } +} diff --git a/llm-host/pnpm-lock.yaml b/llm-host/pnpm-lock.yaml new file mode 100644 index 00000000..cd54ddfe --- /dev/null +++ b/llm-host/pnpm-lock.yaml @@ -0,0 +1,2072 @@ +--- +lockfileVersion: '9.0' + +importers: + + .: + configDependencies: + pnpm-plugin-patchwork: + specifier: 0.4.1 + version: 0.4.1 + +packages: + + pnpm-plugin-patchwork@0.4.1: + resolution: {integrity: sha512-IzQJkoeQaSaYSa1bRko3KUJ+6nBmYg8Qxuyy0llnhPfAGSDP/+gM/Z7R0HGLtTpCE3K57CXS9DnAItqb/pNIZQ==} + +snapshots: + + pnpm-plugin-patchwork@0.4.1: {} + +--- +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@chee/patchwork-llm': + specifier: automerge:m6dt2f4hYdhyvmFDvXYbYDdNQey + version: automerge:m6dt2f4hYdhyvmFDvXYbYDdNQey@kHSoEYZn3QrxfLpLC7Lbm2j979WAsxGgNMH8wZd6jbasFTkx9(@automerge/automerge-repo@2.6.0-subduction.40) + solid-js: + specifier: ^1.9.11 + version: 1.9.15 + devDependencies: + '@chee/patchwork-bundles': + specifier: ^0.2.0 + version: 0.2.0(@automerge/automerge-repo-keyhive@0.0.0-alpha.71)(@automerge/automerge@3.2.1)(@automerge/vanillajs@2.5.0)(@keyhive/keyhive@0.0.0-alpha.40) + '@inkandswitch/patchwork-bootloader': + specifier: ^0.0.3 + version: 0.0.3(@automerge/automerge-repo-keyhive@0.0.0-alpha.71)(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.1)(@automerge/vanillajs@2.5.0)(@keyhive/keyhive@0.0.0-alpha.40) + '@inkandswitch/patchwork-plugins': + specifier: ^0.0.5 + version: 0.0.5 + happy-dom: + specifier: ^15.11.7 + version: 15.11.7 + typescript: + specifier: ^5.8.3 + version: 5.9.3 + vite: + specifier: ^5.4.19 + version: 5.4.21(@types/node@20.19.43) + vite-plugin-solid: + specifier: ^2.10.0 + version: 2.11.14(solid-js@1.9.15)(vite@5.4.21(@types/node@20.19.43)) + vitest: + specifier: ^3.2.4 + version: 3.2.7(@types/debug@4.1.13)(@types/node@20.19.43)(happy-dom@15.11.7) + +packages: + + '@automerge/automerge-repo-keyhive@0.0.0-alpha.71': + resolution: {integrity: sha512-bDP6QPNTG9eCYVnLW/PuODLFmGrOe4rwaOSAWLgW9VrHg/5aBlP0PmrzeJ58loYvGlqYeN6PWrV+GFDqCPUcug==} + + '@automerge/automerge-repo-network-broadcastchannel@2.5.0': + resolution: {integrity: sha512-57rUTYf0W6kL9DXwjRtyGLqeyz65tDIpiNQdc1nXHnN4VkNMvNo0xzb37nyFKWHEqPWA0DGCcBA8HRlbVHHyHw==} + + '@automerge/automerge-repo-network-messagechannel@2.5.0': + resolution: {integrity: sha512-G7m161tId4djPMzDPlP7nIEsDY7ddbKfKvpJP7kkVqv3OA/Ml8fzyKaU7C1++s/g0FSNCTiN6hOhFCbSkb6/Dw==} + + '@automerge/automerge-repo-network-websocket@2.5.0': + resolution: {integrity: sha512-zrwbkW8hk3fWkXdeaH+YDfGG63WaB9WLfWYyinZx2AQv4mr6D6UhIhviIjweMmRxMLen6NlIcdu3toHV33ZuKg==} + + '@automerge/automerge-repo-network-websocket@2.5.0-alpha.2': + resolution: {integrity: sha512-EtdsSA9PhuiWWbvT36DGylBeGCL7Aop8nSe4FrBYchBCWpqqZOBLdbxD0crwbCUwTBTJy76pXhSHjObQi0y4zQ==} + + '@automerge/automerge-repo-storage-indexeddb@2.5.0': + resolution: {integrity: sha512-7MJYJ5S6K7dHlbvs5/u/v9iexqOeprU/qQonup28r2IoVqwzjuN5ezaoVk6JRBMDI/ZxWfU4rNrqVrVlB49yXA==} + + '@automerge/automerge-repo@2.5.0': + resolution: {integrity: sha512-bdxuMuKmxw0ZjwQXecrIX1VrHXf445bYCftNJJ5vqgGWVvINB5ZKFYAbtgPIyu1Y0TXQKvc6eqESaDeL+g8MmA==} + + '@automerge/automerge-repo@2.5.0-alpha.2': + resolution: {integrity: sha512-Sjl1CaD4IpmboNIG9/EL9Run+AKrL8/1h3Om1NZp9syBnJN8OWjeG4ttpQce6k8HdPyl8sllAHYy8/Q4Ukh39w==} + + '@automerge/automerge-repo@2.6.0-subduction.40': + resolution: {integrity: sha512-9r3GS8FtZKoIJOtJqkQR0ukF7HSu+IyV5Iqf7MP3ELDz/TEaUbZV8ysyl5Ju4fVZjmagI/n3oH2h4ySk4FdEFQ==} + engines: {node: '>=22.13'} + + '@automerge/automerge-subduction@0.16.0': + resolution: {integrity: sha512-BOOn/4p8cF4LhjDUQdgM3qyZFya+M7ZuZywuC99x/kbhk4UK7dw/QcsifcHG5E1TAW4R/K17ZGFUC5nCVUSSWg==} + + '@automerge/automerge@3.2.1': + resolution: {integrity: sha512-xsEZlZe8lWyVCbD7Z7roH4X6ZKlRxMmmumb0M5CuxJWptUEfYsf+1z9lFEJh4aDBbWTLAgxsXd+TBi6jKTj0+g==} + + '@automerge/automerge@3.3.0-fragments.2': + resolution: {integrity: sha512-F5KL+YXD0GMFH/as0/kJ3+lotpuJsH1SSrhhKdYSGtR+S9ytBNTw+s3pAjF2Vr0+ZdTMHxsGjqj5SRkJiSNokQ==} + + '@automerge/vanillajs@2.5.0': + resolution: {integrity: sha512-18Z179fBkRYhfJOvzcz5NU+TSJOfOBez1ZQqjNnRlmHB0xwaC9P/MFuuc9ujWtJSiRAh6jzB3HxUZpCIjEfXFw==} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.18.6': + resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.9': + resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-jsx@7.29.7': + resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.8': + resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + resolution: {integrity: sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA==} + cpu: [arm64] + os: [darwin] + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + resolution: {integrity: sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q==} + cpu: [x64] + os: [darwin] + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + resolution: {integrity: sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg==} + cpu: [arm64] + os: [linux] + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + resolution: {integrity: sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ==} + cpu: [arm] + os: [linux] + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + resolution: {integrity: sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA==} + cpu: [x64] + os: [linux] + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + resolution: {integrity: sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA==} + cpu: [x64] + os: [win32] + + '@chee/patchwork-bundles@0.2.0': + resolution: {integrity: sha512-Hc8YhdYM1s/6ij08wMFFBa0+LUyPyW7EBh8yrIsWtBzurWcsHnCtdF7ObZtWVqslkLjlgLqcvpDHYpWhfg6I0g==} + + '@chee/patchwork-llm@automerge:m6dt2f4hYdhyvmFDvXYbYDdNQey@kHSoEYZn3QrxfLpLC7Lbm2j979WAsxGgNMH8wZd6jbasFTkx9': + resolution: {automergeUrl: automerge:m6dt2f4hYdhyvmFDvXYbYDdNQey, type: custom:automerge} + version: 0.3.0 + peerDependencies: + '@automerge/automerge-repo': ^2.5.6 + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@inkandswitch/patchwork-bootloader@0.0.3': + resolution: {integrity: sha512-FPVsH+ncVw4gPWUvOweotcTdL421Dg+/KcYBOUVyqZ3PxpWoLwLPw1jevZVMuGrIURR2MNkLCQqVPd5tlZ2qsA==} + peerDependencies: + '@automerge/automerge': 3.2.1 + '@automerge/automerge-repo': 2.5.0 + '@automerge/automerge-repo-keyhive': 0.0.0-alpha.71 + '@automerge/vanillajs': 2.5.0 + '@keyhive/keyhive': 0.0.0-alpha.40 + + '@inkandswitch/patchwork-filesystem@0.0.3': + resolution: {integrity: sha512-x6yqX1Xyr05ug4YfRyfKJ7jSm/zjhMxU7g/e70nz4QbxfLmcG98LHB9qAkFI2zd+VvKLnTvSpPGu2UvJzXGciw==} + + '@inkandswitch/patchwork-filesystem@0.2.9': + resolution: {integrity: sha512-To2zBNQoAC4BT+PwpZrc0aKZhjLOTd0poJJs/wYEP1wX3r9xr97Og9CdLEvsqsVnEIUXEdZvizH0fbmY0VA14w==} + peerDependencies: + '@automerge/automerge': '*' + '@automerge/automerge-repo': '*' + + '@inkandswitch/patchwork-plugins@0.0.5': + resolution: {integrity: sha512-pQv5M7asDdN8J2moTmLoSnqA8FC/vcOuNJnKveL3MzjEhH0GQdIl6vV3k5jxs83ZpB8stwceE85Zsve/QRnVSA==} + + '@inkandswitch/patchwork-providers@0.3.0': + resolution: {integrity: sha512-CUxWbONfOiz5SCzOpX/7zDTMePWgq7e4778eBJpbjC5Et4lnyL2/UrOZ6YJ4HiCG7r7MbfM86GUi63bqjTeutg==} + peerDependencies: + '@automerge/automerge-repo': '*' + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@keyhive/keyhive@0.0.0-alpha.40': + resolution: {integrity: sha512-FlvtX6ZK2q47OqqJZ/8G+SAevKJxjSdldhwJPlhUT9QTIv0LNmJO17pkEtLJ0VVrmPryP0tKQDIaQtmcE1f7Tg==} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@rollup/rollup-android-arm-eabi@4.63.4': + resolution: {integrity: sha512-I+BSHzTAhKN2n7ZwGZsegGcZjDpLqFOMAtJz/u6uFGe0pUFbq56dEHjqJV/ZUdRJtNXNxA+hREUatZBvMR3Oiw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.63.4': + resolution: {integrity: sha512-pu3BdjS2LtEzRu2elmGzS3fIeWSZy4BMDIaLNwjorO76+k2d0LMluijhsDx3KQyQBQ/lLUZCQA9/s6csvUfuhw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.63.4': + resolution: {integrity: sha512-xfSrj9MHnWK9GaSqT9U0ImHtH/N8WZlHLx4cZHiuLcqs640hvZ3hLPd5UR2AZS57FaE8HrRUSpltbZdWRxHiDA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.63.4': + resolution: {integrity: sha512-bqU99PLJb/dqb3S0GIMdeuyAEETSUgZBoqXYd3Sd+WCsV+MmPhnN6JrotWyir31+QgH7EvvE5/mwGJlEoci8Fw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.63.4': + resolution: {integrity: sha512-JinsFZ5G40oXQb+sUuiA5x689vhr6dDYK0H0NL+rwKdL6CqnmYN8PE4ZwfRSoIjrCxqTQG/SLfTtSvHeGxoVlw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.63.4': + resolution: {integrity: sha512-GAdA4UxpiNm27cLHr2GqXBpAD0x9FqwYBY7/YSP0Ss0/PNi4k8gbviqpIpYbVSRBaS2ZcegXEzgTQMbRNCwxCw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.63.4': + resolution: {integrity: sha512-qDd6NoA1znaLjp4jR5U/KWCdLAKDJNB8W9ChbbDaKbo0xA+Atln5HK6LFCZ4oJQpemtRZA288DCirFRjrspptw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.63.4': + resolution: {integrity: sha512-WtB5Tz5KTNINb8ZA+8sQ7bmjuS1JrRT7YverYIhUGdWWDlpzVWmIwuZE+jidkEXUn1l0zrEkaIMa8dHF3NGcsA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.63.4': + resolution: {integrity: sha512-VcQ3L1tjnkKzWjryAVaFhHEWcqOfICX9uxVVoDzm2t0DpgKRHd2zOpVrJc0xsWeBZcBFyYROCIBdyR/fS174pg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.63.4': + resolution: {integrity: sha512-6+ZQX6P5s0cMDN2Ypb8Lbm2+/sZYmZjdaYny992ujUU9UKi/4CWoJWsl1pNvjWJHNHGK51m+jKGLlh1ylb2ifQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.63.4': + resolution: {integrity: sha512-D72ZnvkFkBXOfzMMQLcwfPLyGkKb7HZ9/mf97B7v6/P5Lbv4oFOtSY/uHbS8lH6uKUOxoKiuokdb50XZSzzbJw==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.63.4': + resolution: {integrity: sha512-piU6BxeqA3O9KSu3kRCIQQtNqFFaTu21SEV4FwaRZowpnj3bLaWPZHw+xFqCs0XlJ+aOH3PTRWGoglH+mKA/OA==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.63.4': + resolution: {integrity: sha512-/5PGpHwqt2EEEOUs1XwzubE/ucr0dWDQ+to3zqi4Ds7EWpwtQ79wXc4JBoxqj/OwpawTsKWzJxHfSuBOq3DrWA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.63.4': + resolution: {integrity: sha512-cX3beZDLWt7G2oJF+nhChiT+qtaihs+S2xi7ziGmVB+2pwPng6D0Ed0HmElQOgv2UsUmSJJLGwpBao/3TDx3VA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.63.4': + resolution: {integrity: sha512-1uz2mGWHyptR7DgHHrlbdRAjXK7v7elGZ9lMja910/RP+ZYbX6xAmCiU9UZSX4hqmgtHMv6lr5l3kq1HIOpcag==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.63.4': + resolution: {integrity: sha512-nLS8topojxyz7SRpKR2IODRpQ0XPZ+xaOXvT3+hqK/Uy8Lo5HFgkkIBiIrCu5tL5YqzTvgovGw55PwpahTAGig==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.63.4': + resolution: {integrity: sha512-gs7DRKotr3l3q+jGPQBjH0ng1FjlEDm5ueQrkw5JtQvtLyEIcLASqAEaor56BhkKRzk+IcQzrcanBdb/bBQn8g==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.63.4': + resolution: {integrity: sha512-791ET7W17NnScOZM7h4dX5hYspxE28htPFsb1awY/NRR8+PRNkS53e475rDdxXXDrP+kwnCcNWg9CX5ztn/Aqw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.63.4': + resolution: {integrity: sha512-iwZQRcmj7g88g3tzefIrQY7qvmuA/cfYwhrDtTBhsmukO4U2huVO5W+86XacUMRvdSFVAc6kZUZy21JaRwiB9w==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.63.4': + resolution: {integrity: sha512-dVHFp9gRWrdTpnqQuGfCwd7hOQDatK1VCP2iWhLY/cGrOQs/ucFzJ6A5SRqbXX12ZDI8EUuejSM5kwg+ja7Png==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.63.4': + resolution: {integrity: sha512-t3NlauOW6gxZVVFcBEnO62Cb4wbyDFL416gTg1uFI/2tgqYQlf69FbSE115Ajre9I+c26Lk4mcmdFUsS/DGifQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.63.4': + resolution: {integrity: sha512-xWuIaSye5FWZF8+UYtVEcHtRJDN5kN9Kfgxx3Kq8XIov9KSKbc1fiqQCm90SKrgQbUXZelbnUhnlUJmfSE7P9A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.63.4': + resolution: {integrity: sha512-9ALJJUOg/ZflMJepVo2PlgsGxSaxN7SQ4Z8GoZfVlarWr6r3rkHUNsd/zAio7p4YMtChSMXPionxej4Hkf6CXQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.63.4': + resolution: {integrity: sha512-blj9z5qx/Pv4WU0W1NMFDB97e0JH5ed+aZGywW8WCvp/NhWX/4PFAq5uu6Q0AebNn+Vo6KzUYDT++JzTT5ojlQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.63.4': + resolution: {integrity: sha512-Erx822VRBwLa124shbj+wNXe//BOgMEctDV0m1aqTQdNO1S69DgNUCFKC1RCeZfixs1J31l6igk1ziyXErbigQ==} + cpu: [x64] + os: [win32] + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@20.19.43': + resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} + + '@types/serviceworker@0.0.153': + resolution: {integrity: sha512-/cg6dFEkNchJLyRCGo4Gb8mF200qr3xskM5dCPgbtK0OzXxcFcXa6BEBdyG7JksRsTrvCR+V6aFPncoOYAwYhQ==} + + '@vitest/expect@3.2.7': + resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} + + '@vitest/mocker@3.2.7': + resolution: {integrity: sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.7': + resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==} + + '@vitest/runner@3.2.7': + resolution: {integrity: sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==} + + '@vitest/snapshot@3.2.7': + resolution: {integrity: sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==} + + '@vitest/spy@3.2.7': + resolution: {integrity: sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==} + + '@vitest/utils@3.2.7': + resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + babel-plugin-jsx-dom-expressions@0.40.10: + resolution: {integrity: sha512-lxve6Y02YiZTldB7efKpnbf1BH00XCFZNYYW235jSGsYaJNFtHrYlKV6/O+miHbjqpIr9FTe5+0no4hofAMbfA==} + peerDependencies: + '@babel/core': ^7.20.12 + + babel-preset-solid@1.9.15: + resolution: {integrity: sha512-GBmg1OiPb+OwcH51XbDAKPtvrPfQW7rCJTJxcp8+yhtWwN+kqnbEJk2SgVybd+uhTxTKAvjaFyiQSr/eUZBwzg==} + peerDependencies: + '@babel/core': ^7.0.0 + solid-js: ^1.9.15 + peerDependenciesMeta: + solid-js: + optional: true + + base-x@4.0.1: + resolution: {integrity: sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==} + + base-x@5.0.1: + resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + + baseline-browser-mapping@2.11.25: + resolution: {integrity: sha512-gMmEShwwq7FJqMwvfRwvCl00v4kN+KOfJqXn+f4nrufak5gNHJOksd/60Dvjuz7sI8Y5WiSFBa8FEYr+zoyqCw==} + engines: {node: '>=6.0.0'} + hasBin: true + + browserslist@4.29.0: + resolution: {integrity: sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bs58@5.0.0: + resolution: {integrity: sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==} + + bs58@6.0.0: + resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} + + bs58check@3.0.1: + resolution: {integrity: sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==} + + bs58check@4.0.0: + resolution: {integrity: sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + caniuse-lite@1.0.30001810: + resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==} + + cbor-extract@2.2.2: + resolution: {integrity: sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng==} + hasBin: true + + cbor-x@1.6.6: + resolution: {integrity: sha512-8QiD9PGOxyQHo7s2pzwTBH6lTjqekxPdl9Aq6fXvZgCuCJHOht1puDEA/fTr6mciB76c+M+Gi0qT2i1a4pm4Wg==} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + electron-to-chromium@1.5.433: + resolution: {integrity: sha512-5lCAbyZBjtmUt/RAGHRqrL2q0oEFRThDAsZHHDn9XHa89Qw7gMYOeSicBTy+AHfvo0r6vwsZvqNJTQIQy1BLzA==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + happy-dom@15.11.7: + resolution: {integrity: sha512-KyrFvnl+J9US63TEzwoiJOQzZBJY7KgBushJA8X61DMbNsH+2ONkDuLDnCnwUiPTF42tLoEmrPyoqbenVA5zrg==} + engines: {node: '>=18.0.0'} + + html-entities@2.3.3: + resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} + + is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + merge-anything@5.1.7: + resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} + engines: {node: '>=12.13'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-gyp-build-optional-packages@5.1.1: + resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} + hasBin: true + + node-releases@2.0.56: + resolution: {integrity: sha512-x0InOIyzgdk+eyaWaRJFH5snEtiImgBgblZ2CyPrLmqqcuMQkEvcDPHbzqbD8eDsSeJbVOjn+crzyzHaM4D+/A==} + engines: {node: '>=18'} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + + resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + + rollup@4.63.4: + resolution: {integrity: sha512-4U0liVayNIoLp3GFl1FcI8561WepLnZ1rqfraGh7S9B3Ur5F9S283y8Futii7RUU2C/97tOBmBy7nYvhoiOpbQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + seroval-plugins@1.5.6: + resolution: {integrity: sha512-HXuLAX2pu/UByPpaeo/TaMfvMIi+1QqIoPJYCcAtU8QkVNwgR6MPlGuCQTErV1JwraaMbYaWVIBX7mppzGLATQ==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.5.6: + resolution: {integrity: sha512-rVQVWjjSvlINzaQPZH5JFqsqEsIWdTxY3iJZCnTL/5gQbXIRooVZKI60tVCkOVfzcRPejboxO2t0P89dg5mQaA==} + engines: {node: '>=10'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + solid-js@1.9.15: + resolution: {integrity: sha512-EeiY2xfpZJqPLjXspVEKjAII4yv8NyG//NxZ3IpOFHdUNnnTyL0uJOeS9LWGvA7cFCz5y94cjFwYlmw5Luncsg==} + + solid-refresh@0.6.3: + resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==} + peerDependencies: + solid-js: ^1.3 + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + tinyargs@0.1.4: + resolution: {integrity: sha512-5OpdhMRRE70j0zT7mrBpx12FJI+y0EGx8zMe8Vl2/aiwGgpW3X70OVHlsHmHeeG0ncnnWdq1o+k/S5d9ONgzGA==} + engines: {node: '>=14'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.6: + resolution: {integrity: sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==} + engines: {node: '>=14.0.0'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} + + update-browserslist-db@1.3.3: + resolution: {integrity: sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uuid@14.0.2: + resolution: {integrity: sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==} + hasBin: true + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite-plugin-solid@2.11.14: + resolution: {integrity: sha512-7ZVBt8rpoyqmlwin2kRIUveaHoF6/kulY7gsnD+qFh4nS29V4OPAnw+ojoAspXIjObiL9o1xh9a/nTuYHm02Rw==} + peerDependencies: + '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.0.0 || ^7.0.0 + solid-js: ^1.7.2 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + peerDependenciesMeta: + '@testing-library/jest-dom': + optional: true + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + vite: + optional: true + + vitest@3.2.7: + resolution: {integrity: sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.7 + '@vitest/ui': 3.2.7 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xstate@5.33.2: + resolution: {integrity: sha512-8tC7yXgeCvpT8gKEeEje6ikJJG1wpnoLiXe+HfECW8m10ubMd3QxKOwWA8KxPJVBcLwfdRoMKYxIBGYKmo37/A==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + +snapshots: + + '@automerge/automerge-repo-keyhive@0.0.0-alpha.71': + dependencies: + '@automerge/automerge-repo': 2.5.0-alpha.2 + '@automerge/automerge-repo-network-websocket': 2.5.0-alpha.2 + '@keyhive/keyhive': 0.0.0-alpha.40 + eventemitter3: 5.0.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-repo-network-broadcastchannel@2.5.0': + dependencies: + '@automerge/automerge-repo': 2.5.0 + transitivePeerDependencies: + - supports-color + + '@automerge/automerge-repo-network-messagechannel@2.5.0': + dependencies: + '@automerge/automerge-repo': 2.5.0 + debug: 4.4.3 + eventemitter3: 5.0.4 + transitivePeerDependencies: + - supports-color + + '@automerge/automerge-repo-network-websocket@2.5.0': + dependencies: + '@automerge/automerge-repo': 2.5.0 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + isomorphic-ws: 5.0.0(ws@8.21.3) + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-repo-network-websocket@2.5.0-alpha.2': + dependencies: + '@automerge/automerge-repo': 2.5.0-alpha.2 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + isomorphic-ws: 5.0.0(ws@8.21.3) + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-repo-storage-indexeddb@2.5.0': + dependencies: + '@automerge/automerge-repo': 2.5.0 + transitivePeerDependencies: + - supports-color + + '@automerge/automerge-repo@2.5.0': + dependencies: + '@automerge/automerge': 3.2.1 + bs58check: 3.0.1 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + fast-sha256: 1.3.0 + uuid: 9.0.1 + xstate: 5.33.2 + transitivePeerDependencies: + - supports-color + + '@automerge/automerge-repo@2.5.0-alpha.2': + dependencies: + '@automerge/automerge': 3.2.1 + bs58check: 3.0.1 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + fast-sha256: 1.3.0 + uuid: 9.0.1 + xstate: 5.33.2 + transitivePeerDependencies: + - supports-color + + '@automerge/automerge-repo@2.6.0-subduction.40': + dependencies: + '@automerge/automerge': 3.3.0-fragments.2 + '@automerge/automerge-subduction': 0.16.0 + bs58check: 4.0.0 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + fast-sha256: 1.3.0 + isomorphic-ws: 5.0.0(ws@8.21.3) + uuid: 14.0.2 + ws: 8.21.3 + xstate: 5.33.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-subduction@0.16.0': {} + + '@automerge/automerge@3.2.1': {} + + '@automerge/automerge@3.3.0-fragments.2': {} + + '@automerge/vanillajs@2.5.0': + dependencies: + '@automerge/automerge-repo': 2.5.0 + '@automerge/automerge-repo-network-broadcastchannel': 2.5.0 + '@automerge/automerge-repo-network-messagechannel': 2.5.0 + '@automerge/automerge-repo-network-websocket': 2.5.0 + '@automerge/automerge-repo-storage-indexeddb': 2.5.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.8': + dependencies: + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.29.0 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.18.6': + dependencies: + '@babel/types': 7.29.8 + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + + '@babel/parser@7.29.9': + dependencies: + '@babel/types': 7.29.8 + + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + + '@babel/traverse@7.29.8': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + optional: true + + '@chee/patchwork-bundles@0.2.0(@automerge/automerge-repo-keyhive@0.0.0-alpha.71)(@automerge/automerge@3.2.1)(@automerge/vanillajs@2.5.0)(@keyhive/keyhive@0.0.0-alpha.40)': + dependencies: + '@automerge/automerge-repo': 2.6.0-subduction.40 + '@inkandswitch/patchwork-bootloader': 0.0.3(@automerge/automerge-repo-keyhive@0.0.0-alpha.71)(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.1)(@automerge/vanillajs@2.5.0)(@keyhive/keyhive@0.0.0-alpha.40) + '@inkandswitch/patchwork-filesystem': 0.2.9(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.1) + es-module-lexer: 2.3.2 + resolve.exports: 2.0.3 + unplugin: 2.3.11 + transitivePeerDependencies: + - '@automerge/automerge' + - '@automerge/automerge-repo-keyhive' + - '@automerge/vanillajs' + - '@keyhive/keyhive' + - bufferutil + - supports-color + - utf-8-validate + + '@chee/patchwork-llm@automerge:m6dt2f4hYdhyvmFDvXYbYDdNQey@kHSoEYZn3QrxfLpLC7Lbm2j979WAsxGgNMH8wZd6jbasFTkx9(@automerge/automerge-repo@2.6.0-subduction.40)': + dependencies: + '@automerge/automerge-repo': 2.6.0-subduction.40 + '@inkandswitch/patchwork-providers': 0.3.0(@automerge/automerge-repo@2.6.0-subduction.40) + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@inkandswitch/patchwork-bootloader@0.0.3(@automerge/automerge-repo-keyhive@0.0.0-alpha.71)(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.1)(@automerge/vanillajs@2.5.0)(@keyhive/keyhive@0.0.0-alpha.40)': + dependencies: + '@automerge/automerge': 3.2.1 + '@automerge/automerge-repo': 2.6.0-subduction.40 + '@automerge/automerge-repo-keyhive': 0.0.0-alpha.71 + '@automerge/vanillajs': 2.5.0 + '@keyhive/keyhive': 0.0.0-alpha.40 + '@types/debug': 4.1.13 + debug: 4.4.3 + resolve.exports: 2.0.3 + service-worker-types: '@types/serviceworker@0.0.153' + tinyargs: 0.1.4 + transitivePeerDependencies: + - supports-color + + '@inkandswitch/patchwork-filesystem@0.0.3': + dependencies: + '@automerge/automerge': 3.2.1 + '@automerge/automerge-repo': 2.5.0 + '@types/debug': 4.1.13 + '@types/node': 20.19.43 + debug: 4.4.3 + resolve.exports: 2.0.3 + transitivePeerDependencies: + - supports-color + + '@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.1)': + dependencies: + '@automerge/automerge': 3.2.1 + '@automerge/automerge-repo': 2.6.0-subduction.40 + debug: 4.4.3 + resolve.exports: 2.0.3 + transitivePeerDependencies: + - supports-color + + '@inkandswitch/patchwork-plugins@0.0.5': + dependencies: + '@automerge/automerge': 3.2.1 + '@automerge/automerge-repo': 2.5.0 + '@inkandswitch/patchwork-filesystem': 0.0.3 + '@types/debug': 4.1.13 + '@types/node': 20.19.43 + debug: 4.4.3 + eventemitter3: 5.0.4 + resolve.exports: 2.0.3 + transitivePeerDependencies: + - supports-color + + '@inkandswitch/patchwork-providers@0.3.0(@automerge/automerge-repo@2.6.0-subduction.40)': + dependencies: + '@automerge/automerge-repo': 2.6.0-subduction.40 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@keyhive/keyhive@0.0.0-alpha.40': {} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@noble/hashes@1.8.0': {} + + '@rollup/rollup-android-arm-eabi@4.63.4': + optional: true + + '@rollup/rollup-android-arm64@4.63.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.63.4': + optional: true + + '@rollup/rollup-darwin-x64@4.63.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.63.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.63.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.63.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.63.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.63.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.63.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.63.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.63.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.63.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.63.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.63.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.63.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.63.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.63.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.63.4': + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.8 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.8 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/ms@2.1.0': {} + + '@types/node@20.19.43': + dependencies: + undici-types: 6.21.0 + + '@types/serviceworker@0.0.153': {} + + '@vitest/expect@3.2.7': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.7(vite@5.4.21(@types/node@20.19.43))': + dependencies: + '@vitest/spy': 3.2.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@20.19.43) + + '@vitest/pretty-format@3.2.7': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.7': + dependencies: + '@vitest/utils': 3.2.7 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.7': + dependencies: + tinyspy: 4.0.6 + + '@vitest/utils@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + acorn@8.18.0: {} + + assertion-error@2.0.1: {} + + babel-plugin-jsx-dom-expressions@0.40.10(@babel/core@7.29.7): + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.18.6 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/types': 7.29.8 + html-entities: 2.3.3 + parse5: 7.3.0 + + babel-preset-solid@1.9.15(@babel/core@7.29.7)(solid-js@1.9.15): + dependencies: + '@babel/core': 7.29.7 + babel-plugin-jsx-dom-expressions: 0.40.10(@babel/core@7.29.7) + optionalDependencies: + solid-js: 1.9.15 + + base-x@4.0.1: {} + + base-x@5.0.1: {} + + baseline-browser-mapping@2.11.25: {} + + browserslist@4.29.0: + dependencies: + baseline-browser-mapping: 2.11.25 + caniuse-lite: 1.0.30001810 + electron-to-chromium: 1.5.433 + node-releases: 2.0.56 + update-browserslist-db: 1.3.3(browserslist@4.29.0) + + bs58@5.0.0: + dependencies: + base-x: 4.0.1 + + bs58@6.0.0: + dependencies: + base-x: 5.0.1 + + bs58check@3.0.1: + dependencies: + '@noble/hashes': 1.8.0 + bs58: 5.0.0 + + bs58check@4.0.0: + dependencies: + '@noble/hashes': 1.8.0 + bs58: 6.0.0 + + cac@6.7.14: {} + + caniuse-lite@1.0.30001810: {} + + cbor-extract@2.2.2: + dependencies: + node-gyp-build-optional-packages: 5.1.1 + optionalDependencies: + '@cbor-extract/cbor-extract-darwin-arm64': 2.2.2 + '@cbor-extract/cbor-extract-darwin-x64': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm64': 2.2.2 + '@cbor-extract/cbor-extract-linux-x64': 2.2.2 + '@cbor-extract/cbor-extract-win32-x64': 2.2.2 + optional: true + + cbor-x@1.6.6: + optionalDependencies: + cbor-extract: 2.2.2 + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + + convert-source-map@2.0.0: {} + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + detect-libc@2.1.2: + optional: true + + electron-to-chromium@1.5.433: {} + + entities@4.5.0: {} + + entities@6.0.1: {} + + es-module-lexer@1.7.0: {} + + es-module-lexer@2.3.2: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escalade@3.2.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + eventemitter3@5.0.4: {} + + expect-type@1.4.0: {} + + fast-sha256@1.3.0: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + happy-dom@15.11.7: + dependencies: + entities: 4.5.0 + webidl-conversions: 7.0.0 + whatwg-mimetype: 3.0.0 + + html-entities@2.3.3: {} + + is-what@4.1.16: {} + + isomorphic-ws@5.0.0(ws@8.21.3): + dependencies: + ws: 8.21.3 + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + loupe@3.2.1: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + merge-anything@5.1.7: + dependencies: + is-what: 4.1.16 + + ms@2.1.3: {} + + nanoid@3.3.19: {} + + node-gyp-build-optional-packages@5.1.1: + dependencies: + detect-libc: 2.1.2 + optional: true + + node-releases@2.0.56: {} + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + postcss@8.5.28: + dependencies: + nanoid: 3.3.19 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + resolve.exports@2.0.3: {} + + rollup@4.63.4: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.63.4 + '@rollup/rollup-android-arm64': 4.63.4 + '@rollup/rollup-darwin-arm64': 4.63.4 + '@rollup/rollup-darwin-x64': 4.63.4 + '@rollup/rollup-freebsd-arm64': 4.63.4 + '@rollup/rollup-freebsd-x64': 4.63.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.63.4 + '@rollup/rollup-linux-arm-musleabihf': 4.63.4 + '@rollup/rollup-linux-arm64-gnu': 4.63.4 + '@rollup/rollup-linux-arm64-musl': 4.63.4 + '@rollup/rollup-linux-loong64-gnu': 4.63.4 + '@rollup/rollup-linux-loong64-musl': 4.63.4 + '@rollup/rollup-linux-ppc64-gnu': 4.63.4 + '@rollup/rollup-linux-ppc64-musl': 4.63.4 + '@rollup/rollup-linux-riscv64-gnu': 4.63.4 + '@rollup/rollup-linux-riscv64-musl': 4.63.4 + '@rollup/rollup-linux-s390x-gnu': 4.63.4 + '@rollup/rollup-linux-x64-gnu': 4.63.4 + '@rollup/rollup-linux-x64-musl': 4.63.4 + '@rollup/rollup-openbsd-x64': 4.63.4 + '@rollup/rollup-openharmony-arm64': 4.63.4 + '@rollup/rollup-win32-arm64-msvc': 4.63.4 + '@rollup/rollup-win32-ia32-msvc': 4.63.4 + '@rollup/rollup-win32-x64-gnu': 4.63.4 + '@rollup/rollup-win32-x64-msvc': 4.63.4 + fsevents: 2.3.3 + + semver@6.3.1: {} + + seroval-plugins@1.5.6(seroval@1.5.6): + dependencies: + seroval: 1.5.6 + + seroval@1.5.6: {} + + siginfo@2.0.0: {} + + solid-js@1.9.15: + dependencies: + csstype: 3.2.3 + seroval: 1.5.6 + seroval-plugins: 1.5.6(seroval@1.5.6) + + solid-refresh@0.6.3(solid-js@1.9.15): + dependencies: + '@babel/generator': 7.29.8 + '@babel/helper-module-imports': 7.29.7 + '@babel/types': 7.29.8 + solid-js: 1.9.15 + transitivePeerDependencies: + - supports-color + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + tinyargs@0.1.4: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.6: {} + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + unplugin@2.3.11: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.18.0 + picomatch: 4.0.7 + webpack-virtual-modules: 0.6.2 + + update-browserslist-db@1.3.3(browserslist@4.29.0): + dependencies: + browserslist: 4.29.0 + escalade: 3.2.0 + picocolors: 1.1.1 + + uuid@14.0.2: {} + + uuid@9.0.1: {} + + vite-node@3.2.4(@types/node@20.19.43): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 5.4.21(@types/node@20.19.43) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite-plugin-solid@2.11.14(solid-js@1.9.15)(vite@5.4.21(@types/node@20.19.43)): + dependencies: + '@babel/core': 7.29.7 + '@types/babel__core': 7.20.5 + babel-preset-solid: 1.9.15(@babel/core@7.29.7)(solid-js@1.9.15) + merge-anything: 5.1.7 + solid-js: 1.9.15 + solid-refresh: 0.6.3(solid-js@1.9.15) + vite: 5.4.21(@types/node@20.19.43) + vitefu: 1.1.3(vite@5.4.21(@types/node@20.19.43)) + transitivePeerDependencies: + - supports-color + + vite@5.4.21(@types/node@20.19.43): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.28 + rollup: 4.63.4 + optionalDependencies: + '@types/node': 20.19.43 + fsevents: 2.3.3 + + vitefu@1.1.3(vite@5.4.21(@types/node@20.19.43)): + optionalDependencies: + vite: 5.4.21(@types/node@20.19.43) + + vitest@3.2.7(@types/debug@4.1.13)(@types/node@20.19.43)(happy-dom@15.11.7): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.7 + '@vitest/mocker': 3.2.7(vite@5.4.21(@types/node@20.19.43)) + '@vitest/pretty-format': 3.2.7 + '@vitest/runner': 3.2.7 + '@vitest/snapshot': 3.2.7 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.4.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 5.4.21(@types/node@20.19.43) + vite-node: 3.2.4(@types/node@20.19.43) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.13 + '@types/node': 20.19.43 + happy-dom: 15.11.7 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + webidl-conversions@7.0.0: {} + + webpack-virtual-modules@0.6.2: {} + + whatwg-mimetype@3.0.0: {} + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + ws@8.21.3: {} + + xstate@5.33.2: {} + + yallist@3.1.1: {} diff --git a/llm-host/pnpm-workspace.yaml b/llm-host/pnpm-workspace.yaml new file mode 100644 index 00000000..b6a60e31 --- /dev/null +++ b/llm-host/pnpm-workspace.yaml @@ -0,0 +1,11 @@ +# pnpm 11 reads its settings from this file. There is no root workspace, so +# every package carries the settings it needs rather than inheriting them. +minimumReleaseAge: 0 +verifyDepsBeforeRun: false +allowBuilds: + "@swc/core": true + cbor-extract: true + core-js: true + esbuild: true +configDependencies: + pnpm-plugin-patchwork: "0.4.1+sha512-IzQJkoeQaSaYSa1bRko3KUJ+6nBmYg8Qxuyy0llnhPfAGSDP/+gM/Z7R0HGLtTpCE3K57CXS9DnAItqb/pNIZQ==" diff --git a/llm-host/src/LlmConfigTray.tsx b/llm-host/src/LlmConfigTray.tsx new file mode 100644 index 00000000..de861545 --- /dev/null +++ b/llm-host/src/LlmConfigTray.tsx @@ -0,0 +1,216 @@ +import { createSignal, onCleanup, Show } from "solid-js"; +import { Portal, render } from "solid-js/web"; +// @ts-ignore — plain-JS library, ships no type declarations +import { + dom as pickerDom, + ensureConfig, + readScopedConfig, + readConfig, + writeScopeOverride, + writeConfig, +} from "@chee/patchwork-llm"; + +/** + * The LLM config tray tool — a `patchwork:component` (element) => cleanup. + * + * Runs in the HOST realm (it's a system-tray icon). It owns the picker UI over + * the LLM settings doc. Chat (and anything else) opens it either by clicking the + * tray icon or by dispatching a `patchwork:open-tool` + * {detail:{component:"llm-config-tray", scope, toolPrompt, toolTools}} event + * (relayed across the isolation boundary by the isolation open-tool bridge). + * + * The picker is driven through its injectable `source` {read, write} so this tool + * is the sole owner of settings-doc writes; per-tool/per-doc scope overrides are + * written via writeScopeOverride, the global default via writeConfig. + */ + +/** @typedef {{toolId?: string, docId?: string, toolName?: string, docName?: string}} Scope */ + +export function LlmConfigTray(element: HTMLElement) { + const style = document.createElement("style"); + style.textContent = ` + .llm-config-tray { position: relative; display: inline-flex; align-items: center; } + .llm-config-tray-button { + display: inline-flex; align-items: center; gap: var(--studio-space-2xs, 0.25rem); + height: 1.75rem; padding: 0 var(--studio-space-xs, 0.375rem); + border: 1px solid var(--studio-chrome-offset-20, var(--studio-fill-offset-20, #d4d4d4)); + border-radius: var(--studio-radius-sm, 4px); + background: var(--studio-chrome, var(--studio-fill, white)); + color: var(--studio-chrome-line, var(--studio-line, black)); + font: 500 0.75rem/1 var(--studio-family-sans, system-ui, sans-serif); cursor: pointer; + } + .llm-config-tray-button:hover { + background: var(--studio-chrome-offset-10, var(--studio-fill-offset-10, #f2f2f2)); + } + .llm-config-tray-popover { + position: fixed; z-index: 2147483000; + max-width: min(92vw, 520px); max-height: 80vh; overflow: auto; + background: var(--studio-fill, white); color: var(--studio-line, black); + border: 1px solid var(--studio-fill-offset-20, #d4d4d4); + border-radius: var(--studio-radius-md, 8px); + box-shadow: var(--studio-shadow-lg, 0 8px 32px rgba(0,0,0,0.24)); + padding: var(--studio-space-sm, 0.5rem); + } + `; + element.append(style); + + // Current scope the picker edits (set when opened via event; icon click uses + // the global default scope). + const [open, setOpen] = createSignal(false); + const [scope, setScope] = createSignal({}); + const [payload, setPayload] = createSignal({}); + const [pos, setPos] = createSignal<{ left: number; top: number } | null>( + null + ); + + let buttonEl!: HTMLButtonElement; + let popoverEl: HTMLDivElement | undefined; + let pickerHost: HTMLDivElement | undefined; + let currentPicker: any = null; + + // Build the picker's source from the current scope: scoped reads/writes when a + // toolId is present, else the global default config. + function sourceFor(sc: Scope) { + if (sc && sc.toolId) { + return { + read: () => { + try { + return readScopedConfig(sc); + } catch { + return readConfig(); + } + }, + write: (next: any) => writeScopeOverride(sc, next), + }; + } + return { + read: () => readConfig(), + write: (next: any) => writeConfig(next), + }; + } + + async function mountPicker() { + const sc = scope(); + const p = payload() || {}; + await ensureConfig(sc && sc.toolId ? sc : undefined, element); + if (!pickerHost) return; + pickerHost.replaceChildren(); + currentPicker = pickerDom({ + source: sourceFor(sc), + scope: sc && sc.toolId ? sc : undefined, + toolName: sc?.toolName || p.toolName, + toolPrompt: p.toolPrompt, + toolTools: p.toolTools, + }); + pickerHost.append(currentPicker); + } + + function positionPopover() { + const r = buttonEl.getBoundingClientRect(); + // Prefer opening above the tray (tray sits at the bottom), aligned right. + setPos({ left: Math.max(8, r.right - 520), top: Math.max(8, r.top - 8) }); + } + + function openPicker(sc: Scope, p: any) { + setScope(sc || {}); + setPayload(p || {}); + positionPopover(); + setOpen(true); + queueMicrotask(mountPicker); + } + + function closePicker() { + try { + currentPicker?.destroy?.(); + } catch {} + currentPicker = null; + setOpen(false); + } + + // Open via the `patchwork:open-tool` event (from chat / other tools, incl. + // relayed across isolation). Match our component id. + const onOpenTool = (e: Event) => { + const detail = (e as CustomEvent).detail; + if (!detail || detail.component !== "llm-config-tray") return; + openPicker(detail.scope || {}, detail); + }; + document.addEventListener("patchwork:open-tool", onOpenTool as EventListener); + + // Close on outside click. Use composedPath() (captured at event time, before + // the picker re-renders its own DOM on a selection) rather than + // popoverEl.contains(target): the picker rebuilds its option list when config + // changes, so the clicked node is often already detached by the time the click + // bubbles to document — contains() would then wrongly report "outside" and + // close the popover on every model click. + const onDocClick = (e: MouseEvent) => { + if (!open()) return; + const path = e.composedPath(); + if ( + (popoverEl && path.includes(popoverEl)) || + (buttonEl && path.includes(buttonEl)) + ) { + return; // click was inside the popover or on the tray button + } + closePicker(); + }; + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape" && open()) closePicker(); + }; + document.addEventListener("click", onDocClick); + document.addEventListener("keydown", onKey); + + const dispose = render( + () => ( +
+ + + +
event.stopPropagation()} + > +
+
+ + +
+ ), + element + ); + + onCleanup(() => { + try { + currentPicker?.destroy?.(); + } catch {} + }); + + return () => { + dispose(); + style.remove(); + document.removeEventListener( + "patchwork:open-tool", + onOpenTool as EventListener + ); + document.removeEventListener("click", onDocClick); + document.removeEventListener("keydown", onKey); + }; +} diff --git a/llm-host/src/client.js b/llm-host/src/client.js new file mode 100644 index 00000000..a5fd83ce --- /dev/null +++ b/llm-host/src/client.js @@ -0,0 +1,106 @@ +/** + * The LLM consume-half — the mirror of worker-spec.js. + * + * worker-spec.js (the SERVE half) turns a consumer's request frames into worker + * traffic; this file (the CONSUME half) turns a tool's `generate(...)` call into + * those request frames and reads the event stream back. Together they are the LLM + * *protocol* layer, sitting above the generic transport (@grjte/patchwork-worker) + * and below any tool that wants to generate. + * + * How a tool gets it: NOT by import. index.ts registers `makeLLMClient` as the + * `patchwork:worker-client` plugin with id "llm", paired with the `patchwork:worker` + * plugin of the same id. A tool calls the transport's + * `connectWorkerClient("llm", …)`, which resolves this factory from the plugin + * registry, opens a session for "llm", and returns `makeLLMClient(session)`. So + * both halves of the protocol ship in this one package and cannot drift, and the + * tool has no build-time dependency on it. + * + * ⚠ Imports NOTHING — and must never import @chee/patchwork-llm or + * ./worker-spec.js. This chunk is what a sandboxed tool loads (via the plugin's + * `load()`); the library and the serve half stay host-side. + * + * Tool calls: the serve half parses `` text host-side for providers + * without native function calling, so `result.toolCalls` is populated (or null) + * for every provider and a consumer never parses model text itself. + * + * Frame vocabulary (must stay in step with worker-spec.js): + * request: {id, op:"generate", scope, system, tools, messages|text, sessionKey} + * events: {id, type:"token"|"stats"|"prediction"|"status"|"model"| + * "result"|"error", ...} result/error terminal. + */ + +/** + * @typedef {Object} GenOpts + * @property {any} [scope] {toolId, docId} — the host resolves config for this scope + * @property {string|Record} [system] string | {default, [provider]} + * @property {any[]} [tools] tool descriptors {name, description, defaultOff?, parameters?} + * @property {string} [sessionKey] + * @property {HTMLElement} [element] a node in the mounted , for discovery + * @property {AbortSignal} [signal] + * @property {(delta:string, full:string)=>void} [onToken] + * @property {(message:string)=>void} [onStatus] + * @property {(label:string)=>void} [onModel] + * + * @typedef {Object} GenResult + * @property {string} text + * @property {any[]|null} toolCalls + * @property {string} [toolMode] + * + * @typedef {{generate: (messages: any[]|string, opts?: GenOpts) => Promise}} LLMClient + */ + +/** + * The `patchwork:worker-client` factory for kind "llm": bind the LLM protocol to + * an already-open transport session and return the client API. + * + * @param {{request: (frame:any, opts:any)=>{promise:Promise}}} session + * @returns {LLMClient} + */ +export function makeLLMClient(session) { + /** + * Generate a completion. Streams tokens via opts.onToken; resolves with the + * final text + any structured tool calls. Tool EXECUTION stays in the tool — + * the worker only receives tool descriptors and returns calls to run. + * @param {any[]|string} messages + * @param {GenOpts} [opts] + * @returns {Promise} + */ + function generate(messages, opts = {}) { + const payload = typeof messages === "string" ? {text: messages} : {messages} + + const {promise} = session.request( + { + op: "generate", + sessionKey: opts.sessionKey, + scope: opts.scope, + system: opts.system, + tools: opts.tools, + ...payload, + }, + { + element: opts.element, + signal: opts.signal, + terminal: { + result: (/** @type {any} */ f) => ({ + text: f.text, + toolCalls: f.toolCalls || null, + toolMode: f.toolMode, + }), + error: (/** @type {any} */ f) => { + throw new Error(f.message) + }, + }, + // prediction / stats frames are ignored by the caller unless it asks. + onFrame: (/** @type {any} */ f) => { + if (f.type === "token") opts.onToken?.(f.delta, f.text) + else if (f.type === "status") opts.onStatus?.(f.message) + else if (f.type === "model") opts.onModel?.(f.label) + }, + } + ) + + return /** @type {Promise} */ (promise) + } + + return {generate} +} diff --git a/llm-host/src/client.test.js b/llm-host/src/client.test.js new file mode 100644 index 00000000..45fe38a6 --- /dev/null +++ b/llm-host/src/client.test.js @@ -0,0 +1,112 @@ +import {describe, it, expect, vi} from "vitest" +import {makeLLMClient} from "./client.js" + +/** + * A fake transport session: records the request frame + opts and lets the test + * drive the terminal/onFrame callbacks the way session.js would. + */ +function fakeSession() { + /** @type {any} */ + const seen = {frame: null, opts: null} + let settle = /** @type {any} */ (null) + const session = { + request(frame, opts) { + seen.frame = frame + seen.opts = opts + const promise = new Promise((resolve, reject) => { + settle = {resolve, reject} + }) + return {promise, abort: vi.fn()} + }, + } + /** Deliver a terminal frame through opts.terminal, like session.js does. */ + const terminal = (f) => { + try { + settle.resolve(seen.opts.terminal[f.type](f)) + } catch (e) { + settle.reject(e) + } + } + const frame = (f) => seen.opts.onFrame(f) + return {session, seen, terminal, frame} +} + +describe("makeLLMClient", () => { + it("returns the client API: {generate} only", () => { + const {session} = fakeSession() + const client = makeLLMClient(session) + expect(Object.keys(client)).toEqual(["generate"]) + expect(typeof client.generate).toBe("function") + }) + + it("sends a generate frame with messages, scope, system, tools and sessionKey", async () => { + const {session, seen, terminal} = fakeSession() + const client = makeLLMClient(session) + const el = {} + const signal = new AbortController().signal + const p = client.generate([{role: "user", content: "hi"}], { + sessionKey: "doc-1", + scope: {toolId: "chat"}, + system: {default: "be brief"}, + tools: [{name: "x"}], + element: /** @type {any} */ (el), + signal, + }) + expect(seen.frame).toEqual({ + op: "generate", + sessionKey: "doc-1", + scope: {toolId: "chat"}, + system: {default: "be brief"}, + tools: [{name: "x"}], + messages: [{role: "user", content: "hi"}], + }) + expect(seen.opts.element).toBe(el) + expect(seen.opts.signal).toBe(signal) + terminal({type: "result", text: "hello", toolCalls: null}) + await expect(p).resolves.toEqual({text: "hello", toolCalls: null, toolMode: undefined}) + }) + + it("sends a string prompt as text", async () => { + const {session, seen, terminal} = fakeSession() + const p = makeLLMClient(session).generate("continue this") + expect(seen.frame.text).toBe("continue this") + expect(seen.frame.messages).toBeUndefined() + terminal({type: "result", text: "…"}) + await p + }) + + it("passes structured toolCalls and toolMode through from the result frame", async () => { + const {session, terminal} = fakeSession() + const p = makeLLMClient(session).generate("x") + terminal({type: "result", text: "t", toolCalls: [{name: "x", args: {}}], toolMode: "template"}) + await expect(p).resolves.toEqual({ + text: "t", + toolCalls: [{name: "x", args: {}}], + toolMode: "template", + }) + }) + + it("rejects with the error frame's message", async () => { + const {session, terminal} = fakeSession() + const p = makeLLMClient(session).generate("x") + terminal({type: "error", message: "boom"}) + await expect(p).rejects.toThrow("boom") + }) + + it("routes token / status / model frames to the callbacks", async () => { + const {session, frame, terminal} = fakeSession() + const onToken = vi.fn() + const onStatus = vi.fn() + const onModel = vi.fn() + const p = makeLLMClient(session).generate("x", {onToken, onStatus, onModel}) + frame({type: "token", delta: "h", text: "h"}) + frame({type: "status", message: "loading"}) + frame({type: "model", label: "Local 1B"}) + frame({type: "stats", tokPerSec: 1}) // ignored + expect(onToken).toHaveBeenCalledWith("h", "h") + expect(onStatus).toHaveBeenCalledWith("loading") + expect(onModel).toHaveBeenCalledWith("Local 1B") + terminal({type: "result", text: "h"}) + await p + }) +}) diff --git a/llm-host/src/index.ts b/llm-host/src/index.ts new file mode 100644 index 00000000..700b4f0c --- /dev/null +++ b/llm-host/src/index.ts @@ -0,0 +1,81 @@ +import type { Plugin } from "@inkandswitch/patchwork-plugins"; + +/** + * @grjte/llm-host — the host-realm LLM package. + * + * `@chee/patchwork-llm` is a plain library (npm-bound, no Patchwork coupling). + * This package is the Patchwork side of it: it registers what the platform needs + * to know about, and owns everything that must run in the host realm because it + * touches the settings doc and the API key. + * + * Three plugins: + * + * `llm-config-tray` (patchwork:component) + * The system-tray icon that opens the model/config picker over the LLM + * settings doc. Consumer tools never read config — they open this tool via a + * `patchwork:open-tool` event (relayed across the isolation boundary) and + * otherwise just name a scope when they generate. + * + * `llm` (patchwork:worker) + * The LLM worker itself. The generic worker provider + * (`@grjte/patchwork-worker`) resolves it by this id when a tool calls + * `connectWorker("llm", …)`, in or out of isolation. Registering it + * declaratively is what keeps the provider kind-agnostic: it no longer + * imports the LLM, so nothing service-specific loads until someone actually + * connects. + * + * `llm` (patchwork:worker-client) + * The CONSUME half of the same protocol, paired with the worker by id. Its + * `load()` resolves to `makeLLMClient`, a factory `(session) => {generate}`. + * A tool calls `connectWorkerClient("llm", …)` from @grjte/patchwork-worker, + * which resolves this factory from the registry and binds it to a session — + * so the tool never imports this package. Both halves ship here and cannot + * drift. Under isolation the registry is mirrored into the iframe and the + * iframe loads this package's entry + the client chunk; nothing else of + * llm-host (and none of @chee/patchwork-llm) enters the sandbox. + * + * All three are behind `load()`, so importing this module registers descriptors + * only — the library (worker, config, picker, transformers.js glue) is imported + * lazily on first use rather than eagerly at frame mount. Descriptor fields must + * stay plain data: the isolation bridge structured-clones them. + */ +export const plugins: Plugin[] = [ + { + type: "patchwork:component", + id: "llm-config-tray", + name: "LLM Model & Config", + icon: "Bot", + tags: ["system-tray"], + async load() { + const { LlmConfigTray } = await import("./LlmConfigTray.js"); + return LlmConfigTray; + }, + }, + { + type: "patchwork:worker", + id: "llm", + name: "LLM", + async load() { + // Resolves to a WorkerSpec that @grjte/patchwork-worker serves. The library + // supplies the compute worker (createWorker) and the config/tool functions; + // the spec below adds the LLM op vocabulary and the settings-doc resolution + // that must run host-side (the key/settings doc never cross into a sandbox). + // @ts-ignore — plain-JS library, ships no bundled types + const lib = await import("@chee/patchwork-llm"); + // @ts-ignore — plain-JS sibling, JSDoc-typed + const { makeLLMWorkerSpec } = await import("./worker-spec.js"); + return makeLLMWorkerSpec(lib); + }, + }, + { + type: "patchwork:worker-client", + id: "llm", + name: "LLM client", + async load() { + // The consume half: pure, imports nothing, safe to load into a sandbox. + // @ts-ignore — plain-JS sibling, JSDoc-typed + const { makeLLMClient } = await import("./client.js"); + return makeLLMClient; + }, + }, +]; diff --git a/llm-host/src/worker-spec.js b/llm-host/src/worker-spec.js new file mode 100644 index 00000000..ca5d9d10 --- /dev/null +++ b/llm-host/src/worker-spec.js @@ -0,0 +1,377 @@ +/** + * The LLM WorkerSpec — the host-realm glue that turns @chee/patchwork-llm into a + * `patchwork:worker` that @grjte/patchwork-worker can serve. + * + * The generic transport (@grjte/patchwork-worker/serve.js) owns the stream pair, + * the per-connection worker lifetime, id demux, and abort plumbing. This file + * owns everything LLM-specific: resolving config/secrets from the settings doc, + * building tool schemas, the op vocabulary (generate / predict / the analytical + * ops / preload), and the model label. None of that can live in the sandbox — + * the settings doc is denylisted and the API key must never cross — which is why + * it lives HERE, in the host-realm package, rather than in the library or in the + * transport. + * + * `makeLLMWorkerSpec(lib)` takes the loaded @chee/patchwork-llm module (so this + * file has no static dependency on it — llm-host loads the library from its + * automerge doc at runtime) and returns the spec. + * + * Request preparation (prompt resolution, CallConfig, the native-vs-templated + * tools decision, the builtin detour, the input shape, the worker payload) is + * the library's `prepareGenerate` / `buildGeneratePayload` — the same code its + * own same-realm `generate()` runs — so the two paths cannot diverge. What stays + * here is host policy: which overrides a consumer may set, provider-conditional + * system maps, `toolToggles`, and parsing `` text into `toolCalls` so + * consumers (src/client.js, the consume half) never parse model output. + */ + +/** @param {...any} args */ +function clog(...args) { + try { + console.log("[llm-host]", ...args) + } catch {} +} + +/** + * Resolve a (possibly provider-conditional) system prompt. `system` may be a + * plain string (used as-is) or a map like `{default, local, openrouter, …}` — + * the entry matching the provider wins, falling back to `default`. + * @param {any} system + * @param {string} provider + */ +function resolveSystemForProvider(system, provider) { + if (system == null) return undefined + if (typeof system === "string") return system + if (typeof system === "object") return system[provider] ?? system.default ?? undefined + return undefined +} + +/** + * Filter tool descriptors by the config's `toolToggles`. A tool is included + * unless turned off (`toggles[name] === false`); a `defaultOff` tool is opt-in + * (included only if `toggles[name] === true`). Matching is by descriptor `name`. + * @param {any[]} tools + * @param {Record} toggles + */ +function filterToolsByToggles(tools, toggles) { + if (!Array.isArray(tools)) return tools + const t = toggles || {} + return tools.filter((tool) => { + if (tool && tool.defaultOff) return t[tool.name] === true + return t[tool.name] !== false + }) +} + +/** + * Build the LLM WorkerSpec. + * @param {any} lib the loaded @chee/patchwork-llm module + */ +export function makeLLMWorkerSpec(lib) { + const { + createWorker, + ensureConfig, + ensureSettingsDoc, + settingsDocHandle, + callConfig, + applyPrompts, + effectiveSystem, + describeConfig, + fetchOpenRouterModels, + builtinGenerate, + resolveCfgPrompts, + prepareGenerate, + buildGeneratePayload, + parseToolCalls, + } = lib + + // OpenRouter catalogue cache (host-side): turning a model id into a human name + // needs the catalogue. Fetch once and reuse; failure just yields the raw id. + /** @type {any[]|null} */ + let openrouterModelsCache = null + async function openrouterModels() { + if (openrouterModelsCache) return openrouterModelsCache + try { + openrouterModelsCache = await fetchOpenRouterModels() + } catch { + openrouterModelsCache = [] + } + return openrouterModelsCache + } + + /** + * Human-readable label for the resolved config. Consults the OpenRouter + * catalogue for that provider. Never includes secrets. + * @param {any} cfg + */ + async function modelLabel(cfg) { + try { + const openrouter = cfg.provider === "openrouter" ? (await openrouterModels()) || [] : [] + return describeConfig(cfg, {openrouterModels: openrouter}) + } catch { + return "the configured model" + } + } + + /** + * Resolve the unresolved config for a request frame — the DOM/repo-bound + * preamble. Config is resolved HERE from the frame's `scope`, never from a + * consumer-supplied `frame.config` (which would let a tool pick the provider + * AND supply its own key, defeating host-side resolution). + * + * @param {any} frame + * @param {Promise|null} settingsWarm the connection's in-flight + * settings-doc resolution (spec.open), awaited before reading config. + */ + async function resolveCfg0(frame, settingsWarm) { + if (settingsWarm) await settingsWarm + let cfg0 = await ensureConfig(frame.scope) + // Self-heal a cold config: `ensureConfig` resolves elementless and on its + // retryable paths silently yields DEFAULTS (local model for an OpenRouter + // account, or OpenRouter with an empty apiKey → 401). The warm normally + // settles this; this retry covers a caller whose warm timed out, and + // `ensureSettingsDoc` deliberately doesn't cache its null results. + if (!settingsDocHandle()) { + await ensureSettingsDoc() + cfg0 = await ensureConfig(frame.scope) + } + return cfg0 + } + + /** + * Only the sampling knobs a consumer legitimately owns are forwarded to + * callConfig. The frame is NOT passed wholesale: callConfig also honours + * provider / apiKey / model / url overrides, so handing it the raw frame would + * let a consumer redirect generation at its own endpoint with its own key. + * @param {any} frame + */ + function samplingOverrides(frame) { + return { + temperature: frame.temperature, + topP: frame.topP, + topk: frame.topk, + maxNewTokens: frame.maxNewTokens, + } + } + + /** + * Prepare a `generate` frame with the library's own request preparation, + * after applying host policy: the provider-conditional system map and the + * user's toolToggles. Returns what the library's generate() would have + * computed, plus `hasTools` (whether any tool survived the toggles). + * + * @param {any} frame + * @param {Promise|null} settingsWarm + */ + async function prepareFrame(frame, settingsWarm) { + const cfg0 = await resolveCfg0(frame, settingsWarm) + // The provider is fixed by the settings doc: sampling overrides never carry + // one and prompt resolution doesn't change it, so cfg0.provider is what + // callConfig will report. + const tools = filterToolsByToggles(frame.tools, cfg0.toolToggles || {}) + const prepared = await prepareGenerate(cfg0, { + messages: frame.text != null ? frame.text : frame.messages, + system: resolveSystemForProvider(frame.system, cfg0.provider), + tools, + // A continuation is a raw string prompt with no chat messages. + continuation: !!frame.continuation && typeof frame.messages === "undefined", + overrides: samplingOverrides(frame), + }) + return {...prepared, hasTools: Array.isArray(tools) && tools.length > 0} + } + + /** + * Config for the non-generate ops (predict, the analytical ops, preload): + * resolved prompts + the flat CallConfig with sampling overrides only. + * @param {any} frame + * @param {Promise|null} settingsWarm + */ + async function resolveForFrame(frame, settingsWarm) { + const cfg0 = await resolveCfg0(frame, settingsWarm) + const cfg = await resolveCfgPrompts(cfg0) + const config = callConfig(cfg, samplingOverrides(frame)) + return {cfg, config} + } + + /** + * The `toolCalls` a result frame should carry. Native function-calling + * providers return structured calls; for the rest (local / builtin) the model + * writes `` text, which is parsed HERE so every consumer sees the + * same shape and never parses model output itself. Parsing is gated on the + * request actually offering tools — prose that happens to contain a bare + * `{"name": …}` object is not a tool call. `null` when there are none. + * + * @param {string} text + * @param {any[]|null|undefined} toolCalls structured calls from the worker + * @param {boolean} hasTools + * @returns {any[]|null} + */ + function withToolCalls(text, toolCalls, hasTools) { + if (Array.isArray(toolCalls) && toolCalls.length) return toolCalls + if (!hasTools) return null + const parsed = parseToolCalls(text || "") + return parsed.length ? parsed : null + } + + return { + createWorker, + + /** + * Warm the settings doc before any frame is handled. Returned as opaque + * state; the transport bounds it with a timeout and passes it to `handle`, + * where `resolveForFrame` awaits it. `ctx.element` reaches the + * `patchwork:tool-storage` provider through the DOM; without it config + * silently falls back to DEFAULTS (the 401 case). + * @param {{element?: HTMLElement}} ctx + */ + open(ctx) { + return ctx.element ? Promise.resolve(ensureSettingsDoc(ctx.element)).catch(() => null) : null + }, + + /** + * Turn one consumer frame into worker traffic. Returns an abort token + * (`{sessionKey}`) so `op:"abort"` can cancel the matching worker request. + * + * `io` comes from @grjte/patchwork-worker's serveWorkerSpec: + * post(msg, transfer?) send to the worker + * emit(frame) enqueue onto this consumer's readable (id-tagged + * by the transport; we emit worker-shaped frames) + * on(fn) handle worker messages for this workerId; return + * true from fn when the request is complete + * workerId the transport-minted id to tag worker payloads + * state whatever open() resolved (the settings warm) + * + * @param {any} frame + * @param {any} io + */ + async handle(frame, io) { + const {post, emit, on, workerId, state: settingsWarm} = io + const op = frame.op + const sessionKey = frame.sessionKey || workerId + + if (op === "generate") { + const {cfg, config, extraSystem, builtin, input, hasTools} = await prepareFrame( + frame, + settingsWarm + ) + // Tell the consumer which model is answering (host-computed; no secrets). + void modelLabel(cfg).then((label) => emit({type: "model", label})) + + // builtin (Chrome Prompt API) runs in-realm, not in the worker. + if (builtin) { + try { + const full = await builtinGenerate(input, { + temperature: config.temperature, + topK: config.topK, + system: effectiveSystem(cfg, extraSystem), + onToken: (_d, f) => emit({type: "token", delta: "", text: f}), + onStatus: (m) => emit({type: "status", message: m}), + signal: undefined, + }) + emit({type: "result", text: full, toolCalls: withToolCalls(full, null, hasTools)}) + } catch (e) { + const err = /** @type {any} */ (e) + emit({type: "error", message: err?.message || String(e)}) + } + return {sessionKey} + } + + on((msg) => { + switch (msg.type) { + case "token": + case "prediction": + case "stats": + emit(msg) + return false + case "result": + emit({...msg, toolCalls: withToolCalls(msg.text, msg.toolCalls, hasTools)}) + return true + case "error": + clog("generate: worker error", msg.message) + emit(msg) + return true + } + return false + }) + post(buildGeneratePayload(config, input, {id: workerId, sessionKey})) + return {sessionKey} + } + + if (op === "predict") { + const {cfg, config} = await resolveForFrame(frame, settingsWarm) + config.continuation = !!frame.continuation + if (config.provider === "builtin") { + emit({type: "predictions", candidates: []}) + return {sessionKey} + } + const promptedText = applyPrompts(frame.text, cfg, frame.system) + on((msg) => { + if (msg.type === "predictions" || msg.type === "error") { + emit(msg) + return true + } + return false + }) + post({type: "predict", id: workerId, sessionKey, provider: config.provider, text: promptedText, config}) + return {sessionKey} + } + + // Analytical ops: local-only, single terminal result frame. + /** @type {Record} */ + const analytical = { + "score-tokens": ["score-progress", "token-scores"], + "compute-importance": ["importance-scores"], + "compute-attention-weights": ["attention-weights"], + "extract-features": ["features"], + "extract-cut-features": ["cut-features"], + "decode-tokens": ["decoded-tokens"], + "probe-attention": ["probe-attention-result"], + } + if (analytical[op]) { + const {config} = await resolveForFrame(frame, settingsWarm) + const [progressType, terminalType] = analytical[op] + const terminal = terminalType || progressType + on((msg) => { + if (msg.type === terminal) { + emit(msg) + return true + } + if (progressType && terminalType && msg.type === progressType) { + emit(msg) + return false + } + if (msg.type === "error") { + emit(msg) + return true + } + return false + }) + /** @type {any} */ + const payload = {type: op, id: workerId, sessionKey, provider: config.provider, config} + if (frame.text != null) payload.text = frame.text + if (frame.ids != null) payload.ids = frame.ids + post(payload) + return {sessionKey} + } + + if (op === "preload") { + const {config} = await resolveForFrame(frame, settingsWarm) + post({type: "preload", provider: config.provider, config}) + return {sessionKey} + } + + // Unknown op — nothing to do. + return {sessionKey} + }, + + /** + * Cancel one in-flight request. The worker keys `activeGenerations` by + * `sessionKey`, so that is what we post. + * @param {{sessionKey: string}} token + * @param {(m:any)=>void} post + */ + abort(token, post) { + try { + post({type: "abort", sessionKey: token.sessionKey}) + } catch {} + }, + } +} diff --git a/llm-host/src/worker-spec.test.js b/llm-host/src/worker-spec.test.js new file mode 100644 index 00000000..1d6367bb --- /dev/null +++ b/llm-host/src/worker-spec.test.js @@ -0,0 +1,143 @@ +import {describe, it, expect, vi, beforeEach} from "vitest" +import {makeLLMWorkerSpec} from "./worker-spec.js" + +/** + * A fake @chee/patchwork-llm: just enough for the spec's host policy to be + * observable. `prepareGenerate` echoes what it was given so the test can assert + * on the overrides / system / tools the spec chose to forward. + */ +function fakeLib(cfg = {provider: "local", toolToggles: {}}) { + const lib = { + createWorker: () => ({}), + ensureConfig: vi.fn(async () => cfg), + ensureSettingsDoc: vi.fn(async () => ({})), + settingsDocHandle: () => ({}), + callConfig: (c, o) => ({provider: c.provider, ...o}), + applyPrompts: (input) => input, + effectiveSystem: () => "", + describeConfig: () => "Model", + fetchOpenRouterModels: async () => [], + builtinGenerate: vi.fn(async () => "built-in text"), + resolveCfgPrompts: async (c) => c, + prepareGenerate: vi.fn(async (cfg0, opts) => ({ + cfg: cfg0, + config: {provider: cfg0.provider, ...opts.overrides}, + extraSystem: opts.system, + builtin: cfg0.provider === "builtin", + input: opts.messages, + })), + buildGeneratePayload: (config, input, ids) => ({type: "generate", ...ids, provider: config.provider, config, messages: input}), + parseToolCalls: vi.fn((text) => (text.includes("") ? [{name: "x", args: {}}] : [])), + } + return lib +} + +function fakeIO() { + /** @type {any} */ + const io = { + post: vi.fn(), + emit: vi.fn(), + handler: null, + on: (fn) => { + io.handler = fn + }, + workerId: "w1", + state: null, + } + return io +} + +const flush = () => new Promise((r) => setTimeout(r, 0)) + +describe("makeLLMWorkerSpec · generate", () => { + let lib, spec, io + beforeEach(() => { + lib = fakeLib() + spec = makeLLMWorkerSpec(lib) + io = fakeIO() + }) + + it("parses text host-side when the request offered tools", async () => { + await spec.handle({op: "generate", messages: [{role: "user", content: "hi"}], tools: [{name: "x"}]}, io) + io.handler({type: "result", text: "{}", toolCalls: null, toolMode: "template"}) + const result = io.emit.mock.calls.map((c) => c[0]).find((f) => f.type === "result") + expect(result.toolCalls).toEqual([{name: "x", args: {}}]) + expect(result.toolMode).toBe("template") + expect(lib.parseToolCalls).toHaveBeenCalledOnce() + }) + + it("passes native toolCalls through untouched", async () => { + await spec.handle({op: "generate", messages: [], tools: [{name: "x"}]}, io) + const native = [{name: "x", args: {a: 1}}] + io.handler({type: "result", text: "", toolCalls: native}) + const result = io.emit.mock.calls.map((c) => c[0]).find((f) => f.type === "result") + expect(result.toolCalls).toBe(native) + expect(lib.parseToolCalls).not.toHaveBeenCalled() + }) + + it("does not parse prose when no tools were offered", async () => { + await spec.handle({op: "generate", messages: []}, io) + io.handler({type: "result", text: '{"name":"x"}', toolCalls: null}) + const result = io.emit.mock.calls.map((c) => c[0]).find((f) => f.type === "result") + expect(result.toolCalls).toBeNull() + expect(lib.parseToolCalls).not.toHaveBeenCalled() + }) + + it("forwards only sampling overrides, never provider / apiKey / model / url", async () => { + await spec.handle( + { + op: "generate", + messages: [], + temperature: 0.2, + topP: 0.9, + provider: "openrouter", + apiKey: "sk-evil", + model: "gpt", + url: "https://attacker", + config: {apiKey: "sk-evil"}, + }, + io + ) + const [, opts] = lib.prepareGenerate.mock.calls[0] + expect(opts.overrides).toEqual({temperature: 0.2, topP: 0.9, topk: undefined, maxNewTokens: undefined}) + expect(Object.keys(opts.overrides)).not.toContain("provider") + expect(Object.keys(opts.overrides)).not.toContain("apiKey") + }) + + it("applies the provider-conditional system map and toolToggles before preparing", async () => { + lib = fakeLib({provider: "local", toolToggles: {off: false, optin: true}}) + spec = makeLLMWorkerSpec(lib) + await spec.handle( + { + op: "generate", + messages: [], + system: {default: "d", local: "L"}, + tools: [{name: "on"}, {name: "off"}, {name: "optin", defaultOff: true}, {name: "hidden", defaultOff: true}], + }, + io + ) + const [, opts] = lib.prepareGenerate.mock.calls[0] + expect(opts.system).toBe("L") + expect(opts.tools.map((t) => t.name)).toEqual(["on", "optin"]) + }) + + it("posts the library-built payload tagged with the transport's workerId", async () => { + const token = await spec.handle({op: "generate", messages: [{role: "user", content: "hi"}], sessionKey: "s1"}, io) + expect(token).toEqual({sessionKey: "s1"}) + expect(io.post).toHaveBeenCalledWith( + expect.objectContaining({type: "generate", id: "w1", sessionKey: "s1", messages: [{role: "user", content: "hi"}]}) + ) + }) + + it("runs builtin in-realm and still fills toolCalls", async () => { + lib = fakeLib({provider: "builtin", toolToggles: {}}) + lib.builtinGenerate = vi.fn(async () => "{}") + spec = makeLLMWorkerSpec(lib) + await spec.handle({op: "generate", messages: [], tools: [{name: "x"}]}, io) + await flush() + expect(io.post).not.toHaveBeenCalled() + const result = io.emit.mock.calls.map((c) => c[0]).find((f) => f.type === "result") + expect(result.text).toBe("{}") + expect(result.toolCalls).toEqual([{name: "x", args: {}}]) + }) +}) diff --git a/llm-host/tsconfig.json b/llm-host/tsconfig.json new file mode 100644 index 00000000..4696f12f --- /dev/null +++ b/llm-host/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ESNEXT", + "module": "ESNEXT", + "moduleResolution": "bundler", + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowJs": true, + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src"] +} diff --git a/llm-host/vite.config.ts b/llm-host/vite.config.ts new file mode 100644 index 00000000..886e68f2 --- /dev/null +++ b/llm-host/vite.config.ts @@ -0,0 +1,26 @@ +import {defineConfig} from "vite" +import solidPlugin from "vite-plugin-solid" +import patchworkBundles from "@chee/patchwork-bundles/vite" +import external from "@inkandswitch/patchwork-bootloader/externals" + +export default defineConfig({ + base: "./", + // Rewrite the automerge:-versioned @chee/patchwork-llm dep to a shared + // service-worker URL resolved at runtime (cross-origin), so the LLM lib loads + // as one canonical copy. This tool runs HOST-only, so loading patchwork-llm + // here is fine. (The tray injects its own