From 5af6f4292de8571d55f285daa6faa50accdb0cc2 Mon Sep 17 00:00:00 2001 From: Tyler-RNG Date: Thu, 23 Apr 2026 08:55:00 -0400 Subject: [PATCH 01/10] Rename package to @tyler-rng/sprite-core; add CI and release workflows --- .github/workflows/ci.yml | 29 +++++++++++++++++++++++++++++ .github/workflows/release.yml | 33 +++++++++++++++++++++++++++++++++ package.json | 4 ++-- 3 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6e47268 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + smoke: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Validate package.json + run: node -e "require('./package.json')" + - name: Validate openclaw.plugin.json + run: node -e "require('./openclaw.plugin.json')" + - name: Verify name matches install.npmSpec + run: | + node -e " + const pkg = require('./package.json'); + const spec = pkg.openclaw?.install?.npmSpec; + if (spec !== pkg.name) { + console.error('Mismatch: name=' + pkg.name + ' vs install.npmSpec=' + spec); + process.exit(1); + } + " diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c594a28 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,33 @@ +name: Release + +on: + push: + tags: + - 'v*' + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + registry-url: 'https://registry.npmjs.org' + + - name: Verify tag matches package version + run: | + TAG_VERSION="${GITHUB_REF_NAME#v}" + PKG_VERSION=$(node -p "require('./package.json').version") + if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then + echo "Tag $GITHUB_REF_NAME (version $TAG_VERSION) does not match package.json version $PKG_VERSION" + exit 1 + fi + + - name: Publish to npm + run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/package.json b/package.json index 81cdb20..0056c9e 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@openclaw/sprite-core", + "name": "@tyler-rng/sprite-core", "version": "2026.4.15-beta.1", "description": "OpenClaw SpriteCore plugin — in-gateway asset + TTS + STT data plane for multi-state avatars", "type": "module", @@ -25,7 +25,7 @@ "./index.ts" ], "install": { - "npmSpec": "@openclaw/sprite-core", + "npmSpec": "@tyler-rng/sprite-core", "defaultChoice": "npm", "minHostVersion": ">=2026.4.10" }, From 05779397f87e5f57643b25e8ce50da17d67d896a Mon Sep 17 00:00:00 2001 From: Tyler-RNG Date: Thu, 23 Apr 2026 09:00:45 -0400 Subject: [PATCH 02/10] Point release workflow at GitHub Packages; add private-beta install guide --- .github/workflows/release.yml | 11 +++++---- README.md | 46 +++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c594a28..533b303 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,13 +10,14 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - id-token: write + packages: write steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 - registry-url: 'https://registry.npmjs.org' + registry-url: 'https://npm.pkg.github.com' + scope: '@tyler-rng' - name: Verify tag matches package version run: | @@ -27,7 +28,7 @@ jobs: exit 1 fi - - name: Publish to npm - run: npm publish --access public + - name: Publish to GitHub Packages + run: npm publish env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 7c257c1..a95c17c 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,52 @@ workspace-relative image path, an http(s) URL, a data URI, or a short string / emoji. Anything richer (atlas, multiple states, prompting vocabulary, voice selection) lives in this plugin's config block. +## Install (private beta) + +This plugin is currently private. Installing it requires a GitHub Personal +Access Token and a one-time npm config. You must be a collaborator on the +`Tyler-RNG/sprite-core` GitHub repo. + +**1. Create a GitHub Personal Access Token.** Go to + and generate a classic token with +the `read:packages` scope (that's the only scope you need). Copy the token +(it starts with `ghp_`). + +**2. Point the `@tyler-rng` npm scope at GitHub Packages.** Add these two +lines to your `~/.npmrc` (create it if it doesn't exist): + +``` +@tyler-rng:registry=https://npm.pkg.github.com +//npm.pkg.github.com/:_authToken=ghp_YOUR_TOKEN_HERE +``` + +Replace `ghp_YOUR_TOKEN_HERE` with the token from step 1. Then +`chmod 600 ~/.npmrc` so other users on the machine can't read your token. + +**3. Install with the normal openclaw command.** + +```bash +openclaw plugin install @tyler-rng/sprite-core +``` + +OpenClaw resolves the `@tyler-rng` scope against GitHub Packages using your +token, downloads the tarball, and extracts it into your plugin directory. +Updates later: `openclaw plugin update @tyler-rng/sprite-core`. + +**4. Enable and configure it.** See [Enable](#enable) below for the +`openclaw.json` config block to paste in, then restart your gateway. + +**Troubleshooting:** + +- `401 Unauthorized` — your token is wrong, expired, or missing the + `read:packages` scope. Regenerate it. +- `404 Not Found` — either you're not a collaborator on + `Tyler-RNG/sprite-core`, or your `~/.npmrc` doesn't have the + `@tyler-rng:registry=...` line pointing at `npm.pkg.github.com`. +- Plugin installs but doesn't load — confirm + `plugins.entries["sprite-core"].enabled: true` is in your `openclaw.json` + and restart the gateway. + ## Enable ```jsonc From 9147aedba27ad618da99c2b390812f07d9b27c23 Mon Sep 17 00:00:00 2001 From: Tyler-RNG Date: Thu, 23 Apr 2026 09:03:48 -0400 Subject: [PATCH 03/10] Start plugin at 1.0.0 (independent of openclaw versioning) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0056c9e..1ef2b5a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tyler-rng/sprite-core", - "version": "2026.4.15-beta.1", + "version": "1.0.0", "description": "OpenClaw SpriteCore plugin — in-gateway asset + TTS + STT data plane for multi-state avatars", "type": "module", "repository": { From 9ae9e0f1e88fe2c4f9759e9f445a9910c3f42374 Mon Sep 17 00:00:00 2001 From: Tyler-RNG Date: Thu, 23 Apr 2026 15:10:24 -0400 Subject: [PATCH 04/10] Restructure into pnpm workspace with cross-language client SDKs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves the plugin into packages/plugin/ and seeds three client kits plus a shared schema + fixture suite so the server plugin and every client language can stay in lockstep: schema/ TypeBox wire schema + marker grammar (source of truth) fixtures/ language-agnostic conformance JSON packages/plugin/ existing @tyler-rng/sprite-core (unchanged behaviour) packages/client-js/ TypeScript reference renderer (@tyler-rng/sprite-core-client) packages/client-kotlin/ Kotlin core + android modules (ai.openclaw.spritecore:sprite-core-client[-android]) packages/client-swift/ SwiftPM package (SpriteCoreClient) The Kotlin kit is lifted from openclaw-src/apps/shared/OpenClawDisplayKit (+ OpenClawDisplayKitAndroid) with the missing \`emotions\` field added to CharacterManifest and the marker parser pulled in from the phone-app sources. AgentAvatarSource rewritten to pure JVM (no android.util.Log / org.json dep), with a pluggable logger callback. Marker parser (TS/Kotlin/Swift) now uniformly carries play-count — the TS reference implementation previously lagged Kotlin on \`<<>>\`; all three now match the existing model-facing prompt. Swift kit is a fresh hand-written port (CharacterManifest Codable, AnimationGraph, actor-based SpriteAnimationPlayer with AsyncStream, marker parser) matching Kotlin semantics 1:1. CI split into plugin-smoke (validates packages/plugin/ metadata) and workspace-smoke (validates root + client-js + schema package.json and every fixture JSON). Release workflow retargeted at packages/plugin/; client publish jobs deferred until the build conversation settles. Plugin publish path (@tyler-rng/sprite-core from GitHub Packages) unchanged — only the source directory moved. --- .github/workflows/ci.yml | 27 +- .github/workflows/release.yml | 17 +- .gitignore | 11 + README.md | 462 +++--------------- fixtures/README.md | 118 +++++ .../animation-graph/wildcard-transitions.json | 65 +++ fixtures/manifest/minimal-headshot.json | 26 + fixtures/marker/bare-markers.json | 33 ++ fixtures/marker/invalid-shapes.json | 24 + fixtures/marker/play-count-markers.json | 36 ++ fixtures/marker/split-across-chunks.json | 24 + fixtures/sprite-player/phased-intro.json | 51 ++ fixtures/sprite-player/ping-pong.json | 39 ++ fixtures/sprite-player/play-count.json | 41 ++ package.json | 43 +- packages/client-js/README.md | 66 +++ packages/client-js/package.json | 44 ++ .../client-js/src/animation-graph.test.ts | 87 ++++ packages/client-js/src/animation-graph.ts | 101 ++++ packages/client-js/src/asset-source.ts | 192 ++++++++ packages/client-js/src/frame-source.ts | 47 ++ packages/client-js/src/index.ts | 7 + packages/client-js/src/marker.test.ts | 91 ++++ packages/client-js/src/observable.ts | 40 ++ packages/client-js/src/schema.ts | 3 + packages/client-js/src/sprite-player.test.ts | 91 ++++ packages/client-js/src/sprite-player.ts | 260 ++++++++++ packages/client-js/src/ticker.ts | 14 + packages/client-js/tsconfig.json | 19 + packages/client-js/vitest.config.ts | 8 + packages/client-kotlin/README.md | 80 +++ .../client-kotlin/android/build.gradle.kts | 70 +++ .../android/src/main/AndroidManifest.xml | 2 + .../client/android/BitmapFrameSource.kt | 76 +++ packages/client-kotlin/build.gradle.kts | 8 + packages/client-kotlin/core/build.gradle.kts | 54 ++ .../spritecore/client/AgentAvatarSource.kt | 172 +++++++ .../spritecore/client/AnimationGraph.kt | 100 ++++ .../spritecore/client/AvatarMarkerParser.kt | 227 +++++++++ .../spritecore/client/CharacterManifest.kt | 217 ++++++++ .../openclaw/spritecore/client/FrameSource.kt | 43 ++ .../client/SpriteAnimationPlayer.kt | 220 +++++++++ .../ai/openclaw/spritecore/client/Ticker.kt | 29 ++ .../spritecore/client/AnimationGraphTest.kt | 118 +++++ .../client/AvatarMarkerParserTest.kt | 195 ++++++++ .../spritecore/client/ManifestParseTest.kt | 143 ++++++ .../client/SpriteAnimationPlayerTest.kt | 235 +++++++++ .../spritecore/client/SystemTickerTest.kt | 39 ++ packages/client-kotlin/gradle.properties | 5 + packages/client-kotlin/settings.gradle.kts | 20 + packages/client-swift/Package.swift | 30 ++ packages/client-swift/README.md | 65 +++ .../SpriteCoreClient/AnimationGraph.swift | 88 ++++ .../SpriteCoreClient/AvatarMarkerParser.swift | 185 +++++++ .../SpriteCoreClient/CharacterManifest.swift | 255 ++++++++++ .../SpriteCoreClient/FrameSource.swift | 50 ++ .../SpriteAnimationPlayer.swift | 218 +++++++++ .../Sources/SpriteCoreClient/Ticker.swift | 14 + .../AvatarMarkerParserTests.swift | 53 ++ .../ManifestParseTests.swift | 65 +++ packages/plugin/README.md | 436 +++++++++++++++++ index.ts => packages/plugin/index.ts | 0 .../plugin/openclaw.plugin.json | 0 packages/plugin/package.json | 39 ++ .../plugin/scripts}/pixellab-animate.mjs | 0 .../plugin/scripts}/pixellab-create.mjs | 0 .../plugin/scripts}/pixellab-export.mjs | 0 .../plugin/src}/agents-route.test.ts | 0 {src => packages/plugin/src}/agents-route.ts | 0 .../plugin/src}/assets-route.test.ts | 0 {src => packages/plugin/src}/assets-route.ts | 0 .../plugin/src}/character-manifest.test.ts | 0 .../plugin/src}/character-manifest.ts | 0 {src => packages/plugin/src}/http-helpers.ts | 0 .../plugin/src}/prompting.test.ts | 0 {src => packages/plugin/src}/prompting.ts | 0 .../plugin/src}/provider-auth.test.ts | 0 {src => packages/plugin/src}/provider-auth.ts | 0 .../plugin/src}/stt-route.test.ts | 0 {src => packages/plugin/src}/stt-route.ts | 0 .../plugin/src}/tts-route.test.ts | 0 {src => packages/plugin/src}/tts-route.ts | 0 {src => packages/plugin/src}/types.ts | 0 .../plugin/template}/agent/README.md | 0 .../plugin/template}/agent/agent.atlas.json | 0 .../plugin/template}/agent/agent.atlas.webp | Bin .../agent/regenerate-placeholder-atlas.mjs | 0 packages/plugin/tsconfig.json | 25 + pnpm-workspace.yaml | 4 + schema/README.md | 32 ++ schema/package.json | 23 + schema/src/display.ts | 215 ++++++++ schema/src/index.ts | 2 + schema/src/marker.ts | 215 ++++++++ schema/tsconfig.json | 19 + tsconfig.json | 16 - 96 files changed, 5733 insertions(+), 456 deletions(-) create mode 100644 fixtures/README.md create mode 100644 fixtures/animation-graph/wildcard-transitions.json create mode 100644 fixtures/manifest/minimal-headshot.json create mode 100644 fixtures/marker/bare-markers.json create mode 100644 fixtures/marker/invalid-shapes.json create mode 100644 fixtures/marker/play-count-markers.json create mode 100644 fixtures/marker/split-across-chunks.json create mode 100644 fixtures/sprite-player/phased-intro.json create mode 100644 fixtures/sprite-player/ping-pong.json create mode 100644 fixtures/sprite-player/play-count.json create mode 100644 packages/client-js/README.md create mode 100644 packages/client-js/package.json create mode 100644 packages/client-js/src/animation-graph.test.ts create mode 100644 packages/client-js/src/animation-graph.ts create mode 100644 packages/client-js/src/asset-source.ts create mode 100644 packages/client-js/src/frame-source.ts create mode 100644 packages/client-js/src/index.ts create mode 100644 packages/client-js/src/marker.test.ts create mode 100644 packages/client-js/src/observable.ts create mode 100644 packages/client-js/src/schema.ts create mode 100644 packages/client-js/src/sprite-player.test.ts create mode 100644 packages/client-js/src/sprite-player.ts create mode 100644 packages/client-js/src/ticker.ts create mode 100644 packages/client-js/tsconfig.json create mode 100644 packages/client-js/vitest.config.ts create mode 100644 packages/client-kotlin/README.md create mode 100644 packages/client-kotlin/android/build.gradle.kts create mode 100644 packages/client-kotlin/android/src/main/AndroidManifest.xml create mode 100644 packages/client-kotlin/android/src/main/kotlin/ai/openclaw/spritecore/client/android/BitmapFrameSource.kt create mode 100644 packages/client-kotlin/build.gradle.kts create mode 100644 packages/client-kotlin/core/build.gradle.kts create mode 100644 packages/client-kotlin/core/src/main/kotlin/ai/openclaw/spritecore/client/AgentAvatarSource.kt create mode 100644 packages/client-kotlin/core/src/main/kotlin/ai/openclaw/spritecore/client/AnimationGraph.kt create mode 100644 packages/client-kotlin/core/src/main/kotlin/ai/openclaw/spritecore/client/AvatarMarkerParser.kt create mode 100644 packages/client-kotlin/core/src/main/kotlin/ai/openclaw/spritecore/client/CharacterManifest.kt create mode 100644 packages/client-kotlin/core/src/main/kotlin/ai/openclaw/spritecore/client/FrameSource.kt create mode 100644 packages/client-kotlin/core/src/main/kotlin/ai/openclaw/spritecore/client/SpriteAnimationPlayer.kt create mode 100644 packages/client-kotlin/core/src/main/kotlin/ai/openclaw/spritecore/client/Ticker.kt create mode 100644 packages/client-kotlin/core/src/test/kotlin/ai/openclaw/spritecore/client/AnimationGraphTest.kt create mode 100644 packages/client-kotlin/core/src/test/kotlin/ai/openclaw/spritecore/client/AvatarMarkerParserTest.kt create mode 100644 packages/client-kotlin/core/src/test/kotlin/ai/openclaw/spritecore/client/ManifestParseTest.kt create mode 100644 packages/client-kotlin/core/src/test/kotlin/ai/openclaw/spritecore/client/SpriteAnimationPlayerTest.kt create mode 100644 packages/client-kotlin/core/src/test/kotlin/ai/openclaw/spritecore/client/SystemTickerTest.kt create mode 100644 packages/client-kotlin/gradle.properties create mode 100644 packages/client-kotlin/settings.gradle.kts create mode 100644 packages/client-swift/Package.swift create mode 100644 packages/client-swift/README.md create mode 100644 packages/client-swift/Sources/SpriteCoreClient/AnimationGraph.swift create mode 100644 packages/client-swift/Sources/SpriteCoreClient/AvatarMarkerParser.swift create mode 100644 packages/client-swift/Sources/SpriteCoreClient/CharacterManifest.swift create mode 100644 packages/client-swift/Sources/SpriteCoreClient/FrameSource.swift create mode 100644 packages/client-swift/Sources/SpriteCoreClient/SpriteAnimationPlayer.swift create mode 100644 packages/client-swift/Sources/SpriteCoreClient/Ticker.swift create mode 100644 packages/client-swift/Tests/SpriteCoreClientTests/AvatarMarkerParserTests.swift create mode 100644 packages/client-swift/Tests/SpriteCoreClientTests/ManifestParseTests.swift create mode 100644 packages/plugin/README.md rename index.ts => packages/plugin/index.ts (100%) rename openclaw.plugin.json => packages/plugin/openclaw.plugin.json (100%) create mode 100644 packages/plugin/package.json rename {scripts => packages/plugin/scripts}/pixellab-animate.mjs (100%) rename {scripts => packages/plugin/scripts}/pixellab-create.mjs (100%) rename {scripts => packages/plugin/scripts}/pixellab-export.mjs (100%) rename {src => packages/plugin/src}/agents-route.test.ts (100%) rename {src => packages/plugin/src}/agents-route.ts (100%) rename {src => packages/plugin/src}/assets-route.test.ts (100%) rename {src => packages/plugin/src}/assets-route.ts (100%) rename {src => packages/plugin/src}/character-manifest.test.ts (100%) rename {src => packages/plugin/src}/character-manifest.ts (100%) rename {src => packages/plugin/src}/http-helpers.ts (100%) rename {src => packages/plugin/src}/prompting.test.ts (100%) rename {src => packages/plugin/src}/prompting.ts (100%) rename {src => packages/plugin/src}/provider-auth.test.ts (100%) rename {src => packages/plugin/src}/provider-auth.ts (100%) rename {src => packages/plugin/src}/stt-route.test.ts (100%) rename {src => packages/plugin/src}/stt-route.ts (100%) rename {src => packages/plugin/src}/tts-route.test.ts (100%) rename {src => packages/plugin/src}/tts-route.ts (100%) rename {src => packages/plugin/src}/types.ts (100%) rename {template => packages/plugin/template}/agent/README.md (100%) rename {template => packages/plugin/template}/agent/agent.atlas.json (100%) rename {template => packages/plugin/template}/agent/agent.atlas.webp (100%) rename {template => packages/plugin/template}/agent/regenerate-placeholder-atlas.mjs (100%) create mode 100644 packages/plugin/tsconfig.json create mode 100644 pnpm-workspace.yaml create mode 100644 schema/README.md create mode 100644 schema/package.json create mode 100644 schema/src/display.ts create mode 100644 schema/src/index.ts create mode 100644 schema/src/marker.ts create mode 100644 schema/tsconfig.json delete mode 100644 tsconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e47268..d7a87b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,8 +6,11 @@ on: pull_request: jobs: - smoke: + plugin-smoke: runs-on: ubuntu-latest + defaults: + run: + working-directory: packages/plugin steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -27,3 +30,25 @@ jobs: process.exit(1); } " + + # Client SDKs — deeper build/test wiring lands in a follow-up once the + # build conversation settles (pnpm install deps, Gradle wrapper, swift test + # container). For now, just validate JSON shape on every push. + workspace-smoke: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Validate workspace root package.json + run: node -e "require('./package.json')" + - name: Validate client-js package.json + run: node -e "require('./packages/client-js/package.json')" + - name: Validate schema package.json + run: node -e "require('./schema/package.json')" + - name: Validate fixture JSON files + run: | + find fixtures -name '*.json' | while read f; do + node -e "JSON.parse(require('fs').readFileSync('$f', 'utf8'))" || exit 1 + done diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 533b303..061282b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,12 +5,21 @@ on: tags: - 'v*' +# TODO (build conversation): the client SDKs (client-js, client-kotlin, +# client-swift) need their own publish paths — Maven Central / Maven GitHub +# Packages for Kotlin, tagged Git for SwiftPM, and a separate npm publish for +# client-js. Today this workflow only publishes the plugin to GitHub Packages +# from packages/plugin/. See the repo-root README for the plan. + jobs: - publish: + publish-plugin: runs-on: ubuntu-latest permissions: contents: read packages: write + defaults: + run: + working-directory: packages/plugin steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -19,16 +28,16 @@ jobs: registry-url: 'https://npm.pkg.github.com' scope: '@tyler-rng' - - name: Verify tag matches package version + - name: Verify tag matches plugin package version run: | TAG_VERSION="${GITHUB_REF_NAME#v}" PKG_VERSION=$(node -p "require('./package.json').version") if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then - echo "Tag $GITHUB_REF_NAME (version $TAG_VERSION) does not match package.json version $PKG_VERSION" + echo "Tag $GITHUB_REF_NAME (version $TAG_VERSION) does not match plugin package.json version $PKG_VERSION" exit 1 fi - - name: Publish to GitHub Packages + - name: Publish plugin to GitHub Packages run: npm publish env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index cfa3bac..db8ebce 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,14 @@ dist/ .env.local coverage/ .turbo/ + +# Kotlin / Gradle +packages/client-kotlin/**/build/ +packages/client-kotlin/**/.gradle/ +packages/client-kotlin/.gradle/ +packages/client-kotlin/local.properties + +# Swift / SwiftPM +packages/client-swift/.build/ +packages/client-swift/.swiftpm/ +packages/client-swift/Package.resolved diff --git a/README.md b/README.md index a95c17c..831925f 100644 --- a/README.md +++ b/README.md @@ -1,436 +1,86 @@ # SpriteCore -OpenClaw plugin that owns the data plane for multi-state sprite avatars and -voice/TTS. Once enabled, SpriteCore is the single source of truth for: +Plugin + cross-language client SDKs for multi-state sprite avatars, streaming +TTS, and streaming STT on [OpenClaw](https://github.com/openclaw/openclaw). -- per-agent avatar config (atlas image + manifest) -- per-agent voice descriptor (provider + voiceId for the watch / phone) -- the prompt block that teaches the model which avatar states exist (so it - knows when to emit `<<>>`, `<<>>`, etc., optionally with a - `-N` play-count suffix like `<<>>` or `<<>>`) -- HTTP asset serving (`/openclaw-assets/*`) -- streaming TTS proxy (`/stream/tts`) -- streaming STT proxy (`/stream/stt`) -- the gateway RPC `node.getCharacterManifest` that ships the watch a - ready-to-render manifest +This repo holds four publishable artifacts, sharing one version and one +wire-schema source of truth so the server plugin and every client language +stay in lockstep: -The agent's `identity.avatar` field in `openclaw.json` stays narrow: a -workspace-relative image path, an http(s) URL, a data URI, or a short string / -emoji. Anything richer (atlas, multiple states, prompting vocabulary, voice -selection) lives in this plugin's config block. +| Package | Language | Artifact | Purpose | +|---|---|---|---| +| [`packages/plugin`](./packages/plugin) | TypeScript (Node) | `@tyler-rng/sprite-core` (npm) | OpenClaw gateway plugin — asset serving, TTS/STT proxy, prompt block, `node.getCharacterManifest` RPC | +| [`packages/client-js`](./packages/client-js) | TypeScript | `@tyler-rng/sprite-core-client` (npm) | Browser / Node reference implementation of the render engine | +| [`packages/client-kotlin`](./packages/client-kotlin) | Kotlin (JVM + Android) | `ai.openclaw.spritecore:sprite-core-client` (Maven) | Kotlin kit for wearables, phones, desktop | +| [`packages/client-swift`](./packages/client-swift) | Swift | `SpriteCoreClient` (SwiftPM) | iOS / macOS / tvOS / watchOS kit | -## Install (private beta) +The canonical wire schema lives in [`schema/`](./schema) (TypeBox). Kotlin and +Swift type files are generated from it so they cannot drift. Runtime behaviour +is locked down by the shared [`fixtures/`](./fixtures) suite — every language's +test harness replays the same JSON cases and must produce byte-identical +outputs. -This plugin is currently private. Installing it requires a GitHub Personal -Access Token and a one-time npm config. You must be a collaborator on the -`Tyler-RNG/sprite-core` GitHub repo. - -**1. Create a GitHub Personal Access Token.** Go to - and generate a classic token with -the `read:packages` scope (that's the only scope you need). Copy the token -(it starts with `ghp_`). - -**2. Point the `@tyler-rng` npm scope at GitHub Packages.** Add these two -lines to your `~/.npmrc` (create it if it doesn't exist): +## Layout ``` -@tyler-rng:registry=https://npm.pkg.github.com -//npm.pkg.github.com/:_authToken=ghp_YOUR_TOKEN_HERE +sprite-core/ +├── pnpm-workspace.yaml +├── package.json ← workspace root +├── schema/ ← TypeBox wire schema (source of truth) +├── fixtures/ ← language-agnostic conformance JSON +├── scripts/ ← (TODO) codegen TS → Kotlin / Swift +├── docs/ ← shared avatar/TTS/STT protocol docs +└── packages/ + ├── plugin/ ← OpenClaw plugin (was the root of this repo) + ├── client-js/ ← TypeScript reference renderer + ├── client-kotlin/ ← Kotlin kit (core + android modules) + └── client-swift/ ← Swift kit (SwiftPM) ``` -Replace `ghp_YOUR_TOKEN_HERE` with the token from step 1. Then -`chmod 600 ~/.npmrc` so other users on the machine can't read your token. - -**3. Install with the normal openclaw command.** +## Quickstart ```bash -openclaw plugin install @tyler-rng/sprite-core +pnpm install +pnpm test # runs schema + TS client + plugin tests +./gradlew -p packages/client-kotlin test +swift test --package-path packages/client-swift ``` -OpenClaw resolves the `@tyler-rng` scope against GitHub Packages using your -token, downloads the tarball, and extracts it into your plugin directory. -Updates later: `openclaw plugin update @tyler-rng/sprite-core`. +Each package has its own README with language-specific install + usage. The +**`<<>>` / `<<>>` marker grammar** and the +**`CharacterManifest` wire shape** are defined once in `schema/` and mirrored +everywhere else. -**4. Enable and configure it.** See [Enable](#enable) below for the -`openclaw.json` config block to paste in, then restart your gateway. +## Versioning -**Troubleshooting:** +All four packages release together at one version. A bump to `schema/` +invalidates conformance and requires a release across all four. See +[`CHANGELOG.md`](./CHANGELOG.md) *(to be added)*. -- `401 Unauthorized` — your token is wrong, expired, or missing the - `read:packages` scope. Regenerate it. -- `404 Not Found` — either you're not a collaborator on - `Tyler-RNG/sprite-core`, or your `~/.npmrc` doesn't have the - `@tyler-rng:registry=...` line pointing at `npm.pkg.github.com`. -- Plugin installs but doesn't load — confirm - `plugins.entries["sprite-core"].enabled: true` is in your `openclaw.json` - and restart the gateway. +## Consuming from OpenClaw -## Enable +OpenClaw installs the plugin like any other npm-served extension: ```jsonc +// openclaw.json { "plugins": { "entries": { - "sprite-core": { - "enabled": true, - "config": { - "assets": { - "enabled": true, - "assetsDir": "./assets", - "publicAssets": false, - "maxAssetSizeBytes": 10485760, - "publicBaseUrl": "https://..ts.net", - }, - "streamTts": { - "enabled": true, - "provider": "elevenlabs", - "apiKey": { "source": "env", "id": "ELEVENLABS_API_KEY" }, - "defaultModel": "eleven_turbo_v2", - }, - "agents": { - "agent": { - "avatar": { - "kind": "atlas", - "default": "idle", - "manifest": "avatars/agent/agent.atlas.json", - }, - "voice": { - "provider": "elevenlabs", - "voiceId": "", - "label": "default", - }, - "prompting": { - "descriptions": { - "idle": "calm / listening", - "thinking": "processing the user's request", - "happy": "warm / pleased", - "sad": "sympathy / disappointment", - }, - }, - }, - }, - }, - }, - }, - }, -} -``` - -## Default `agent` template - -Ships under `template/agent/` in this repo. It declares four states -(`idle`, `thinking`, `happy`, `sad`) and includes a placeholder atlas image -(four solid-colored squares) so the runtime works the moment you enable the -plugin — no art required. - -To use the template: - -1. Copy `template/agent/` from this repo into - `~/.openclaw/assets/avatars/agent/` (or wherever your `assetsDir` resolves - to under the `avatars//` convention). -2. Paste the config block from `template/agent/README.md` into your - `openclaw.json` under `plugins.entries["sprite-core"].config.agents.agent`. -3. Restart the gateway. The watch will fetch the manifest, render the four - placeholder colors, and auto-swap to `thinking` on every send. - -Replace the placeholder image with real art whenever you have it; the manifest -schema does not need to change. See `template/agent/README.md` for the swap -procedure. - -## Config reference - -### `assets` - -Static asset serving for atlas images, frame trees, audio clips. - -| Field | Type | Notes | -| ------------------- | --------- | ------------------------------------------------------------------------------------------------- | -| `enabled` | `boolean` | Required to be `true` for the route to register. | -| `assetsDir` | `string` | Path the route serves from. Relative paths resolve under `~/.openclaw/state`. Default `./assets`. | -| `publicAssets` | `boolean` | When `true`, `/openclaw-assets/*` skips gateway auth. Use only when intentional. | -| `maxAssetSizeBytes` | `number` | Hard cap on per-file size. Default 10 MiB. | -| `publicBaseUrl` | `string` | URL the plugin advertises to clients in `/sprite-core/agents`. Useful for Tailscale endpoints. | - -Path traversal (`..`), symlinks pointing outside `assetsDir`, and dotfiles are -rejected. ETag + 24 h `Cache-Control` are set automatically. - -### `streamTts` - -Streaming TTS proxy. Today only ElevenLabs is wired. - -| Field | Type | Notes | -| -------------- | -------------- | -------------------------------------------------------------------------------------------------- | -| `enabled` | `boolean` | Required to be `true` for the route to register. | -| `provider` | `"elevenlabs"` | Only value supported today. | -| `apiKey` | `SecretInput` | Use `{ "source": "env", "id": "ELEVENLABS_API_KEY" }`. Plain strings are accepted but discouraged. | -| `defaultModel` | `string` | ElevenLabs model id. Default `eleven_turbo_v2`. Override per request via `?model=` query param. | - -> **The plugin ships without an ElevenLabs key.** You provide your own. -> Without `streamTts.enabled = true` and a valid `apiKey`, `/stream/tts` -> returns 503 and the watch falls back silently — agents still work, the -> avatar still animates, just no spoken audio. See [ElevenLabs setup](#elevenlabs-setup). -> -> For the full wire protocol of `/stream/tts` (query params, streaming MP3 -> response, how emotion directives map to ElevenLabs `voice_settings`, client -> composition examples) see [`docs/tts-integration.md`](docs/tts-integration.md). - -### `streamStt` - -Streaming STT proxy. Parallel to `streamTts` — same provider, same key, same -auth model. Clients POST raw audio; the plugin wraps it in multipart and -forwards to ElevenLabs's `/v1/speech-to-text`. - -| Field | Type | Notes | -| -------------- | -------------- | ----------------------------------------------------------------------------------------------- | -| `enabled` | `boolean` | Required to be `true` for the route to register. | -| `provider` | `"elevenlabs"` | Only value supported today. | -| `apiKey` | `SecretInput` | Same key as TTS — ElevenLabs uses one key for both. Reuse `{ "source": "env", "id": "ELEVENLABS_API_KEY" }`. | -| `defaultModel` | `string` | ElevenLabs model id. Default `scribe_v1`. Override per request via `?model=`. | -| `maxBodyBytes` | `number` | Optional plugin-level cap on inbound body size (checked against `Content-Length`). No default. | - -> For the full wire protocol of `/stream/stt` (accepted audio formats, query -> params → multipart field mapping, response JSON shape, error codes, curl -> example, phone-side press-and-hold flow) see -> [`docs/stt-integration.md`](docs/stt-integration.md). - -### `agents.` - -Per-agent rich descriptor that supersedes the legacy -`agents.list[].identity.avatar` object form and `agents.list[].voice` block. - -| Field | Type | Notes | -| ----------- | ----------------- | ------------------------------------------------------------------------------------------------- | -| `avatar` | `AvatarConfig` | Atlas descriptor — see below. | -| `voice` | `VoiceConfig` | `{ provider, voiceId, label, … }` — extra keys passed through to the watch. | -| `prompting` | `PromptingConfig` | Per-state descriptions used to build the model-side instruction. Optional `instruction` override. | - -#### `AvatarConfig` — `kind: "atlas"` (only kind currently supported) - -| Field | Type | Notes | -| ---------- | --------- | ------------------------------------------------------------ | -| `kind` | `"atlas"` | Discriminator. | -| `default` | `string` | State the agent holds when idle. Conventionally `idle`. | -| `manifest` | `string` | Path to the atlas JSON manifest, resolved under `assetsDir`. | - -The manifest itself owns frame rects, animations, and transitions — see -`docs/avatars/formats.md` for the full atlas schema. - -#### `VoiceConfig` - -Pass-through descriptor surfaced to the watch / phone via -`/sprite-core/agents`. Extra keys are allowed. - -```jsonc -"voice": { - "provider": "elevenlabs", - "voiceId": "21m00Tcm4TlvDq8ikWAM", - "label": "default" + "sprite-core": { "enabled": true, "config": { /* ... */ } } + } + } } ``` -#### `PromptingConfig` - -Drives the system-prompt block that teaches the model the avatar's emotion -vocabulary. - -| Field | Type | Notes | -| -------------- | ----------------------- | ------------------------------------------------------------------------------------------------- | -| `descriptions` | `Record` | One entry per state. Used to render `- : ` lines in the injected instruction. | -| `instruction` | `string` (optional) | Explicit override. When set, replaces the auto-generated text entirely. | - -The state names you list here must match keys in the atlas manifest's -`animations` table — that's how the watch maps a model-emitted -`<<>>` marker to the right animation. - -The keyword vocabulary (state names) lives in the gateway plugin; the parsing -of `<<>>` markers from the model output stays on the gateway side -(`src/gateway/avatar-marker-parser.ts`) and the playback code stays on the -edge devices (Wear OS DisplayKit). So edge devices stay generic — any state -name in the manifest just works. - -## Routes - -| Path | Auth | Purpose | -| ----------------------------- | --------- | ---------------------------------------------------------------------- | -| `GET /openclaw-assets/` | gateway\* | Static asset serving. \*`auth: "plugin"` when `publicAssets: true`. | -| `GET /stream/tts` | gateway | Streaming TTS proxy (ElevenLabs). | -| `POST /stream/stt` | gateway | Streaming STT proxy (ElevenLabs). | -| `GET /sprite-core/agents` | gateway | `{ agents: { : { avatar, voice } }, publicBaseUrl? }` for clients. | - -## Gateway RPC - -| Method | Purpose | -| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `node.getCharacterManifest` | Returns `{ manifest, revision }` — a ready-to-render manifest assembled from the plugin's per-agent atlas config + on-disk atlas JSON. The watch calls this through the phone relay. | - -`node.getCharacterManifest` is registered by this plugin via -`api.registerGatewayMethod` from `index.ts`. When the -plugin is disabled, the method is unregistered and returns "method not found" -naturally — operators get a graceful degradation rather than a stale handler. - -## How `thinking` auto-plays - -The Wear OS phone-relay (`apps/android/app/src/main/java/ai/openclaw/app/wear/WearRelayService.kt`) -publishes a `state: "thinking"` cue on the `/openclaw/avatars//state` -DataClient path the moment the user sends a message. If your manifest declares -a `thinking` animation, DisplayKit swaps to it. If it doesn't, the watch -no-ops and stays on the previous state. - -Model-emitted `<<>>` markers (parsed by -`src/gateway/avatar-marker-parser.ts`) override this state mid-reply — last -write wins. - -## ElevenLabs setup - -The plugin **does not** ship with a key. Steps for an operator: - -1. Create an ElevenLabs account at . -2. Get your API key from the profile menu. -3. Export it in your shell environment (the gateway must inherit it): - ```bash - export ELEVENLABS_API_KEY="sk_..." - ``` -4. Pick a voice id from your ElevenLabs voice library. -5. Wire both into `openclaw.json` under `plugins.entries["sprite-core"].config`: - - `streamTts.apiKey = { "source": "env", "id": "ELEVENLABS_API_KEY" }` - - `agents..voice = { "provider": "elevenlabs", "voiceId": "" }` -6. Restart the gateway. - -If you don't enable `streamTts`, agents still work normally — the watch's -TTS playback path falls back silently. - -## Security - -- Asset serving rejects path traversal (`..`), symlinks pointing outside - `assetsDir`, and dotfile access. -- File size capped by `maxAssetSizeBytes`. -- `publicAssets: true` skips gateway auth — only set this when you intentionally - serve operator-chosen files to anonymous clients (e.g. avatars on a public web page). -- The ElevenLabs API key should be a `SecretRef` (env, file, keychain), never - inlined as a plain string in committed config. - -## Plugin self-containment - -Everything avatar / character-manifest now lives in this plugin: - -- `src/prompting.ts` owns `buildPromptingInstruction` + `isAtlasAvatarConfig`. -- `src/character-manifest.ts` owns `buildCharacterManifest` and the wire-shape - inlined `CharacterManifest` type. -- `index.ts` registers `node.getCharacterManifest` via - `api.registerGatewayMethod` and reads fresh plugin config per call. - -Core has no atlas-shaped types: `IdentityConfig.avatar` is narrowed back to -`string` (path / URL / data URI / emoji), `AgentAvatarAtlasConfig` and -friends are deleted, the gateway agent row no longer carries an `avatarAtlas` -block. Disable the plugin and the only thing that stops working is the -multi-state sprite avatar (the simple string avatar still resolves through -core's `resolveAgentAvatar`). - -### Open follow-ups - -- None of substance. The prompt instruction is live (wired via - `api.registerSystemPromptContribution` from `index.ts`), and per-agent - `voice` has been removed from core — the plugin is the sole owner. - -## Pixellab.ai pipeline - -The plugin ships two Node scripts. Together they cover the create → animate -→ package flow end to end (once the animate step has its own script). - -### Create a character - -```bash -node scripts/pixellab-create.mjs \ - --name "elf" \ - --description "a magical elf with pointed ears" -``` - -Queues a 4-direction character on pixellab, polls the background job, and -prints the new `character_id` plus the four rotation URLs so you can eyeball -the look before adding animations. `--json` emits just the id + rotations -for scripting. - -### Add animations - -Not yet ported. Use the pixellab.ai web UI or the animate-character script -(operator-supplied). - -### Export into SpriteCore - -The plugin ships a Node exporter that downloads a finished pixellab.ai -character bundle by UUID and writes a SpriteCore-compatible atlas + manifest -directly into `/avatars//`: - -```bash -# Quick path — assumes pixellab key is in `pass` or exported as PIXELLAB_API_KEY -node scripts/pixellab-export.mjs \ - --uid - -# Explicit key command + custom output root -PIXELLAB_API_KEY="$(op read op://vault/pixellab/api-key)" \ - node scripts/pixellab-export.mjs \ - --uid \ - --assets-root ~/.openclaw/state/assets/avatars \ - --overwrite - -# Dry-run the plan without touching pixellab or disk -node scripts/pixellab-export.mjs --uid --dry-run -``` - -Auth resolution order: `PIXELLAB_API_KEY` env → `--api-key-command ""` -→ `pass show pixellab/api-key`. Pick whichever matches your secret store. - -Output: - -- `/avatars//.atlas.webp` — packed atlas image. -- `/avatars//.atlas.json` — manifest. -- `/avatars//frames//NN.webp` — per-state frame - tree (useful for re-packing via `pnpm avatar:pack`). - -The exporter pairs zip-folder hashes with the pixellab API's -`animation_type` field (via `GET /characters//animations`) to emit clean -canonical SpriteCore state names — `happy`, `sad`, `thinking`, `idle` — and -generates descriptions from the animation's `display_name` (or the original -emotion prompt when no display name is set). Duplicate canonical names (e.g. -two `idle` animations of different lengths) get `_2`/`_3` suffixes. If the -metadata fetch fails, it falls back to verbose slug names. - -For the end-to-end create → approve → animate → export flow, see the -`openclaw-pixellab-avatar` skill at -`.agents/skills/openclaw-pixellab-avatar/SKILL.md`. - -The `pixellab.ai` online pixel-sprite generator is a candidate art pipeline -for the template. The intent is: +And the apps pull in the language-appropriate client SDK: -1. Operator runs a Claude Code skill (`.agents/skills/openclaw-pixellab-avatar/SKILL.md`). -2. Skill walks them through pixellab signup + API key extraction. -3. Skill prompts pixellab to generate a character + the emotions/states the - operator wants. -4. A packaging script (`scripts/avatars/pixellab-import.mjs`) downloads the - results and wires them into the SpriteCore template layout - (`avatars//.atlas.{webp,json}`). +- Web / Electron / React Native → `@tyler-rng/sprite-core-client` (npm) +- Android phone + Wear OS watch → `ai.openclaw.spritecore:sprite-core-client` (Maven) +- iOS / macOS → `SpriteCoreClient` (SwiftPM) -The skill exists as a stub. The packaging script is not yet implemented (the -upstream pixellab.ai API contract needs to be confirmed first); see -`scripts/avatars/pixellab-import.mjs` for the placeholder. +For live-development against OpenClaw without publishing, both Gradle +(`includeBuild`) and SwiftPM (`path:`) support local path links. -## Open follow-ups +## License -- **Pixellab exporter transition cleanup.** `scripts/pixellab-export.mjs` - unconditionally writes `*->thinking` / `thinking->*` transitions into - every atlas manifest, even when the `thinking` animation has no phased - `.intro` / `.outro` sub-sequences (the common case for v3-mode outputs). - Lint noise in the generated manifest; the runtime silently no-ops on the - missing phases. Only emit those transitions when the thinking animation - actually has intro/outro phases. ~10-line fix. -- **Pixellab `animate` template-mode investigation.** `scripts/pixellab-animate.mjs` - uses `mode: "v3"`, which produces `animation_type: "custom-"` names - instead of canonical `happy` / `sad` / `thinking` names. The exporter - currently papers over this with a `--rename` mapping. Pixellab's API may - expose a `template_animation_id` path (or a PATCH for `display_name`) - that would eliminate the workaround — confirm against - `https://api.pixellab.ai/v2/openapi.json` and migrate if available. -- **Authenticated end-to-end smoke against ElevenLabs.** Unit tests cover - the handler logic exhaustively, but nothing has sent real audio through - `POST /stream/stt` + real text through `GET /stream/tts` on a paired - device end-to-end recently. Worth one credit-burning pass periodically. +MIT. diff --git a/fixtures/README.md b/fixtures/README.md new file mode 100644 index 0000000..6f1b39a --- /dev/null +++ b/fixtures/README.md @@ -0,0 +1,118 @@ +# Conformance Fixtures + +Language-agnostic test oracles. All three client SDKs (`client-js`, +`client-kotlin`, `client-swift`) load fixtures from this directory and +replay the declared inputs; byte-identical behaviour is the contract. + +When a fixture passes on all three, the runtimes are conforming. + +## Directory layout + +``` +fixtures/ +├── README.md ← this file +├── animation-graph/ ← `AnimationGraph` behaviour +│ └── wildcard-transitions.json +├── sprite-player/ ← `SpriteAnimationPlayer` behaviour +│ ├── phased-intro.json +│ ├── play-count.json +│ └── ping-pong.json +├── marker/ ← `AvatarMarkerParser` behaviour +│ ├── bare-markers.json +│ ├── play-count-markers.json +│ ├── split-across-chunks.json +│ └── invalid-shapes.json +└── manifest/ ← decoder shape checks + └── minimal-headshot.json +``` + +## Fixture kinds + +### `animation-graph/*.json` + +```jsonc +{ + "kind": "animation-graph", + "description": "wildcard precedence resolves most-specific → least-specific", + "manifest": { /* CharacterManifest */ }, + "mode": "headshot", + "cases": [ + { + "name": "exact match beats wildcard", + "resolveTransition": { "from": "idle", "to": "thinking" }, + "expected": "thinking.intro" + } + ] +} +``` + +Runners: +1. Deserialize `manifest`. +2. Build the graph via `AnimationGraph.fromManifest(manifest, mode)`. +3. For each case, run `resolveTransition(from, to)` and compare. + - Expected string → phase reference (TransitionRef.Phase) + - Expected object `{ blend, ms }` → crossfade + +### `sprite-player/*.json` + +```jsonc +{ + "kind": "sprite-player", + "description": "playing a phased state once runs intro then loop", + "manifest": { /* CharacterManifest */ }, + "mode": "headshot", + "requests": [ + { "target": "thinking", "playCount": null, "advanceMs": 500 } + ], + "expectedRefSequence": [ + "thinking.intro.00", "thinking.intro.01", "thinking.loop.00" + ] +} +``` + +Runners use a fake `Ticker` that advances virtual time and record every +`currentRef` emission. Expected sequence is compared after all requests +and their `advanceMs` intervals have been processed. + +### `marker/*.json` + +```jsonc +{ + "kind": "marker", + "description": "bare <<>> stripped and surfaced", + "cases": [ + { + "name": "single marker", + "chunks": ["hello <<>> world"], + "expectedCleanedText": "hello world", + "expectedMarkers": [{ "state": "happy", "count": null }] + }, + { + "name": "split across chunks", + "chunks": ["start <<>> end"], + "expectedCleanedText": "start end", + "expectedMarkers": [{ "state": "happy", "count": null }] + } + ] +} +``` + +Runners create a fresh parser per case, push chunks in order, call flush, +and compare concatenated cleaned text + total markers list. + +### `manifest/*.json` + +Simplest form — just a JSON object to decode + assertions about the result. +Used to pin decoder behaviour (required fields, optional defaults, union +handling for `TransitionRef`). Expected output is implementation-defined +per runner — usually "it decodes without error and fields match." + +## Adding a new fixture + +1. Drop the JSON file in the right sub-directory. +2. Add a runner assertion in each language's test suite (or, for pure + marker/manifest fixtures, rely on the shared loader). +3. `pnpm test` / `./gradlew test` / `swift test` should all pass. + +If a fixture is meant to exercise a **future** feature, mark it with +`"skip": true` at the top and a note in `description` saying why. diff --git a/fixtures/animation-graph/wildcard-transitions.json b/fixtures/animation-graph/wildcard-transitions.json new file mode 100644 index 0000000..91629b4 --- /dev/null +++ b/fixtures/animation-graph/wildcard-transitions.json @@ -0,0 +1,65 @@ +{ + "kind": "animation-graph", + "description": "Wildcard transitions resolve by specificity: exact → from-wildcard → to-wildcard → *->*.", + "manifest": { + "version": 1, + "agentId": "agent", + "modes": ["headshot"], + "stateMap": { "idle": "idle", "thinking": "thinking", "happy": "happy" }, + "content": { + "headshot": { + "animations": { + "idle": { + "sequence": { "frames": [{ "ref": "idle.00" }], "fps": 12, "loop": "infinite" } + }, + "thinking": { + "intro": { "frames": [{ "ref": "thinking.intro.00" }], "fps": 24, "loop": "once" }, + "loop": { "frames": [{ "ref": "thinking.loop.00" }], "fps": 12, "loop": "infinite" }, + "outro": { "frames": [{ "ref": "thinking.outro.00" }], "fps": 24, "loop": "once" } + }, + "happy": { + "sequence": { "frames": [{ "ref": "happy.00" }], "fps": 24, "loop": "once" } + } + }, + "transitions": { + "idle->happy": "happy", + "thinking->*": "thinking.outro", + "*->thinking": "thinking.intro", + "*->*": "idle" + } + } + }, + "assets": { + "refs": { + "idle.00": "p/idle_00", + "thinking.intro.00": "p/thinking_intro_00", + "thinking.loop.00": "p/thinking_loop_00", + "thinking.outro.00": "p/thinking_outro_00", + "happy.00": "p/happy_00" + } + } + }, + "mode": "headshot", + "cases": [ + { + "name": "exact pair match wins over all wildcards", + "resolveTransition": { "from": "idle", "to": "happy" }, + "expected": "happy" + }, + { + "name": "from-wildcard wins when no exact pair", + "resolveTransition": { "from": "thinking", "to": "happy" }, + "expected": "thinking.outro" + }, + { + "name": "to-wildcard wins when no exact or from-wildcard", + "resolveTransition": { "from": "happy", "to": "thinking" }, + "expected": "thinking.intro" + }, + { + "name": "star-star catches what nothing else does", + "resolveTransition": { "from": "happy", "to": "idle" }, + "expected": "idle" + } + ] +} diff --git a/fixtures/manifest/minimal-headshot.json b/fixtures/manifest/minimal-headshot.json new file mode 100644 index 0000000..d5fd332 --- /dev/null +++ b/fixtures/manifest/minimal-headshot.json @@ -0,0 +1,26 @@ +{ + "kind": "manifest", + "description": "Smallest valid manifest: one mode, one flat animation, one asset ref", + "manifest": { + "version": 1, + "agentId": "ginger", + "modes": ["headshot"], + "stateMap": { "idle": "idle" }, + "content": { + "headshot": { + "animations": { + "idle": { + "sequence": { + "frames": [{ "ref": "idle.00" }], + "fps": 12, + "loop": "infinite" + } + } + } + } + }, + "assets": { + "refs": { "idle.00": "atlas/idle_00.webp" } + } + } +} diff --git a/fixtures/marker/bare-markers.json b/fixtures/marker/bare-markers.json new file mode 100644 index 0000000..581d7cc --- /dev/null +++ b/fixtures/marker/bare-markers.json @@ -0,0 +1,33 @@ +{ + "kind": "marker", + "description": "Bare <<>> markers without play-count.", + "cases": [ + { + "name": "single marker in middle of text", + "chunks": ["hello <<>> world"], + "expectedCleanedText": "hello world", + "expectedMarkers": [{ "state": "happy", "count": null }] + }, + { + "name": "two markers in same chunk", + "chunks": ["<<>> a <<>> b"], + "expectedCleanedText": " a b", + "expectedMarkers": [ + { "state": "idle", "count": null }, + { "state": "thinking", "count": null } + ] + }, + { + "name": "no markers in text", + "chunks": ["just some words"], + "expectedCleanedText": "just some words", + "expectedMarkers": [] + }, + { + "name": "empty input", + "chunks": [""], + "expectedCleanedText": "", + "expectedMarkers": [] + } + ] +} diff --git a/fixtures/marker/invalid-shapes.json b/fixtures/marker/invalid-shapes.json new file mode 100644 index 0000000..0f080ad --- /dev/null +++ b/fixtures/marker/invalid-shapes.json @@ -0,0 +1,24 @@ +{ + "kind": "marker", + "description": "Invalid marker bodies emit as literal text; nothing silently vanishes.", + "cases": [ + { + "name": "body contains whitespace", + "chunks": ["keep <<>> me"], + "expectedCleanedText": "keep <<>> me", + "expectedMarkers": [] + }, + { + "name": "empty body", + "chunks": ["keep <<<>>> me"], + "expectedCleanedText": "keep <<<>>> me", + "expectedMarkers": [] + }, + { + "name": "body contains disallowed punctuation", + "chunks": ["keep <<>> me"], + "expectedCleanedText": "keep <<>> me", + "expectedMarkers": [] + } + ] +} diff --git a/fixtures/marker/play-count-markers.json b/fixtures/marker/play-count-markers.json new file mode 100644 index 0000000..9823b90 --- /dev/null +++ b/fixtures/marker/play-count-markers.json @@ -0,0 +1,36 @@ +{ + "kind": "marker", + "description": "<<>> markers carry a numeric play-count suffix.", + "cases": [ + { + "name": "explicit play-once marker", + "chunks": ["<<>> goodbye"], + "expectedCleanedText": " goodbye", + "expectedMarkers": [{ "state": "wink", "count": 1 }] + }, + { + "name": "multi-play count", + "chunks": ["hi <<>>"], + "expectedCleanedText": "hi ", + "expectedMarkers": [{ "state": "happy", "count": 3 }] + }, + { + "name": "count zero == explicit loop forever", + "chunks": ["<<>>"], + "expectedCleanedText": "", + "expectedMarkers": [{ "state": "idle", "count": 0 }] + }, + { + "name": "hyphenated state name without numeric suffix", + "chunks": ["<<>>"], + "expectedCleanedText": "", + "expectedMarkers": [{ "state": "head-cocked", "count": null }] + }, + { + "name": "hyphenated state name with numeric suffix — last dash wins", + "chunks": ["<<>>"], + "expectedCleanedText": "", + "expectedMarkers": [{ "state": "head_cocked", "count": 2 }] + } + ] +} diff --git a/fixtures/marker/split-across-chunks.json b/fixtures/marker/split-across-chunks.json new file mode 100644 index 0000000..039d494 --- /dev/null +++ b/fixtures/marker/split-across-chunks.json @@ -0,0 +1,24 @@ +{ + "kind": "marker", + "description": "The streaming parser recognises markers whose open/body/close is split across push() calls.", + "cases": [ + { + "name": "marker split between chunks", + "chunks": ["start <<>> end"], + "expectedCleanedText": "start end", + "expectedMarkers": [{ "state": "happy", "count": null }] + }, + { + "name": "partial <<< at tail of chunk buffered for next", + "chunks": ["prefix <<", ">> tail"], + "expectedCleanedText": "prefix tail", + "expectedMarkers": [{ "state": "happy", "count": null }] + }, + { + "name": "unterminated marker flushed as literal", + "chunks": ["keep me <<=2026.4.15-beta.1" - }, - "peerDependenciesMeta": { - "openclaw": { - "optional": true - } - }, - "openclaw": { - "extensions": [ - "./index.ts" - ], - "install": { - "npmSpec": "@tyler-rng/sprite-core", - "defaultChoice": "npm", - "minHostVersion": ">=2026.4.10" - }, - "compat": { - "pluginApi": ">=2026.4.15-beta.1" - }, - "build": { - "openclawVersion": "2026.4.15-beta.1" - } - } + "packageManager": "pnpm@9.0.0" } diff --git a/packages/client-js/README.md b/packages/client-js/README.md new file mode 100644 index 0000000..52b3ee5 --- /dev/null +++ b/packages/client-js/README.md @@ -0,0 +1,66 @@ +# @tyler-rng/sprite-core-client + +TypeScript client SDK for the SpriteCore plugin. Consumes the `CharacterManifest` +wire shape emitted by `node.getCharacterManifest` and drives a portable +animation graph + sprite player that any JS runtime (browser, Node, Electron, +React Native) can render. + +This is the reference implementation — the Kotlin and Swift kits in sibling +packages are functional mirrors of this code, validated against the shared +`fixtures/` suite at the repo root. + +## Install + +``` +npm install @tyler-rng/sprite-core-client +``` + +Published to GitHub Packages under the `@tyler-rng` scope. + +## Minimal usage + +```ts +import { + AnimationGraph, + SpriteAnimationPlayer, + InMemorySpriteSource, + createAvatarMarkerParser, +} from "@tyler-rng/sprite-core-client"; + +const envelope = await fetchCharacterManifest("my-agent"); +const graph = AnimationGraph.fromManifest(envelope.manifest, "headshot"); + +const frameSource = new InMemorySpriteSource((bytes) => + createImageBitmap(new Blob([bytes])), +); +for (const [refKey, bytes] of Object.entries(assetBytes)) { + frameSource.put(refKey, bytes); +} + +const player = new SpriteAnimationPlayer(graph); +player.currentRef.subscribe((ref) => { + if (!ref) return; + const bitmap = frameSource.frame(ref); + drawToCanvas(bitmap); +}); + +// When the model emits `<<>>`: +const parser = createAvatarMarkerParser(); +const { markers, cleanedText } = parser.push(streamedChunk); +for (const m of markers) { + player.requestState(m.state, m.count); +} +``` + +## Surface + +- `CharacterManifest` types and TypeBox schemas (re-exported from + `@tyler-rng/sprite-core-schema`) +- `AnimationGraph.fromManifest(manifest, mode)` — projection + wildcard + transition resolver +- `SpriteAnimationPlayer` — state machine, phases, play-count, ping-pong +- `FrameSource` interface + `InMemorySpriteSource` +- `AssetSource` — manifest + asset cache with revision checks +- `createAvatarMarkerParser()` / `splitByMarkers()` — streaming marker parser +- `MutableObservable` — minimal StateFlow equivalent for `currentRef` / + `currentState` diff --git a/packages/client-js/package.json b/packages/client-js/package.json new file mode 100644 index 0000000..084e926 --- /dev/null +++ b/packages/client-js/package.json @@ -0,0 +1,44 @@ +{ + "name": "@tyler-rng/sprite-core-client", + "version": "1.0.0", + "description": "TypeScript client SDK for the SpriteCore plugin — portable animation graph + sprite player for browsers, Node, and any JS runtime", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./schema": { + "types": "./dist/schema.d.ts", + "import": "./dist/schema.js" + } + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/Tyler-RNG/sprite-core.git", + "directory": "packages/client-js" + }, + "license": "MIT", + "scripts": { + "build": "tsc -p tsconfig.json", + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@tyler-rng/sprite-core-schema": "workspace:*" + }, + "devDependencies": { + "typescript": "^5.6.0", + "vitest": "^2.0.0" + }, + "publishConfig": { + "registry": "https://npm.pkg.github.com" + } +} diff --git a/packages/client-js/src/animation-graph.test.ts b/packages/client-js/src/animation-graph.test.ts new file mode 100644 index 0000000..be9f545 --- /dev/null +++ b/packages/client-js/src/animation-graph.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it } from "vitest"; +import { AnimationGraph, resolveTransition } from "./animation-graph.js"; +import type { CharacterManifest } from "./schema.js"; + +const baseManifest: CharacterManifest = { + version: 1, + agentId: "agent", + modes: ["headshot"], + stateMap: { idle: "idle", thinking: "thinking" }, + content: { + headshot: { + animations: { + idle: { + sequence: { + frames: [{ ref: "idle.00" }], + fps: 12, + loop: "infinite", + }, + }, + thinking: { + intro: { + frames: [{ ref: "thinking.intro.00" }], + fps: 24, + loop: "once", + }, + loop: { + frames: [{ ref: "thinking.loop.00" }], + fps: 12, + loop: "infinite", + }, + outro: { + frames: [{ ref: "thinking.outro.00" }], + fps: 24, + loop: "once", + }, + }, + }, + transitions: { + "*->thinking": "thinking.intro", + "thinking->*": "thinking.outro", + }, + }, + }, + assets: { + refs: { + "idle.00": "atlas/idle_00.webp", + "thinking.intro.00": "atlas/thinking_intro_00.webp", + "thinking.loop.00": "atlas/thinking_loop_00.webp", + "thinking.outro.00": "atlas/thinking_outro_00.webp", + }, + }, +}; + +describe("AnimationGraph.fromManifest", () => { + it("projects a mode's content into a graph", () => { + const g = AnimationGraph.fromManifest(baseManifest, "headshot"); + expect(g.defaultState).toBe("idle"); + expect(Object.keys(g.animations).sort()).toEqual(["idle", "thinking"]); + expect(g.transitions["*->thinking"]).toBe("thinking.intro"); + }); + + it("throws when the mode is absent", () => { + expect(() => AnimationGraph.fromManifest(baseManifest, "fullbody")).toThrow( + /no content for mode/, + ); + }); + + it("resolves transitions with wildcard precedence", () => { + const g = AnimationGraph.fromManifest(baseManifest, "headshot"); + expect(g.resolveTransition("idle", "thinking")).toBe("thinking.intro"); + expect(g.resolveTransition("thinking", "idle")).toBe("thinking.outro"); + expect(g.resolveTransition("unknown", "also-unknown")).toBeNull(); + }); +}); + +describe("resolveTransition", () => { + it("parses phase refs", () => { + expect(resolveTransition("thinking.intro")).toEqual({ + animation: "thinking", + phase: "intro", + }); + expect(resolveTransition("thinking")).toEqual({ + animation: "thinking", + phase: "loop", + }); + }); +}); diff --git a/packages/client-js/src/animation-graph.ts b/packages/client-js/src/animation-graph.ts new file mode 100644 index 0000000..c33576a --- /dev/null +++ b/packages/client-js/src/animation-graph.ts @@ -0,0 +1,101 @@ +import type { + Animation, + CharacterManifest, + TransitionRef, +} from "./schema.js"; + +/** + * Resolved animation table + transition graph for a single mode of a single + * character. Both sprite and atlas manifests project into this shape so the + * player stays format-agnostic. + * + * Build via [fromManifest] to pull a mode's content out of a server-synthesized + * [CharacterManifest], or construct directly for tests. + */ +export class AnimationGraph { + constructor( + readonly defaultState: string, + readonly animations: Readonly>, + readonly transitions: Readonly>, + ) {} + + /** + * Resolve a state→state transition against the transitions table using + * wildcard pattern matching. Specificity order (most→least specific): + * + * "->" → "->*" → "*->" → "*->*" + * + * Returns null when nothing matches; the caller then swaps instantly. + */ + resolveTransition(from: string, to: string): TransitionRef | null { + const keys = [`${from}->${to}`, `${from}->*`, `*->${to}`, `*->*`]; + for (const k of keys) { + const t = this.transitions[k]; + if (t !== undefined) return t; + } + return null; + } + + /** + * Extract a single mode's animation graph from a character manifest. The + * default state is taken from [stateMap] — the first key that maps to an + * animation present in [mode]'s content — or fails if no animation is + * present. + */ + static fromManifest(manifest: CharacterManifest, mode: string): AnimationGraph { + const content = manifest.content[mode]; + if (!content) { + throw new Error( + `manifest has no content for mode '${mode}'. Available: [${Object.keys(manifest.content).join(", ")}]`, + ); + } + const defaultState = resolveDefaultState(manifest.stateMap, content.animations); + return new AnimationGraph(defaultState, content.animations, content.transitions ?? {}); + } +} + +function resolveDefaultState( + stateMap: Record, + animations: Record, +): string { + for (const [, animName] of Object.entries(stateMap)) { + if (animName in animations) return animName; + } + const first = Object.keys(animations)[0]; + if (first === undefined) { + throw new Error("manifest mode has no animations"); + } + return first; +} + +/** The three phases of a phased animation; flat animations use `loop`. */ +export type Phase = "intro" | "loop" | "outro"; + +/** + * A transition target resolved for playback: which animation + phase to play + * once before entering the target state's own loop. Used by the player when a + * phase-string `TransitionRef` fires on state change. + */ +export type ResolvedTransition = { + animation: string; + phase: Phase; +}; + +/** Parse `"thinking.intro"` into `{ animation: "thinking", phase: "intro" }`. Unqualified → loop. */ +export function resolveTransition(ref: string): ResolvedTransition { + const dot = ref.indexOf("."); + if (dot < 0) return { animation: ref, phase: "loop" }; + const phase = ref.slice(dot + 1) as Phase; + if (phase !== "intro" && phase !== "loop" && phase !== "outro") { + throw new Error(`unknown phase: ${phase}`); + } + return { animation: ref.slice(0, dot), phase }; +} + +/** + * Treat a flat animation as the `loop` phase so the player can always look up + * phases by name without special-casing flat vs phased at every site. + */ +export function effectiveLoop(anim: Animation) { + return anim.loop ?? anim.sequence; +} diff --git a/packages/client-js/src/asset-source.ts b/packages/client-js/src/asset-source.ts new file mode 100644 index 0000000..30dfb72 --- /dev/null +++ b/packages/client-js/src/asset-source.ts @@ -0,0 +1,192 @@ +import type { CharacterManifest, NodeGetCharacterManifestResult } from "./schema.js"; +import { MutableObservable, type Observable } from "./observable.js"; + +/** + * Versioned per-agent animation signal. `version` bumps on every + * `setAgentState` call so UI consumers keyed on the signal re-trigger even + * when the state name is unchanged. `count` is forwarded from the parsed + * `<<>>` marker and governs playback cadence. + */ +export type AvatarMarkerSignal = { + state: string; + count: number | null; + version: number; +}; + +export type CachedAgent = { + agentId: string; + envelope: NodeGetCharacterManifestResult; + assetBytes: Readonly>; +}; + +export type AssetSourceHooks = { + fetchManifest: (agentId: string) => Promise; + fetchAsset: (relativePath: string) => Promise; +}; + +/** + * Client-side unified fetcher + cache for per-agent CharacterManifest + * envelopes and their asset bytes. Ports the Kotlin `AgentAvatarSource`: + * + * - `characterManifests` — latest envelope per agent + * - `characterAssets` — latest asset bytes map per agent + * - `agentMarkerSignals` — monotonic versioned state signal per agent + * + * Fetch policy is explicit: callers invoke `refresh(agentIds)`. An agent + * already present at the same revision is left alone; revision bumps trigger + * a re-fetch of changed asset refs. + */ +export class AssetSource { + private readonly _characterManifests = new MutableObservable< + Readonly> + >({}); + private readonly _characterAssets = new MutableObservable< + Readonly>>> + >({}); + private readonly _agentStates = new MutableObservable>>({}); + private readonly _agentMarkerSignals = new MutableObservable< + Readonly> + >({}); + private signalVersionSeq = 0; + private inflight: Promise | null = null; + + readonly characterManifests: Observable>> = + this._characterManifests; + readonly characterAssets: Observable< + Readonly>>> + > = this._characterAssets; + readonly agentStates: Observable>> = this._agentStates; + readonly agentMarkerSignals: Observable>> = + this._agentMarkerSignals; + + constructor(private readonly hooks: AssetSourceHooks) {} + + /** + * Kick off a refresh for each agent. Returns when all fetches settle. + * No-ops for agents whose manifest is already cached at the current + * revision. + */ + async refresh(agentIds: readonly string[]): Promise { + if (agentIds.length === 0) return; + // Serialize refreshes to match the Kotlin Mutex behavior. + const prev = this.inflight ?? Promise.resolve(); + let release!: () => void; + this.inflight = new Promise((r) => { + release = r; + }); + try { + await prev; + for (const agentId of agentIds) { + await this.refreshOne(agentId); + } + } finally { + release(); + } + } + + /** + * Update the current state for an agent. Called when an `<<>>` or + * `<<>>` marker fires. + */ + setAgentState(agentId: string, stateName: string, count: number | null = null): void { + this._agentStates.set({ ...this._agentStates.value, [agentId]: stateName }); + this.signalVersionSeq += 1; + const signal: AvatarMarkerSignal = { + state: stateName, + count, + version: this.signalVersionSeq, + }; + this._agentMarkerSignals.set({ + ...this._agentMarkerSignals.value, + [agentId]: signal, + }); + } + + /** + * Snapshot of the current cache. Values are consistent per call; + * concurrent cache updates between calls are expected and safe. + */ + snapshot(): readonly CachedAgent[] { + const manifests = this._characterManifests.value; + const assets = this._characterAssets.value; + return Object.entries(manifests).map(([agentId, envelope]) => ({ + agentId, + envelope, + assetBytes: assets[agentId] ?? {}, + })); + } + + /** Drop any cached entries for agents no longer in [keepIds]. */ + retainOnly(keepIds: Iterable): void { + const keep = new Set(keepIds); + this._characterManifests.set(filterKeys(this._characterManifests.value, keep)); + this._characterAssets.set(filterKeys(this._characterAssets.value, keep)); + this._agentStates.set(filterKeys(this._agentStates.value, keep)); + } + + clear(): void { + this._characterManifests.set({}); + this._characterAssets.set({}); + this._agentStates.set({}); + } + + /** + * Resolve the default state name for [agentId] from its cached manifest. + * Mirrors `AnimationGraph.fromManifest` default-state logic so the two + * never drift. + */ + defaultStateFor(agentId: string): string | null { + const envelope = this._characterManifests.value[agentId]; + if (!envelope) return null; + return resolveDefaultStateName(envelope.manifest); + } + + // --- internals --- + + private async refreshOne(agentId: string): Promise { + const envelope = await this.hooks.fetchManifest(agentId); + if (!envelope) return; + const existing = this._characterManifests.value[agentId]; + if (existing && existing.revision === envelope.revision) return; + this._characterManifests.set({ + ...this._characterManifests.value, + [agentId]: envelope, + }); + + const refs = envelope.manifest.assets.refs; + const bytesByRef: Record = {}; + for (const [refKey, relPath] of Object.entries(refs)) { + const bytes = await this.hooks.fetchAsset(relPath); + if (bytes !== null) { + bytesByRef[refKey] = bytes; + } + } + this._characterAssets.set({ + ...this._characterAssets.value, + [agentId]: bytesByRef, + }); + } +} + +function filterKeys( + source: Readonly>, + keep: Set, +): Record { + const out: Record = {}; + for (const [k, v] of Object.entries(source)) { + if (keep.has(k)) out[k] = v; + } + return out; +} + +function resolveDefaultStateName(manifest: CharacterManifest): string | null { + const mode = manifest.modes.find((m) => m in manifest.content); + if (!mode) return null; + const animations = manifest.content[mode]?.animations; + if (!animations) return null; + for (const [, animName] of Object.entries(manifest.stateMap)) { + if (animName in animations) return animName; + } + const first = Object.keys(animations)[0]; + return first ?? null; +} diff --git a/packages/client-js/src/frame-source.ts b/packages/client-js/src/frame-source.ts new file mode 100644 index 0000000..cfbc0ea --- /dev/null +++ b/packages/client-js/src/frame-source.ts @@ -0,0 +1,47 @@ +import type { FrameRef } from "./schema.js"; + +/** + * Platform-specific resolver from a [FrameRef] to a concrete renderable + * (e.g. `HTMLImageElement`, `ImageBitmap`, an `` URL, whatever the + * caller chooses). The kit itself never constructs frames — callers own the + * pixel pipeline and only feed the player's emitted `FrameRef` into their + * own `FrameSource` when rendering. + * + * Atlas sources honor the optional `x/y/w/h` fields on `FrameRef`; sprite + * sources ignore them and treat `ref` as the whole-image key. + */ +export interface FrameSource { + frame(ref: FrameRef): F | null; +} + +/** + * Simple in-memory sprite source: callers prime a map of whole-image bytes + * keyed by the ref name, and decode happens lazily through [decode]. Useful + * for unit tests and thin clients that don't need per-platform image types. + */ +export class InMemorySpriteSource implements FrameSource { + private readonly bytesByRef = new Map(); + private readonly cache = new Map(); + + constructor(private readonly decode: (bytes: Uint8Array) => F | null) {} + + put(refKey: string, bytes: Uint8Array): void { + this.bytesByRef.set(refKey, bytes); + this.cache.delete(refKey); + } + + keys(): ReadonlySet { + return new Set(this.bytesByRef.keys()); + } + + frame(ref: FrameRef): F | null { + const cached = this.cache.get(ref.ref); + if (cached !== undefined) return cached; + const bytes = this.bytesByRef.get(ref.ref); + if (bytes === undefined) return null; + const decoded = this.decode(bytes); + if (decoded === null) return null; + this.cache.set(ref.ref, decoded); + return decoded; + } +} diff --git a/packages/client-js/src/index.ts b/packages/client-js/src/index.ts new file mode 100644 index 0000000..7954dd0 --- /dev/null +++ b/packages/client-js/src/index.ts @@ -0,0 +1,7 @@ +export * from "./schema.js"; +export * from "./frame-source.js"; +export * from "./ticker.js"; +export * from "./animation-graph.js"; +export * from "./sprite-player.js"; +export * from "./observable.js"; +export * from "./asset-source.js"; diff --git a/packages/client-js/src/marker.test.ts b/packages/client-js/src/marker.test.ts new file mode 100644 index 0000000..9b5f5dd --- /dev/null +++ b/packages/client-js/src/marker.test.ts @@ -0,0 +1,91 @@ +import { describe, it, expect } from "vitest"; +import { + createAvatarMarkerParser, + parseAvatarMarkers, + resolveStateAndCount, + splitByMarkers, +} from "./schema.js"; + +describe("resolveStateAndCount", () => { + it("parses bare state with no dash", () => { + expect(resolveStateAndCount("happy")).toEqual({ state: "happy", count: null }); + }); + it("parses state-N with numeric suffix", () => { + expect(resolveStateAndCount("happy-3")).toEqual({ state: "happy", count: 3 }); + expect(resolveStateAndCount("wink-1")).toEqual({ state: "wink", count: 1 }); + expect(resolveStateAndCount("happy-0")).toEqual({ state: "happy", count: 0 }); + }); + it("leaves non-numeric dash suffixes alone", () => { + expect(resolveStateAndCount("head-cocked")).toEqual({ + state: "head-cocked", + count: null, + }); + }); + it("triggers on the last dash only", () => { + expect(resolveStateAndCount("head_cocked-1")).toEqual({ + state: "head_cocked", + count: 1, + }); + }); +}); + +describe("createAvatarMarkerParser", () => { + it("strips a single marker and surfaces it", () => { + const p = createAvatarMarkerParser(); + const { cleanedText, markers } = p.push("hello <<>> world"); + expect(cleanedText).toBe("hello world"); + expect(markers).toEqual([{ state: "happy", count: null }]); + }); + + it("preserves invalid marker shapes as literal text", () => { + const { cleanedText, markers } = parseAvatarMarkers("bad <<>> marker"); + expect(cleanedText).toBe("bad <<>> marker"); + expect(markers).toEqual([]); + }); + + it("recognizes a marker split across two chunks", () => { + const p = createAvatarMarkerParser(); + const a = p.push("start <<>> end"); + expect(a.cleanedText + b.cleanedText).toBe("start end"); + expect([...a.markers, ...b.markers]).toEqual([{ state: "happy", count: null }]); + }); + + it("surfaces play-count markers", () => { + const { markers } = parseAvatarMarkers("say <<>> it"); + expect(markers).toEqual([{ state: "wink", count: 1 }]); + }); + + it("flushes unterminated markers as literal text", () => { + const p = createAvatarMarkerParser(); + const a = p.push("tail << { + const p = createAvatarMarkerParser(); + const a = p.push("ok <<"); + expect(a.cleanedText).toBe("ok "); + const b = p.push(">> done"); + expect(a.cleanedText + b.cleanedText).toBe("ok done"); + expect([...a.markers, ...b.markers]).toEqual([{ state: "happy", count: null }]); + }); +}); + +describe("splitByMarkers", () => { + it("splits with preceding emotion attached to each segment", () => { + const segs = splitByMarkers("hi <<>> world <<>> end"); + expect(segs).toEqual([ + { text: "hi ", emotion: null, emotionCount: null }, + { text: " world ", emotion: "happy", emotionCount: null }, + { text: " end", emotion: "sad", emotionCount: null }, + ]); + }); + + it("forwards count onto the segment", () => { + const segs = splitByMarkers("<<>> hello"); + expect(segs).toEqual([{ text: " hello", emotion: "wink", emotionCount: 2 }]); + }); +}); diff --git a/packages/client-js/src/observable.ts b/packages/client-js/src/observable.ts new file mode 100644 index 0000000..c3a8559 --- /dev/null +++ b/packages/client-js/src/observable.ts @@ -0,0 +1,40 @@ +/** + * Minimal observable value. Held as a single writable cell with a listener + * set — matches `MutableStateFlow` on the Kotlin side closely enough that + * subscribers see the current value + every subsequent update. Not a + * reactive system; exists only so the player can expose `currentRef` / + * `currentState` without pulling in rxjs. + */ +export interface Observable { + readonly value: T; + subscribe(listener: (value: T) => void): () => void; +} + +export class MutableObservable implements Observable { + private current: T; + private readonly listeners = new Set<(value: T) => void>(); + + constructor(initial: T) { + this.current = initial; + } + + get value(): T { + return this.current; + } + + set(next: T): void { + if (Object.is(this.current, next)) return; + this.current = next; + for (const listener of this.listeners) { + listener(next); + } + } + + subscribe(listener: (value: T) => void): () => void { + this.listeners.add(listener); + listener(this.current); + return () => { + this.listeners.delete(listener); + }; + } +} diff --git a/packages/client-js/src/schema.ts b/packages/client-js/src/schema.ts new file mode 100644 index 0000000..c117443 --- /dev/null +++ b/packages/client-js/src/schema.ts @@ -0,0 +1,3 @@ +// Re-export the wire schema + marker grammar from the source-of-truth package +// so downstream consumers can import everything through this client SDK. +export * from "@tyler-rng/sprite-core-schema"; diff --git a/packages/client-js/src/sprite-player.test.ts b/packages/client-js/src/sprite-player.test.ts new file mode 100644 index 0000000..949e595 --- /dev/null +++ b/packages/client-js/src/sprite-player.test.ts @@ -0,0 +1,91 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { AnimationGraph } from "./animation-graph.js"; +import { SpriteAnimationPlayer } from "./sprite-player.js"; +import type { CharacterManifest } from "./schema.js"; +import type { Ticker } from "./ticker.js"; + +const manifest: CharacterManifest = { + version: 1, + agentId: "agent", + modes: ["headshot"], + stateMap: { idle: "idle", wink: "wink" }, + content: { + headshot: { + animations: { + idle: { + sequence: { + frames: [{ ref: "idle.00" }, { ref: "idle.01" }], + fps: 60, + loop: "infinite", + }, + }, + wink: { + sequence: { + frames: [{ ref: "wink.00" }, { ref: "wink.01" }], + fps: 60, + loop: "once", + holdLastFrame: true, + }, + }, + }, + }, + }, + assets: { + refs: { + "idle.00": "p/idle_00", + "idle.01": "p/idle_01", + "wink.00": "p/wink_00", + "wink.01": "p/wink_01", + }, + }, +}; + +/** Ticker whose `delay` resolves immediately — tests just walk state. */ +class ImmediateTicker implements Ticker { + async delay(_ms: number): Promise { + // microtask yield so the player's loop advances + await Promise.resolve(); + } +} + +async function flushMicrotasks(n = 8): Promise { + for (let i = 0; i < n; i++) await Promise.resolve(); +} + +describe("SpriteAnimationPlayer", () => { + let player: SpriteAnimationPlayer | null = null; + afterEach(async () => { + if (player) await player.dispose(); + player = null; + }); + + it("starts on the default state and emits the first frame", async () => { + const graph = AnimationGraph.fromManifest(manifest, "headshot"); + player = new SpriteAnimationPlayer(graph, new ImmediateTicker()); + await flushMicrotasks(); + expect(player.currentState.value).toBe("idle"); + expect(player.currentRef.value?.ref).toMatch(/^idle\./); + }); + + it("requestState switches state", async () => { + const graph = AnimationGraph.fromManifest(manifest, "headshot"); + player = new SpriteAnimationPlayer(graph, new ImmediateTicker()); + await flushMicrotasks(); + await player.requestState("wink"); + await flushMicrotasks(); + expect(player.currentState.value).toBe("wink"); + }); + + it("requestState with playCount replays even when already in state", async () => { + const graph = AnimationGraph.fromManifest(manifest, "headshot"); + player = new SpriteAnimationPlayer(graph, new ImmediateTicker()); + await flushMicrotasks(); + await player.requestState("wink", 1); + await flushMicrotasks(); + expect(player.currentState.value).toBe("wink"); + // Second call with the same state+count should not error or hang. + await player.requestState("wink", 1); + await flushMicrotasks(); + expect(player.currentState.value).toBe("wink"); + }); +}); diff --git a/packages/client-js/src/sprite-player.ts b/packages/client-js/src/sprite-player.ts new file mode 100644 index 0000000..68efd1f --- /dev/null +++ b/packages/client-js/src/sprite-player.ts @@ -0,0 +1,260 @@ +import type { + Animation, + FrameRef, + FrameSequence, + LoopMode, +} from "./schema.js"; +import { + AnimationGraph, + effectiveLoop, + resolveTransition, + type Phase, +} from "./animation-graph.js"; +import { type Observable, MutableObservable } from "./observable.js"; +import { SystemTicker, type Ticker } from "./ticker.js"; + +const MIN_FRAME_DELAY_MS = 16; + +class CancelledError extends Error { + constructor() { + super("cancelled"); + this.name = "CancelledError"; + } +} + +/** + * Platform-independent playback engine. One instance per character per mode. + * Drives `currentRef` forward over time according to the `AnimationGraph`'s + * animations and transitions; callers materialize frames via their own + * `FrameSource`. + * + * Mirrors the Kotlin `SpriteAnimationPlayer`. Thread/task safety: `requestState` + * is safe to call from any context; internal transitions cancel the previous + * playback via an `AbortController` before the new one starts. + */ +export class SpriteAnimationPlayer { + private readonly ticker: Ticker; + private readonly _currentRef = new MutableObservable(null); + private readonly _currentState: MutableObservable; + private abortController: AbortController | null = null; + private runningTask: Promise | null = null; + + readonly currentRef: Observable = this._currentRef; + readonly currentState: Observable; + + constructor(private readonly graph: AnimationGraph, ticker: Ticker = new SystemTicker()) { + this.ticker = ticker; + this._currentState = new MutableObservable(graph.defaultState); + this.currentState = this._currentState; + // Start playing the default state (entering=true so intros fire). + this.runningTask = this.spawn((signal) => this.playState(graph.defaultState, true, null, signal)); + } + + /** + * Request a state change. If the graph's transitions table has a match for + * `currentState → target`, that transition plays once before the target + * state's own loop starts. + * + * `playCount` semantics (from `<<>>`): + * - null or 0 — loop indefinitely until the next `requestState` + * - N >= 1 — play the loop phase exactly N times, then hold the last + * frame indefinitely. Intro (if any) still plays once. + * + * When `playCount` is non-null we always replay — even when `target` is + * already the current state — so a model emitting the same marker twice in + * a row visibly replays the animation instead of being swallowed as a no-op. + */ + async requestState(target: string, playCount: number | null = null): Promise { + const previousState = this._currentState.value; + const sameState = target === previousState; + const effectiveCount = playCount !== null && playCount > 0 ? playCount : null; + + // Cancel whatever was running. + await this.cancelRunning(); + + if (sameState && effectiveCount === null) { + return; + } + + this.runningTask = this.spawn(async (signal) => { + if (!sameState) { + const transition = this.graph.resolveTransition(previousState, target); + if (typeof transition === "string") { + const { animation, phase } = resolveTransition(transition); + await this.playPhase(animation, phase, "once", signal); + } + // Crossfade transitions are a rendering-side concern the consumer + // applies when the ref changes; the player just snaps through. + } + await this.playState(target, !sameState, effectiveCount, signal); + }); + } + + /** Cancel playback and release internal resources. */ + async dispose(): Promise { + await this.cancelRunning(); + } + + // --- internals --- + + private spawn(body: (signal: AbortSignal) => Promise): Promise { + const controller = new AbortController(); + this.abortController = controller; + const task = (async () => { + try { + await body(controller.signal); + } catch (err) { + if (!(err instanceof CancelledError)) { + throw err; + } + } + })(); + return task; + } + + private async cancelRunning(): Promise { + const prev = this.runningTask; + const prevController = this.abortController; + this.runningTask = null; + this.abortController = null; + if (prevController) prevController.abort(); + if (prev) { + try { + await prev; + } catch { + /* swallow */ + } + } + } + + private async playState( + state: string, + entering: boolean, + playCountOverride: number | null, + signal: AbortSignal, + ): Promise { + this._currentState.set(state); + const anim = this.graph.animations[state]; + if (!anim) return; + if (entering && anim.intro) { + await this.playPhase(state, "intro", null, signal); + } + if (playCountOverride !== null && playCountOverride >= 1) { + await this.playPhaseFinite(state, "loop", playCountOverride, signal); + return; + } + // Flat states fall through to `effectiveLoop`; phased states play `loop`. + // `outro` fires only via requestState() transitions. + await this.playPhase(state, "loop", null, signal); + } + + private async playPhaseFinite( + animName: string, + phase: Phase, + times: number, + signal: AbortSignal, + ): Promise { + const anim = this.graph.animations[animName]; + if (!anim) return; + const seq = pickPhase(anim, phase); + if (!seq || seq.frames.length === 0) return; + const frameDelayMs = Math.max(Math.floor(1000 / seq.fps), MIN_FRAME_DELAY_MS); + for (let round = 0; round < times; round++) { + for (const ref of seq.frames) { + this.throwIfCancelled(signal); + this._currentRef.set(ref); + await this.delayCancellable(frameDelayMs, signal); + } + } + this._currentRef.set(seq.frames[seq.frames.length - 1] ?? null); + await awaitCancellation(signal); + } + + private async playPhase( + animName: string, + phase: Phase, + loopOverride: LoopMode | null, + signal: AbortSignal, + ): Promise { + const anim = this.graph.animations[animName]; + if (!anim) return; + const seq = pickPhase(anim, phase); + if (!seq || seq.frames.length === 0) return; + const frameDelayMs = Math.max(Math.floor(1000 / seq.fps), MIN_FRAME_DELAY_MS); + const loop: LoopMode = loopOverride ?? seq.loop; + + if (loop === "once") { + for (const ref of seq.frames) { + this.throwIfCancelled(signal); + this._currentRef.set(ref); + await this.delayCancellable(frameDelayMs, signal); + } + if (!seq.holdLastFrame) { + this._currentRef.set(null); + } + return; + } + + if (loop === "ping-pong") { + const cap = seq.iterations ?? Number.MAX_SAFE_INTEGER; + let rounds = 0; + while (rounds < cap) { + for (const ref of seq.frames) { + this.throwIfCancelled(signal); + this._currentRef.set(ref); + await this.delayCancellable(frameDelayMs, signal); + } + for (let i = seq.frames.length - 2; i >= 1; i--) { + this.throwIfCancelled(signal); + const ref = seq.frames[i]; + if (ref !== undefined) this._currentRef.set(ref); + await this.delayCancellable(frameDelayMs, signal); + } + rounds++; + } + return; + } + + // infinite + for (;;) { + for (const ref of seq.frames) { + this.throwIfCancelled(signal); + this._currentRef.set(ref); + await this.delayCancellable(frameDelayMs, signal); + } + } + } + + private throwIfCancelled(signal: AbortSignal): void { + if (signal.aborted) throw new CancelledError(); + } + + private async delayCancellable(ms: number, signal: AbortSignal): Promise { + if (signal.aborted) throw new CancelledError(); + await Promise.race([ + this.ticker.delay(ms), + new Promise((_, reject) => { + const onAbort = () => reject(new CancelledError()); + if (signal.aborted) onAbort(); + else signal.addEventListener("abort", onAbort, { once: true }); + }), + ]); + if (signal.aborted) throw new CancelledError(); + } +} + +function pickPhase(anim: Animation, phase: Phase): FrameSequence | undefined { + if (phase === "intro") return anim.intro; + if (phase === "outro") return anim.outro; + return effectiveLoop(anim); +} + +function awaitCancellation(signal: AbortSignal): Promise { + return new Promise((_, reject) => { + if (signal.aborted) { + reject(new CancelledError()); + return; + } + signal.addEventListener("abort", () => reject(new CancelledError()), { once: true }); + }); +} diff --git a/packages/client-js/src/ticker.ts b/packages/client-js/src/ticker.ts new file mode 100644 index 0000000..08a19eb --- /dev/null +++ b/packages/client-js/src/ticker.ts @@ -0,0 +1,14 @@ +/** + * Timing abstraction for frame advancement. The default implementation uses + * `setTimeout`; tests inject a fake ticker that advances virtual time. The + * Kotlin + Swift ports expose the same seam. + */ +export interface Ticker { + delay(ms: number): Promise; +} + +export class SystemTicker implements Ticker { + delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } +} diff --git a/packages/client-js/tsconfig.json b/packages/client-js/tsconfig.json new file mode 100644 index 0000000..7dde006 --- /dev/null +++ b/packages/client-js/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "esModuleInterop": true, + "skipLibCheck": true, + "isolatedModules": true, + "declaration": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["./src/**/*.ts"], + "exclude": ["./dist/**", "./node_modules/**", "./src/**/*.test.ts"] +} diff --git a/packages/client-js/vitest.config.ts b/packages/client-js/vitest.config.ts new file mode 100644 index 0000000..ce36a74 --- /dev/null +++ b/packages/client-js/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["src/**/*.test.ts"], + environment: "node", + }, +}); diff --git a/packages/client-kotlin/README.md b/packages/client-kotlin/README.md new file mode 100644 index 0000000..f5bd58d --- /dev/null +++ b/packages/client-kotlin/README.md @@ -0,0 +1,80 @@ +# sprite-core-client (Kotlin) + +Kotlin client kit for the SpriteCore plugin. Two modules: + +- **`:core`** — pure JVM. `CharacterManifest` data classes, `AnimationGraph`, + `SpriteAnimationPlayer`, `FrameSource`, marker parser, `AgentAvatarSource`. + Works on any JVM target including Android, Wear OS, desktop, or a + JVM server. +- **`:android`** — Android library. `BitmapFrameSource` bridges the core kit + to Android `Bitmap`, decoded via `BitmapFactory`. + +Artifact coordinates (after publish): + +``` +ai.openclaw.spritecore:sprite-core-client:1.0.0 +ai.openclaw.spritecore:sprite-core-client-android:1.0.0 +``` + +## Consuming locally (Gradle composite build) + +From a consuming Gradle project (e.g. an Android app): + +```kotlin +// settings.gradle.kts +includeBuild("../sprite-core/packages/client-kotlin") + +// app/build.gradle.kts +dependencies { + implementation("ai.openclaw.spritecore:sprite-core-client") + implementation("ai.openclaw.spritecore:sprite-core-client-android") +} +``` + +This avoids publishing during active development — Gradle resolves the +modules directly from the checked-out path. + +## Publishing + +To publish snapshots / releases to a Maven registry (GitHub Packages or +Maven Central), configure the registry URL + credentials in +`~/.gradle/gradle.properties` or via CI environment: + +``` +./gradlew :core:publish :android:publish +``` + +Registry coordinates are configurable via `-Pregistry=` in the Gradle +invocation or through CI secrets. See the repo-root `README.md` for the +build/publish conversation in progress. + +## Layout + +``` +packages/client-kotlin/ +├── settings.gradle.kts +├── build.gradle.kts ← plugin version pins +├── gradle.properties +├── core/ +│ ├── build.gradle.kts +│ └── src/main/kotlin/ai/openclaw/spritecore/client/ +│ ├── CharacterManifest.kt ← wire types + JSON parser + ready check +│ ├── AnimationGraph.kt ← projection + transition resolver +│ ├── SpriteAnimationPlayer.kt ← coroutine-driven state machine +│ ├── FrameSource.kt ← platform adapter interface +│ ├── Ticker.kt ← timing abstraction +│ ├── AvatarMarkerParser.kt ← `<<>>` / `<<>>` parser +│ └── AgentAvatarSource.kt ← manifest + asset cache +└── android/ + └── src/main/kotlin/ai/openclaw/spritecore/client/android/ + └── BitmapFrameSource.kt ← `FrameSource` via BitmapFactory +``` + +## Conformance + +Both modules' logic is validated against the fixtures at `../../fixtures/`. +When the wire schema at `../../schema/` changes, regenerate + rerun: + +``` +./gradlew :core:test :android:test +``` diff --git a/packages/client-kotlin/android/build.gradle.kts b/packages/client-kotlin/android/build.gradle.kts new file mode 100644 index 0000000..88e99bb --- /dev/null +++ b/packages/client-kotlin/android/build.gradle.kts @@ -0,0 +1,70 @@ +plugins { + id("com.android.library") + kotlin("android") + kotlin("plugin.serialization") + `maven-publish` +} + +group = "ai.openclaw.spritecore" +version = findProperty("version")?.toString() ?: "1.0.0" + +android { + namespace = "ai.openclaw.spritecore.client.android" + compileSdk = 36 + + defaultConfig { + // Lowest common denominator across phone (31) and wear (30) consumers. + minSdk = 30 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + sourceSets { + getByName("main") { + java.setSrcDirs(listOf("src/main/kotlin")) + } + } + + publishing { + singleVariant("release") { + withSourcesJar() + } + } +} + +kotlin { + jvmToolchain(17) + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) + } +} + +dependencies { + api(project(":core")) + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0") +} + +afterEvaluate { + publishing { + publications { + create("release") { + from(components["release"]) + artifactId = "sprite-core-client-android" + pom { + name.set("SpriteCore Client (Android)") + description.set("Bitmap-backed FrameSource for the SpriteCore client kit on Android / Wear OS") + url.set("https://github.com/Tyler-RNG/sprite-core") + licenses { + license { + name.set("MIT License") + url.set("https://opensource.org/licenses/MIT") + } + } + } + } + } + } +} diff --git a/packages/client-kotlin/android/src/main/AndroidManifest.xml b/packages/client-kotlin/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/packages/client-kotlin/android/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/packages/client-kotlin/android/src/main/kotlin/ai/openclaw/spritecore/client/android/BitmapFrameSource.kt b/packages/client-kotlin/android/src/main/kotlin/ai/openclaw/spritecore/client/android/BitmapFrameSource.kt new file mode 100644 index 0000000..6b09db1 --- /dev/null +++ b/packages/client-kotlin/android/src/main/kotlin/ai/openclaw/spritecore/client/android/BitmapFrameSource.kt @@ -0,0 +1,76 @@ +package ai.openclaw.spritecore.client.android + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.util.Log +import ai.openclaw.spritecore.client.FrameRef +import ai.openclaw.spritecore.client.FrameSource + +/** + * Bridges the pure-JVM client kit to the Android `Bitmap` world. Given a + * [CharacterManifest] and the raw bytes for each `assets.refs` entry, + * [BitmapFrameSource] resolves any [FrameRef] the player emits to a + * concrete [Bitmap]: + * + * - **Sprite-style** frames reference a whole-image asset by key; the full + * decoded bitmap is returned. + * - **Atlas-style** frames reference the atlas image and carry an + * `x/y/w/h` crop rect; the returned bitmap is a + * `createBitmap(src, x, y, w, h)` slice of the decoded atlas, cached per + * `(ref, rect)` pair. + * + * Parsing + ready-check helpers live on the pure-JVM core + * ([ai.openclaw.spritecore.client.CharacterManifestJson], + * [ai.openclaw.spritecore.client.characterManifestBytesReady]). + */ +class BitmapFrameSource( + private val bytesByRef: Map, +) : FrameSource { + private val decoded = mutableMapOf() + private val sliceCache = mutableMapOf() + + override fun frame(ref: FrameRef): Bitmap? { + val whole = decodedFor(ref.ref) ?: return null + if (ref.x == null && ref.y == null && ref.w == null && ref.h == null) { + return whole + } + val key = "${ref.ref}@${ref.x},${ref.y},${ref.w},${ref.h}" + sliceCache[key]?.let { return it } + val x = ref.x ?: 0 + val y = ref.y ?: 0 + val w = ref.w ?: (whole.width - x) + val h = ref.h ?: (whole.height - y) + if (w <= 0 || h <= 0 || x < 0 || y < 0 || x + w > whole.width || y + h > whole.height) { + Log.w( + TAG, + "slice out of bounds ref=${ref.ref} rect=($x,$y,$w,$h) size=(${whole.width},${whole.height})", + ) + return null + } + return try { + val slice = Bitmap.createBitmap(whole, x, y, w, h) + sliceCache[key] = slice + slice + } catch (e: Throwable) { + Log.w(TAG, "slice failed for $key", e) + null + } + } + + private fun decodedFor(refKey: String): Bitmap? { + decoded[refKey]?.let { return it } + val bytes = bytesByRef[refKey] ?: return null + return try { + val bm = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) + if (bm != null) decoded[refKey] = bm + bm + } catch (e: Throwable) { + Log.w(TAG, "decode failed for $refKey", e) + null + } + } + + companion object { + private const val TAG = "BitmapFrameSource" + } +} diff --git a/packages/client-kotlin/build.gradle.kts b/packages/client-kotlin/build.gradle.kts new file mode 100644 index 0000000..8e220ce --- /dev/null +++ b/packages/client-kotlin/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + kotlin("jvm") version "2.0.20" apply false + kotlin("plugin.serialization") version "2.0.20" apply false + id("com.android.library") version "8.6.0" apply false +} + +// Per-module config lives in core/build.gradle.kts and android/build.gradle.kts. +// This root exists only to pin plugin versions. diff --git a/packages/client-kotlin/core/build.gradle.kts b/packages/client-kotlin/core/build.gradle.kts new file mode 100644 index 0000000..2178d0d --- /dev/null +++ b/packages/client-kotlin/core/build.gradle.kts @@ -0,0 +1,54 @@ +plugins { + kotlin("jvm") + kotlin("plugin.serialization") + `maven-publish` +} + +group = "ai.openclaw.spritecore" +version = findProperty("version")?.toString() ?: "1.0.0" + +kotlin { + jvmToolchain(17) + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) + } +} + +dependencies { + api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") + api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0") + + testImplementation(kotlin("test")) + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2") +} + +tasks.test { + useJUnitPlatform() +} + +java { + withSourcesJar() + withJavadocJar() +} + +publishing { + publications { + create("maven") { + from(components["java"]) + artifactId = "sprite-core-client" + pom { + name.set("SpriteCore Client (Kotlin core)") + description.set("Pure-JVM client kit for the SpriteCore plugin — animation graph + sprite player") + url.set("https://github.com/Tyler-RNG/sprite-core") + licenses { + license { + name.set("MIT License") + url.set("https://opensource.org/licenses/MIT") + } + } + } + } + } + // Target registry is configured via -Pregistry= or env in CI; see + // packages/client-kotlin/README.md. +} diff --git a/packages/client-kotlin/core/src/main/kotlin/ai/openclaw/spritecore/client/AgentAvatarSource.kt b/packages/client-kotlin/core/src/main/kotlin/ai/openclaw/spritecore/client/AgentAvatarSource.kt new file mode 100644 index 0000000..c7c5215 --- /dev/null +++ b/packages/client-kotlin/core/src/main/kotlin/ai/openclaw/spritecore/client/AgentAvatarSource.kt @@ -0,0 +1,172 @@ +package ai.openclaw.spritecore.client + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import java.util.concurrent.atomic.AtomicLong + +/** + * Client-side unified fetcher + cache for per-agent CharacterManifest + * envelopes and their asset bytes. Pure JVM — no Android deps — so any + * Kotlin client (wearable, phone, desktop, JVM server) can use it. + * + * Fetch policy is explicit: callers invoke [refresh] with an agent-id list. + * An agent already present at the same manifest revision is left alone; + * revision bumps trigger a re-fetch of the asset refs. + * + * This is the Kotlin mirror of `@tyler-rng/sprite-core-client`'s + * `AssetSource`. The two stay behaviourally identical — the conformance + * suite in `fixtures/` at the repo root enforces it. + */ +class AgentAvatarSource( + private val scope: CoroutineScope, + private val fetchManifest: suspend (agentId: String) -> CharacterManifestEnvelope?, + private val fetchAsset: suspend (relativePath: String) -> ByteArray?, + private val logger: (level: LogLevel, tag: String, msg: String) -> Unit = { _, _, _ -> }, +) { + enum class LogLevel { DEBUG, WARN } + + private val _characterManifests = + MutableStateFlow>(emptyMap()) + val characterManifests: StateFlow> = + _characterManifests.asStateFlow() + + private val _characterAssets = + MutableStateFlow>>(emptyMap()) + val characterAssets: StateFlow>> = + _characterAssets.asStateFlow() + + private val _agentStates = MutableStateFlow>(emptyMap()) + val agentStates: StateFlow> = _agentStates.asStateFlow() + + private val _agentMarkerSignals = MutableStateFlow>(emptyMap()) + val agentMarkerSignals: StateFlow> = + _agentMarkerSignals.asStateFlow() + + private val signalVersionSeq = AtomicLong(0L) + private val fetchMutex = Mutex() + + /** + * Kick off a background fetch for each agent. No-ops for agents whose + * manifest is already cached at the current revision. Returns immediately; + * the flows update as results land. + */ + fun refresh(agentIds: List) { + if (agentIds.isEmpty()) return + scope.launch { + fetchMutex.withLock { + for (agentId in agentIds) { + refreshOne(agentId) + } + } + } + } + + /** + * Update the current state for an agent. Called by the chat-reply path + * when an `<<>>` or `<<>>` marker fires. + */ + fun setAgentState(agentId: String, stateName: String, count: Int? = null) { + _agentStates.update { it + (agentId to stateName) } + val signal = AvatarMarkerSignal( + state = stateName, + count = count, + version = signalVersionSeq.incrementAndGet(), + ) + _agentMarkerSignals.update { it + (agentId to signal) } + } + + /** Snapshot of the current cache for callers to iterate. */ + fun snapshot(): List { + val manifests = _characterManifests.value + val assets = _characterAssets.value + return manifests.map { (agentId, envelope) -> + CachedAgent(agentId = agentId, envelope = envelope, assetBytes = assets[agentId].orEmpty()) + } + } + + /** Drop any cached entries for agents no longer in [keepIds]. */ + fun retainOnly(keepIds: Collection) { + val keep = keepIds.toSet() + _characterManifests.update { it.filterKeys { id -> id in keep } } + _characterAssets.update { it.filterKeys { id -> id in keep } } + _agentStates.update { it.filterKeys { id -> id in keep } } + } + + fun clear() { + _characterManifests.update { emptyMap() } + _characterAssets.update { emptyMap() } + _agentStates.update { emptyMap() } + } + + /** + * Resolve the default state name for [agentId] from its cached manifest. + * Mirrors [AnimationGraph.fromManifest] default-state logic so the two + * never drift. + */ + fun defaultStateFor(agentId: String): String? { + val envelope = _characterManifests.value[agentId] ?: return null + val manifest = envelope.manifest + val mode = manifest.modes.firstOrNull { manifest.content.containsKey(it) } ?: return null + val animations = manifest.content[mode]?.animations ?: return null + val firstFromMap = manifest.stateMap.entries.firstOrNull { animations.containsKey(it.value) } + if (firstFromMap != null) return firstFromMap.value + return animations.keys.firstOrNull() + } + + // --- internals --- + + private suspend fun refreshOne(agentId: String) { + val envelope = fetchManifest(agentId) ?: run { + logger(LogLevel.DEBUG, TAG, "manifest skip $agentId (no structured avatar or RPC failed)") + return + } + val existing = _characterManifests.value[agentId] + if (existing != null && existing.revision == envelope.revision) { + return + } + _characterManifests.update { it + (agentId to envelope) } + + val bytesByRef = mutableMapOf() + for ((refKey, relPath) in envelope.manifest.assets.refs) { + val bytes = fetchAsset(relPath) + if (bytes != null) { + bytesByRef[refKey] = bytes + } else { + logger(LogLevel.WARN, TAG, "asset fetch failed $agentId $refKey") + } + } + _characterAssets.update { it + (agentId to bytesByRef) } + logger( + LogLevel.DEBUG, + TAG, + "cached $agentId rev=${envelope.revision} (${bytesByRef.size}/${envelope.manifest.assets.refs.size} assets)", + ) + } + + data class CachedAgent( + val agentId: String, + val envelope: CharacterManifestEnvelope, + val assetBytes: Map, + ) + + /** + * Versioned per-agent animation signal. [version] bumps on every + * [setAgentState] call so UI consumers keyed on the signal re-trigger + * their effects even when the state name is unchanged. + */ + data class AvatarMarkerSignal( + val state: String, + val count: Int?, + val version: Long, + ) + + companion object { + private const val TAG = "AgentAvatarSource" + } +} diff --git a/packages/client-kotlin/core/src/main/kotlin/ai/openclaw/spritecore/client/AnimationGraph.kt b/packages/client-kotlin/core/src/main/kotlin/ai/openclaw/spritecore/client/AnimationGraph.kt new file mode 100644 index 0000000..6730229 --- /dev/null +++ b/packages/client-kotlin/core/src/main/kotlin/ai/openclaw/spritecore/client/AnimationGraph.kt @@ -0,0 +1,100 @@ +package ai.openclaw.spritecore.client + +/** + * Resolved animation table + transition graph for a single mode of a single + * character. Both sprite and atlas manifests project into this shape so the + * player stays format-agnostic. + * + * Build via [fromManifest] to pull a mode's content out of a server-synthesized + * [CharacterManifest], or construct directly for tests. + */ +data class AnimationGraph( + val defaultState: String, + val animations: Map, + val transitions: Map, +) { + /** + * Resolve a state→state transition against the transitions table using + * wildcard pattern matching. Specificity order (most→least specific): + * + * "->" → "->*" → "*->" → "*->*" + * + * Returns null when nothing matches; the caller then swaps instantly. + */ + fun resolveTransition(from: String, to: String): TransitionRef? { + val keys = listOf("$from->$to", "$from->*", "*->$to", "*->*") + for (k in keys) { + transitions[k]?.let { return it } + } + return null + } + + companion object { + /** + * Extract a single mode's animation graph from a character manifest. + * The default state is taken from [stateMap] — the first key that maps + * to an animation present in [mode]'s content — or fails if no + * animation is present. + */ + fun fromManifest(manifest: CharacterManifest, mode: String): AnimationGraph { + val content = manifest.content[mode] + ?: throw IllegalArgumentException( + "manifest has no content for mode '$mode'. Available: ${manifest.content.keys}", + ) + val default = resolveDefaultState(manifest.stateMap, content.animations) + return AnimationGraph( + defaultState = default, + animations = content.animations, + transitions = content.transitions ?: emptyMap(), + ) + } + + private fun resolveDefaultState( + stateMap: Map, + animations: Map, + ): String { + // stateMap maps agent-state → animation name. Prefer the first + // agent-state whose animation exists in this mode; otherwise fall + // back to any animation name we have. + val firstFromMap = stateMap.entries.firstOrNull { animations.containsKey(it.value) } + if (firstFromMap != null) { + return firstFromMap.value + } + return animations.keys.firstOrNull() + ?: throw IllegalArgumentException("manifest mode has no animations") + } + } +} + +/** + * A transition target resolved for playback: which animation + phase to play + * once before entering the target state's own loop. Used by the player when a + * [TransitionRef.Phase] fires on state change. + */ +data class ResolvedTransition(val animation: String, val phase: Phase) { + companion object { + /** Parse `"thinking.intro"` into `(thinking, intro)`. Unqualified → loop. */ + fun parse(ref: String): ResolvedTransition { + val dot = ref.indexOf('.') + return if (dot < 0) { + ResolvedTransition(ref, Phase.LOOP) + } else { + ResolvedTransition(ref.substring(0, dot), Phase.fromWire(ref.substring(dot + 1))) + } + } + } +} + +/** The three phases of a phased animation; flat animations use [LOOP]. */ +enum class Phase(val wire: String) { + INTRO("intro"), + LOOP("loop"), + OUTRO("outro"), + ; + + companion object { + fun fromWire(value: String): Phase = + entries.firstOrNull { it.wire == value } + ?: throw IllegalArgumentException("unknown phase: $value") + } +} diff --git a/packages/client-kotlin/core/src/main/kotlin/ai/openclaw/spritecore/client/AvatarMarkerParser.kt b/packages/client-kotlin/core/src/main/kotlin/ai/openclaw/spritecore/client/AvatarMarkerParser.kt new file mode 100644 index 0000000..e7e7585 --- /dev/null +++ b/packages/client-kotlin/core/src/main/kotlin/ai/openclaw/spritecore/client/AvatarMarkerParser.kt @@ -0,0 +1,227 @@ +package ai.openclaw.spritecore.client + +/** + * Kotlin port of `src/gateway/avatar-marker-parser.ts`. Recognizes the + * inline escape `<<>>` anywhere in assistant text — not restricted to + * its own line. Matching markers are stripped from the visible text and + * surfaced separately; invalid marker shapes (empty or disallowed state + * names) are emitted verbatim so nothing is silently lost. + * + * The triple-angle-bracket escape is deliberately unusual so the model is + * unlikely to emit it by accident. Prior syntax was `[avatar:state]` on its + * own line; the new syntax permits inline emotion changes mid-utterance so + * TTS and avatar state can switch at each marker boundary. + * + * The parser is stateful across pushes: a marker split mid-token across two + * chunks is still recognized. Non-marker content is emitted immediately when + * possible so streaming UX isn't delayed. + * + * Lives at `ai.openclaw.spritecore.client` — both the phone voice path and the wear + * relay path consume this. Keep in sync with the TS reference. + */ + +const val AVATAR_MARKER_OPEN = "<<<" +const val AVATAR_MARKER_CLOSE = ">>>" + +/** + * One parsed `<<>>` / `<<>>` marker. + * + * [count] semantics forwarded from the wire format: + * null — bare `<<>>`, defaults to "loop until next marker" client-side. + * 0 — explicit loop (same as bare). + * N >= 1 — play animation N times and hold on the last frame. + */ +data class AvatarMarker(val state: String, val count: Int? = null) + +data class AvatarParseResult( + val cleanedText: String, + val markers: List, +) + +/** + * Text segment produced by [splitByMarkers]. `emotion` is the state name of + * the marker immediately preceding this segment (with any `-N` count suffix + * stripped off into [emotionCount]), or `null` for the leading segment + * (before any marker) and for segments introduced by an invalid marker + * shape (which is emitted as literal text). + */ +data class TextSegmentWithEmotion( + val text: String, + val emotion: String?, + val emotionCount: Int? = null, +) + +private val STATE_NAME_RE = Regex("^[a-zA-Z0-9_-]+$") + +private fun isValidStateName(name: String): Boolean = + name.isNotEmpty() && STATE_NAME_RE.matches(name) + +/** + * Splits a raw marker body into (state, count). Triggers on the *last* dash + * when the suffix is a non-negative integer — `head_cocked_1` (N=1) becomes + * `head_cocked` + 1, but `head-cocked` (no digits after dash) stays as + * `head-cocked` + null. Returns null count when the body is all-state. + */ +internal fun resolveStateAndCount(body: String): Pair { + val dashIdx = body.lastIndexOf('-') + if (dashIdx <= 0 || dashIdx == body.length - 1) return body to null + val countPart = body.substring(dashIdx + 1) + val count = countPart.toIntOrNull() + if (count == null || count < 0) return body to null + val state = body.substring(0, dashIdx) + if (state.isEmpty()) return body to null + return state to count +} + +class AvatarMarkerParser { + private var buffer: String = "" + + fun push(chunk: String): AvatarParseResult { + if (chunk.isEmpty()) return AvatarParseResult("", emptyList()) + val combined = buffer + chunk + val (cleaned, markers, remainder) = processSafePrefix(combined) + buffer = remainder + return AvatarParseResult(cleaned, markers) + } + + fun flush(): AvatarParseResult { + if (buffer.isEmpty()) return AvatarParseResult("", emptyList()) + // End of stream: any still-buffered bytes can no longer become a + // marker. Emit them as literal text. + val leftover = buffer + buffer = "" + return AvatarParseResult(leftover, emptyList()) + } + + fun reset() { + buffer = "" + } +} + +/** + * Convenience: parse a complete (non-streamed) string in one shot. + */ +fun parseAvatarMarkers(text: String): AvatarParseResult { + val parser = AvatarMarkerParser() + val a = parser.push(text) + val b = parser.flush() + if (b.cleanedText.isEmpty() && b.markers.isEmpty()) return a + return AvatarParseResult( + cleanedText = a.cleanedText + b.cleanedText, + markers = a.markers + b.markers, + ) +} + +/** + * Split [text] into segments delimited by `<<>>` markers. Each + * segment carries the preceding marker's state as its [TextSegmentWithEmotion.emotion] + * (null for the leading segment before any marker). + * + * Invalid marker shapes — empty state names or disallowed characters — are + * treated as literal text and merged into the enclosing segment. + * + * Empty-text segments are dropped; a reply of pure markers returns an empty + * list. Callers that need the state of an all-markers reply should read the + * markers through [parseAvatarMarkers] directly. + */ +fun splitByMarkers(text: String): List { + if (text.isEmpty()) return emptyList() + val segments = mutableListOf() + val currentText = StringBuilder() + var currentEmotion: String? = null + var currentEmotionCount: Int? = null + var i = 0 + while (i < text.length) { + val openAt = text.indexOf(AVATAR_MARKER_OPEN, i) + if (openAt == -1) { + currentText.append(text, i, text.length) + break + } + // Accumulate literal text up to the opener. + currentText.append(text, i, openAt) + val closeAt = text.indexOf(AVATAR_MARKER_CLOSE, openAt + AVATAR_MARKER_OPEN.length) + if (closeAt == -1) { + // Unterminated marker — rest of string is literal. + currentText.append(text, openAt, text.length) + break + } + val rawBody = text.substring(openAt + AVATAR_MARKER_OPEN.length, closeAt) + if (isValidStateName(rawBody)) { + val (stateName, stateCount) = resolveStateAndCount(rawBody) + // Close the current segment and start a new one tagged with the + // marker's state. + if (currentText.isNotEmpty()) { + segments.add( + TextSegmentWithEmotion( + currentText.toString(), + currentEmotion, + currentEmotionCount, + ), + ) + currentText.setLength(0) + } + currentEmotion = stateName + currentEmotionCount = stateCount + } else { + // Invalid marker shape — emit verbatim as literal text within + // the current segment. + currentText.append(text, openAt, closeAt + AVATAR_MARKER_CLOSE.length) + } + i = closeAt + AVATAR_MARKER_CLOSE.length + } + if (currentText.isNotEmpty()) { + segments.add( + TextSegmentWithEmotion(currentText.toString(), currentEmotion, currentEmotionCount), + ) + } + return segments +} + +private data class ProcessResult( + val cleanedText: String, + val markers: List, + val remainder: String, +) + +/** + * Process as much of [combined] as possible without consuming a potential + * partial marker at the tail. Returns the clean-output prefix, extracted + * markers, and the byte suffix that might still become a marker when more + * input arrives — callers buffer the remainder until the next push. + */ +private fun processSafePrefix(combined: String): ProcessResult { + val markers = mutableListOf() + val out = StringBuilder() + var i = 0 + while (i < combined.length) { + val openAt = combined.indexOf(AVATAR_MARKER_OPEN, i) + if (openAt == -1) { + // No complete `<<<` left. But the tail might be a partial start + // (`<` or `<<`) that could extend into a marker with more input; + // buffer those trailing `<` characters so the next chunk can + // complete them. + var j = combined.length + while (j > i && combined[j - 1] == '<') { + j -= 1 + } + out.append(combined, i, j) + return ProcessResult(out.toString(), markers, combined.substring(j)) + } + out.append(combined, i, openAt) + val closeAt = combined.indexOf(AVATAR_MARKER_CLOSE, openAt + AVATAR_MARKER_OPEN.length) + if (closeAt == -1) { + // Unterminated marker — buffer everything from the opener onward. + return ProcessResult(out.toString(), markers, combined.substring(openAt)) + } + val rawState = combined.substring(openAt + AVATAR_MARKER_OPEN.length, closeAt) + if (isValidStateName(rawState)) { + val (stateName, stateCount) = resolveStateAndCount(rawState) + markers.add(AvatarMarker(stateName, stateCount)) + } else { + // Invalid marker shape — emit verbatim so nothing is silently lost. + out.append(combined, openAt, closeAt + AVATAR_MARKER_CLOSE.length) + } + i = closeAt + AVATAR_MARKER_CLOSE.length + } + return ProcessResult(out.toString(), markers, "") +} diff --git a/packages/client-kotlin/core/src/main/kotlin/ai/openclaw/spritecore/client/CharacterManifest.kt b/packages/client-kotlin/core/src/main/kotlin/ai/openclaw/spritecore/client/CharacterManifest.kt new file mode 100644 index 0000000..bb5e1d8 --- /dev/null +++ b/packages/client-kotlin/core/src/main/kotlin/ai/openclaw/spritecore/client/CharacterManifest.kt @@ -0,0 +1,217 @@ +package ai.openclaw.spritecore.client + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonContentPolymorphicSerializer +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive + +/** + * Pure-Kotlin mirror of the `CharacterManifest` wire schema served by the + * gateway at `node.getCharacterManifest`. These data classes carry zero + * platform deps — Android, iOS (via Kotlin/Native if ever), tests, and thin + * JVM clients all parse the same bytes. + * + * Source of truth lives in `schema/src/display.ts` at the repo root. This + * Kotlin mirror must stay byte-compatible — the conformance suite in + * `fixtures/` at the repo root proves it. + */ +@Serializable +data class CharacterManifest( + val version: Int, + val agentId: String, + val name: String? = null, + val modes: List, + val stateMap: Map, + val content: Map, + val assets: AssetBundle, + val emotions: Map? = null, +) + +@Serializable +data class ModeContent( + val atlas: AtlasRef? = null, + val animations: Map, + val transitions: Map? = null, +) + +@Serializable +data class AtlasRef( + val image: String, + val size: Size, + val frameSize: Size? = null, +) + +@Serializable +data class Size(val w: Int, val h: Int) + +@Serializable +data class FrameRef( + val ref: String, + val x: Int? = null, + val y: Int? = null, + val w: Int? = null, + val h: Int? = null, +) + +@Serializable +data class FrameSequence( + val frames: List, + val fps: Int, + val loop: LoopMode, + val holdLastFrame: Boolean = false, + val iterations: Int? = null, +) + +@Serializable +data class Animation( + val description: String? = null, + val sequence: FrameSequence? = null, + val intro: FrameSequence? = null, + val loop: FrameSequence? = null, + val outro: FrameSequence? = null, +) { + /** + * Treat a flat sequence as the `loop` phase so the player can always look + * up phases by name without special-casing flat vs phased at every site. + */ + val effectiveLoop: FrameSequence? get() = loop ?: sequence +} + +@Serializable(with = LoopModeSerializer::class) +enum class LoopMode(val wire: String) { + INFINITE("infinite"), + ONCE("once"), + PING_PONG("ping-pong"), + ; + + companion object { + fun fromWire(value: String): LoopMode = + entries.firstOrNull { it.wire == value } + ?: throw IllegalArgumentException("unknown loop mode: $value") + } +} + +private object LoopModeSerializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("LoopMode", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: LoopMode) { + encoder.encodeString(value.wire) + } + + override fun deserialize(decoder: Decoder): LoopMode = LoopMode.fromWire(decoder.decodeString()) +} + +/** + * A transition is either a named phase reference (e.g. `"thinking.intro"`) + * the runtime plays once on state swap, or an inline blend directive the + * runtime applies as a visual effect during the swap. + */ +@Serializable(with = TransitionRefSerializer::class) +sealed class TransitionRef { + @Serializable + data class Phase(val value: String) : TransitionRef() + + @Serializable + data class Crossfade(val blend: String = "crossfade", val ms: Int) : TransitionRef() +} + +private object TransitionRefSerializer : + JsonContentPolymorphicSerializer(TransitionRef::class) { + override fun selectDeserializer(element: JsonElement) = when (element) { + is JsonPrimitive -> PhaseStringSerializer + else -> TransitionRef.Crossfade.serializer() + } +} + +private object PhaseStringSerializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("TransitionRef.Phase", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: TransitionRef.Phase) { + encoder.encodeString(value.value) + } + + override fun deserialize(decoder: Decoder): TransitionRef.Phase = + TransitionRef.Phase(decoder.decodeString()) +} + +@Serializable +data class AssetBundle(val refs: Map) { + /** Look up the asset path for a frame ref. Returns null if the ref is unknown. */ + fun pathFor(ref: FrameRef): String? = refs[ref.ref] +} + +/** + * Per-state emotion entry: only the wire-visible `directive` ships to clients. + * (Prompt-visible descriptions are server-only — see `schema/src/display.ts`.) + */ +@Serializable +data class EmotionEntry( + val directive: EmotionDirective? = null, +) + +/** + * Per-emotion TTS voice-directive override. Applied by clients after they + * parse `<<>>` markers out of assistant text — the text segment that + * follows a marker inherits the base TalkDirective merged field-by-field with + * this override. + */ +@Serializable +data class EmotionDirective( + val voiceId: String? = null, + val stability: Double? = null, + val similarity: Double? = null, + val style: Double? = null, + val speakerBoost: Boolean? = null, + val speed: Double? = null, + /** Optional inline audio-tag prefix (e.g. `[happy]`). */ + val audioTag: String? = null, +) + +@Serializable +data class CharacterManifestEnvelope( + val manifest: CharacterManifest, + val revision: Int, +) + +/** + * JSON parser for the envelope published by `node.getCharacterManifest`. + * Lives in core so any JVM client can use it without pulling Android-specific + * JSON helpers. + */ +object CharacterManifestJson { + private val json = Json { ignoreUnknownKeys = true } + + fun parse(text: String): CharacterManifestEnvelope? = try { + json.decodeFromString(CharacterManifestEnvelope.serializer(), text) + } catch (_: Throwable) { + null + } + + /** Pick the first mode in `manifest.modes` whose content is present. */ + fun pickMode(manifest: CharacterManifest): String? = + manifest.modes.firstOrNull { manifest.content.containsKey(it) } +} + +/** + * Returns true when every asset ref declared by `envelope.manifest.assets.refs` + * has bytes in `assetBytes`. Consumers use this to decide whether to render + * (all bytes present, player will find frames) or fall back until bytes + * arrive. Empty `refs` returns true. + */ +fun characterManifestBytesReady( + envelope: CharacterManifestEnvelope, + assetBytes: Map, +): Boolean { + val refs = envelope.manifest.assets.refs.keys + if (refs.isEmpty()) return true + return refs.all { assetBytes.containsKey(it) } +} diff --git a/packages/client-kotlin/core/src/main/kotlin/ai/openclaw/spritecore/client/FrameSource.kt b/packages/client-kotlin/core/src/main/kotlin/ai/openclaw/spritecore/client/FrameSource.kt new file mode 100644 index 0000000..e1f959c --- /dev/null +++ b/packages/client-kotlin/core/src/main/kotlin/ai/openclaw/spritecore/client/FrameSource.kt @@ -0,0 +1,43 @@ +package ai.openclaw.spritecore.client + +/** + * Platform-specific resolver from a [FrameRef] to a concrete renderable + * (e.g. Android `Bitmap`, iOS `UIImage`, a byte array, whatever the caller + * chooses). The kit itself never constructs frames — callers own the pixel + * pipeline and only feed [SpriteAnimationPlayer.currentRef] into their own + * [FrameSource] when rendering. + * + * Atlas sources honor the optional `x/y/w/h` fields on [FrameRef]; sprite + * sources ignore them and treat `ref` as the whole-image key. + */ +fun interface FrameSource { + /** Return the frame for [ref], or null if the ref is unknown. */ + fun frame(ref: FrameRef): FrameT? +} + +/** + * Simple in-memory sprite source: callers prime a byte-array map, decode + * happens lazily through [decode]. Useful for unit tests and thin clients + * that don't need the platform-specific image types. + */ +class InMemorySpriteSource( + private val decode: (ByteArray) -> FrameT?, +) : FrameSource { + private val bytesByRef = mutableMapOf() + private val cache = mutableMapOf() + + fun put(refKey: String, bytes: ByteArray) { + bytesByRef[refKey] = bytes + cache.remove(refKey) + } + + fun keys(): Set = bytesByRef.keys + + override fun frame(ref: FrameRef): FrameT? { + cache[ref.ref]?.let { return it } + val bytes = bytesByRef[ref.ref] ?: return null + val decoded = decode(bytes) ?: return null + cache[ref.ref] = decoded + return decoded + } +} diff --git a/packages/client-kotlin/core/src/main/kotlin/ai/openclaw/spritecore/client/SpriteAnimationPlayer.kt b/packages/client-kotlin/core/src/main/kotlin/ai/openclaw/spritecore/client/SpriteAnimationPlayer.kt new file mode 100644 index 0000000..ced8e58 --- /dev/null +++ b/packages/client-kotlin/core/src/main/kotlin/ai/openclaw/spritecore/client/SpriteAnimationPlayer.kt @@ -0,0 +1,220 @@ +package ai.openclaw.spritecore.client + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +/** + * Platform-independent playback engine. One instance per character per mode. + * Drives [currentRef] forward over time according to the [AnimationGraph]'s + * animations and transitions; callers materialize frames via their own + * [FrameSource]. + * + * Thread safety: [requestState] is safe to call from any thread. Internal + * state mutations happen on the supplied coroutine scope's dispatcher. + */ +class SpriteAnimationPlayer( + private val graph: AnimationGraph, + private val ticker: Ticker = SystemTicker(), + scope: CoroutineScope? = null, +) { + private val owned = scope == null + private val scope: CoroutineScope = scope ?: CoroutineScope(SupervisorJob() + Dispatchers.Default) + + private val _currentRef = MutableStateFlow(null) + /** The frame the caller should be rendering right now. Null = blank. */ + val currentRef: StateFlow = _currentRef.asStateFlow() + + private val _currentState = MutableStateFlow(graph.defaultState) + /** The agent state the player is currently in (post-transition). */ + val currentState: StateFlow = _currentState.asStateFlow() + + private var activeJob: Job? = null + + init { + activeJob = this.scope.launch { + playState(graph.defaultState, entering = true) + } + } + + /** + * Request a state change. If the [graph]'s transitions table has a match + * for `currentState → target`, that transition plays once before the + * target state's own loop starts. + * + * [playCount] semantics (from the client-parsed `<<>>` marker): + * null or 0 → default: the state's configured loop plays indefinitely + * until the next [requestState] cancels it. + * N >= 1 → play the loop phase exactly N times, then hold the last + * frame indefinitely. Intro (if any) still plays once. + * + * When [playCount] is non-null we always replay — even when [target] is + * already the current state — so a model emitting the same marker twice + * in a row ("<<>> then <<>>") visibly replays the + * animation instead of being swallowed as a no-op. + */ + fun requestState(target: String, playCount: Int? = null): Job { + // Capture the job reference before reassignment. Reading activeJob + // from inside the launched block is a race — by the time the block + // runs, activeJob has been overwritten to `job` itself, so the old + // job (often the init-spawned infinite loop) would never be cancelled. + val previousJob = activeJob + val job = scope.launch { + val sameState = target == _currentState.value + if (sameState && (playCount == null || playCount <= 0)) { + previousJob?.cancelAndJoin() + return@launch + } + val previousState = _currentState.value + previousJob?.cancelAndJoin() + if (!sameState) { + val transition = graph.resolveTransition(previousState, target) + if (transition is TransitionRef.Phase) { + val resolved = ResolvedTransition.parse(transition.value) + playPhase( + animName = resolved.animation, + phase = resolved.phase, + loopOverride = LoopMode.ONCE, + ) + } + // Crossfade transitions are currently played as an instant + // swap; the visual blend is a rendering-side concern the + // consumer applies when the ref changes. + } + playState(target, entering = !sameState, playCountOverride = playCount) + } + activeJob = job + return job + } + + /** Cancel playback and, if we own it, the internal scope. */ + fun dispose() { + activeJob?.cancel() + if (owned) { + scope.cancel() + } + } + + // --- internals --- + + private suspend fun playState( + state: String, + entering: Boolean, + playCountOverride: Int? = null, + ) { + _currentState.value = state + val anim = graph.animations[state] ?: return + if (entering && anim.intro != null) { + playPhase(state, Phase.INTRO) + } + if (playCountOverride != null && playCountOverride >= 1) { + playPhaseFinite(state, Phase.LOOP, playCountOverride) + return + } + // Flat states fall through to `effectiveLoop`; phased states play + // `loop` here. `outro` fires only on requestState() via transitions. + playPhase(state, Phase.LOOP) + } + + /** + * Play the [phase] of [animName] exactly [times] times, then hold the + * last frame indefinitely (until this coroutine is cancelled by the + * next [requestState]). Implements the `<<>>` N>=1 contract: + * play N times and pause on the last frame. + */ + private suspend fun playPhaseFinite( + animName: String, + phase: Phase, + times: Int, + ) { + val anim = graph.animations[animName] ?: return + val seq = when (phase) { + Phase.INTRO -> anim.intro + Phase.LOOP -> anim.effectiveLoop + Phase.OUTRO -> anim.outro + } ?: return + if (seq.frames.isEmpty()) return + val frameDelayMs = (1000L / seq.fps).coerceAtLeast(MIN_FRAME_DELAY_MS) + repeat(times) { + for (ref in seq.frames) { + _currentRef.value = ref + ticker.delay(frameDelayMs) + } + } + _currentRef.value = seq.frames.last() + awaitCancellation() + } + + private suspend fun playPhase( + animName: String, + phase: Phase, + loopOverride: LoopMode? = null, + ) { + val anim = graph.animations[animName] ?: return + val seq = when (phase) { + Phase.INTRO -> anim.intro + Phase.LOOP -> anim.effectiveLoop + Phase.OUTRO -> anim.outro + } ?: return + if (seq.frames.isEmpty()) { + return + } + val frameDelayMs = (1000L / seq.fps).coerceAtLeast(MIN_FRAME_DELAY_MS) + val loop = loopOverride ?: seq.loop + + when (loop) { + LoopMode.ONCE -> { + for (ref in seq.frames) { + _currentRef.value = ref + ticker.delay(frameDelayMs) + } + if (!seq.holdLastFrame) { + _currentRef.value = null + } + } + LoopMode.PING_PONG -> { + val cap = seq.iterations ?: Int.MAX_VALUE + var rounds = 0 + while (rounds < cap) { + for (ref in seq.frames) { + _currentRef.value = ref + ticker.delay(frameDelayMs) + } + for (i in seq.frames.size - 2 downTo 1) { + _currentRef.value = seq.frames[i] + ticker.delay(frameDelayMs) + } + rounds++ + } + } + LoopMode.INFINITE -> { + while (true) { + for (ref in seq.frames) { + _currentRef.value = ref + ticker.delay(frameDelayMs) + } + } + } + } + } + + private suspend fun Job.cancelAndJoin() { + cancel() + try { + join() + } catch (_: Throwable) { + // Cancellation unwinds through here; swallow so caller's flow continues. + } + } + + companion object { + private const val MIN_FRAME_DELAY_MS = 16L + } +} diff --git a/packages/client-kotlin/core/src/main/kotlin/ai/openclaw/spritecore/client/Ticker.kt b/packages/client-kotlin/core/src/main/kotlin/ai/openclaw/spritecore/client/Ticker.kt new file mode 100644 index 0000000..8f54de4 --- /dev/null +++ b/packages/client-kotlin/core/src/main/kotlin/ai/openclaw/spritecore/client/Ticker.kt @@ -0,0 +1,29 @@ +package ai.openclaw.spritecore.client + +import kotlinx.coroutines.delay as coroutineDelay + +/** + * Wall clock + scheduler injected into the player so tests can drive playback + * deterministically without real delays. Production code uses [SystemTicker]. + */ +interface Ticker { + /** Current monotonic time in milliseconds. */ + fun nowMs(): Long + + /** Suspend for [ms]; clamped to >= 0 at the implementation. */ + suspend fun delay(ms: Long) +} + +/** Default production ticker: backed by [System.currentTimeMillis] + coroutine delay. */ +class SystemTicker : Ticker { + override fun nowMs(): Long = System.currentTimeMillis() + override suspend fun delay(ms: Long) { + if (ms > 0L) { + // Aliased import — an unqualified `delay(ms)` resolves to this + // enclosing member and self-recurses until StackOverflowError. + // Tests couldn't catch it because they inject a TestTicker; only + // the production SystemTicker path ever exercises this call. + coroutineDelay(ms) + } + } +} diff --git a/packages/client-kotlin/core/src/test/kotlin/ai/openclaw/spritecore/client/AnimationGraphTest.kt b/packages/client-kotlin/core/src/test/kotlin/ai/openclaw/spritecore/client/AnimationGraphTest.kt new file mode 100644 index 0000000..339dc4a --- /dev/null +++ b/packages/client-kotlin/core/src/test/kotlin/ai/openclaw/spritecore/client/AnimationGraphTest.kt @@ -0,0 +1,118 @@ +package ai.openclaw.spritecore.client + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertIs +import kotlin.test.assertNull + +class AnimationGraphTest { + private fun graph( + transitions: Map = emptyMap(), + ) = AnimationGraph( + defaultState = "neutral", + animations = mapOf("neutral" to flatAnim(), "thinking" to flatAnim(), "happy" to flatAnim()), + transitions = transitions, + ) + + private fun flatAnim(): Animation = Animation( + sequence = FrameSequence(listOf(FrameRef("x")), fps = 12, loop = LoopMode.INFINITE), + ) + + @Test + fun returnsNullWhenNoTransitionMatches() { + assertNull(graph().resolveTransition("neutral", "happy")) + } + + @Test + fun prefersConcreteOverWildcardMatches() { + val g = graph( + transitions = mapOf( + "*->*" to TransitionRef.Phase("wild.loop"), + "*->happy" to TransitionRef.Phase("wildhappy.intro"), + "neutral->*" to TransitionRef.Phase("neutralout.outro"), + "neutral->happy" to TransitionRef.Phase("direct.intro"), + ), + ) + val t = g.resolveTransition("neutral", "happy") + assertIs(t) + assertEquals("direct.intro", t.value) + } + + @Test + fun fromToWildcardBeatsToFromWildcard() { + // Rule: `->*` wins over `*->` when both are set. + val g = graph( + transitions = mapOf( + "*->happy" to TransitionRef.Phase("a.intro"), + "neutral->*" to TransitionRef.Phase("b.outro"), + ), + ) + val t = g.resolveTransition("neutral", "happy") + assertIs(t) + assertEquals("b.outro", t.value) + } + + @Test + fun resolvesBlendTransitionsAsBlendRefs() { + val g = graph( + transitions = mapOf( + "*->happy" to TransitionRef.Crossfade(ms = 150), + ), + ) + val t = g.resolveTransition("neutral", "happy") + assertIs(t) + assertEquals(150, t.ms) + } + + @Test + fun parsesTransitionPhaseRefs() { + assertEquals( + ResolvedTransition("thinking", Phase.INTRO), + ResolvedTransition.parse("thinking.intro"), + ) + assertEquals( + ResolvedTransition("thinking", Phase.LOOP), + ResolvedTransition.parse("thinking"), + ) + } + + @Test + fun fromManifestPullsOutRequestedMode() { + val manifest = CharacterManifest( + version = 1, + agentId = "ginger", + modes = listOf("headshot"), + stateMap = mapOf("neutral" to "neutral"), + content = mapOf( + "headshot" to ModeContent( + animations = mapOf( + "neutral" to flatAnim(), + "happy" to flatAnim(), + ), + transitions = mapOf("*->happy" to TransitionRef.Phase("happy.intro")), + ), + ), + assets = AssetBundle(mapOf("x" to "a/x.png")), + ) + val g = AnimationGraph.fromManifest(manifest, "headshot") + assertEquals("neutral", g.defaultState) + assertEquals(setOf("neutral", "happy"), g.animations.keys) + assertEquals("happy.intro", (g.transitions["*->happy"] as TransitionRef.Phase).value) + } + + @Test + fun fromManifestFailsWhenModeMissing() { + val manifest = CharacterManifest( + version = 1, + agentId = "ginger", + modes = listOf("headshot"), + stateMap = mapOf("neutral" to "neutral"), + content = emptyMap(), + assets = AssetBundle(emptyMap()), + ) + assertFailsWith { + AnimationGraph.fromManifest(manifest, "headshot") + } + } +} diff --git a/packages/client-kotlin/core/src/test/kotlin/ai/openclaw/spritecore/client/AvatarMarkerParserTest.kt b/packages/client-kotlin/core/src/test/kotlin/ai/openclaw/spritecore/client/AvatarMarkerParserTest.kt new file mode 100644 index 0000000..1bc81a8 --- /dev/null +++ b/packages/client-kotlin/core/src/test/kotlin/ai/openclaw/spritecore/client/AvatarMarkerParserTest.kt @@ -0,0 +1,195 @@ +package ai.openclaw.spritecore.client + +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.test.Test + +class AvatarMarkerParserTest { + + @Test + fun oneShotStripsInlineMarker() { + val r = parseAvatarMarkers("Hello <<>> world") + assertEquals("Hello world", r.cleanedText) + assertEquals(listOf(AvatarMarker("happy")), r.markers) + } + + @Test + fun oneShotHandlesMarkerOnOwnLineToo() { + val r = parseAvatarMarkers("Hello\n<<>>\nworld\n") + assertEquals("Hello\n\nworld\n", r.cleanedText) + assertEquals(listOf(AvatarMarker("happy")), r.markers) + } + + @Test + fun passesThroughWithNoMarkers() { + val r = parseAvatarMarkers("no marker here.\nsecond line\n") + assertEquals("no marker here.\nsecond line\n", r.cleanedText) + assertTrue(r.markers.isEmpty()) + } + + @Test + fun handlesMultipleMarkersInSequence() { + val r = parseAvatarMarkers("<<>>A<<>>B<<>>") + assertEquals("AB", r.cleanedText) + assertEquals( + listOf(AvatarMarker("happy"), AvatarMarker("sad"), AvatarMarker("neutral")), + r.markers, + ) + } + + @Test + fun letsMarkerMidSentenceSegmentText() { + val r = parseAvatarMarkers("I feel <<>>about this but <<>>about that.") + assertEquals("I feel about this but about that.", r.cleanedText) + assertEquals(listOf(AvatarMarker("happy"), AvatarMarker("sad")), r.markers) + } + + @Test + fun emitsMarkerAtStreamEndWithoutTrailingText() { + val r = parseAvatarMarkers("Hi <<>>") + assertEquals("Hi ", r.cleanedText) + assertEquals(listOf(AvatarMarker("happy")), r.markers) + } + + @Test + fun preservesPartialTrailingNonMarker() { + val r = parseAvatarMarkers("alpha\nbeta") + assertEquals("alpha\nbeta", r.cleanedText) + assertTrue(r.markers.isEmpty()) + } + + @Test + fun treatsInvalidStateNamesAsLiteral() { + val r = parseAvatarMarkers("<<>> then <<<>>>") + assertEquals("<<>> then <<<>>>", r.cleanedText) + assertTrue(r.markers.isEmpty()) + } + + @Test + fun acceptsDashesAndUnderscoresInStateNames() { + val r = parseAvatarMarkers("<<>>") + assertEquals("", r.cleanedText) + assertEquals(listOf(AvatarMarker("head-cocked_1")), r.markers) + } + + @Test + fun doesNotStripUnterminatedOpenerAtEndOfStream() { + val r = parseAvatarMarkers("hi <<", ">", ">") + val outBuilder = StringBuilder() + val markers = mutableListOf() + for (c in chunks) { + val r = parser.push(c) + outBuilder.append(r.cleanedText) + markers.addAll(r.markers) + } + val end = parser.flush() + outBuilder.append(end.cleanedText) + markers.addAll(end.markers) + assertEquals("", outBuilder.toString()) + assertEquals(listOf(AvatarMarker("happy")), markers) + } + + @Test + fun streamingEmitsNonMarkerTailImmediately() { + val parser = AvatarMarkerParser() + val r = parser.push("hello world") + assertEquals("hello world", r.cleanedText) + assertTrue(r.markers.isEmpty()) + val f = parser.flush() + assertEquals("", f.cleanedText) + assertTrue(f.markers.isEmpty()) + } + + @Test + fun streamingBuffersSingleAngleBracketInCaseItExtendsToMarker() { + val parser = AvatarMarkerParser() + val r1 = parser.push("text <") + assertEquals("text ", r1.cleanedText) + assertTrue(r1.markers.isEmpty()) + val r2 = parser.push("<>>") + assertEquals("", r2.cleanedText) + assertEquals(listOf(AvatarMarker("happy")), r2.markers) + } + + @Test + fun streamingBuffersDoubleAngleBracketInCaseItExtendsToMarker() { + val parser = AvatarMarkerParser() + val r1 = parser.push("text <<") + assertEquals("text ", r1.cleanedText) + assertTrue(r1.markers.isEmpty()) + val r2 = parser.push(">>") + assertEquals("", r2.cleanedText) + assertEquals(listOf(AvatarMarker("happy")), r2.markers) + } + + @Test + fun streamingFlushesUnterminatedOpenerAsLiteral() { + val parser = AvatarMarkerParser() + parser.push("text <<>>") + // After reset, the `<<` buffer was dropped — `>>` alone isn't + // a valid opener so it emits as literal. + assertEquals(">>", r.cleanedText) + assertTrue(r.markers.isEmpty()) + } + + @Test + fun splitByMarkersTagsEachSegmentWithPrecedingMarker() { + val segments = splitByMarkers("Hi <<>>I'm glad, <<>>but also down.") + assertEquals( + listOf( + TextSegmentWithEmotion("Hi ", null), + TextSegmentWithEmotion("I'm glad, ", "happy"), + TextSegmentWithEmotion("but also down.", "sad"), + ), + segments, + ) + } + + @Test + fun splitByMarkersReturnsSingleSegmentWhenNoMarkers() { + val segments = splitByMarkers("plain text") + assertEquals(listOf(TextSegmentWithEmotion("plain text", null)), segments) + } + + @Test + fun splitByMarkersReturnsEmptyListForEmptyInput() { + assertTrue(splitByMarkers("").isEmpty()) + } + + @Test + fun splitByMarkersSkipsEmptySegmentsBetweenAdjacentMarkers() { + val segments = splitByMarkers("<<>><<>>real text") + assertEquals( + listOf(TextSegmentWithEmotion("real text", "sad")), + segments, + ) + } + + @Test + fun splitByMarkersTreatsInvalidMarkersAsLiteralWithinEnclosingSegment() { + val segments = splitByMarkers("prefix <<>> suffix") + assertEquals( + listOf(TextSegmentWithEmotion("prefix <<>> suffix", null)), + segments, + ) + } +} diff --git a/packages/client-kotlin/core/src/test/kotlin/ai/openclaw/spritecore/client/ManifestParseTest.kt b/packages/client-kotlin/core/src/test/kotlin/ai/openclaw/spritecore/client/ManifestParseTest.kt new file mode 100644 index 0000000..5bc10bd --- /dev/null +++ b/packages/client-kotlin/core/src/test/kotlin/ai/openclaw/spritecore/client/ManifestParseTest.kt @@ -0,0 +1,143 @@ +package ai.openclaw.spritecore.client + +import kotlinx.serialization.json.Json +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class ManifestParseTest { + private val json = Json { ignoreUnknownKeys = false } + + @Test + fun parsesMinimalHeadshotManifest() { + val manifest = json.decodeFromString( + """ + { + "version": 1, + "agentId": "ginger", + "modes": ["headshot"], + "stateMap": { "neutral": "neutral" }, + "content": { + "headshot": { + "animations": { + "neutral": { + "sequence": { + "frames": [{ "ref": "neutral" }], + "fps": 12, + "loop": "infinite" + } + } + } + } + }, + "assets": { "refs": { "neutral": "avatars/ginger/neutral.gif" } } + } + """.trimIndent(), + ) + assertEquals("ginger", manifest.agentId) + assertEquals(listOf("headshot"), manifest.modes) + val anim = manifest.content.getValue("headshot").animations.getValue("neutral") + assertEquals(LoopMode.INFINITE, anim.sequence?.loop) + assertEquals("avatars/ginger/neutral.gif", manifest.assets.pathFor(FrameRef("neutral"))) + } + + @Test + fun parsesPhasedAnimationsAndTransitions() { + val manifest = json.decodeFromString( + """ + { + "version": 1, + "agentId": "ginger", + "modes": ["headshot"], + "stateMap": { "thinking": "thinking" }, + "content": { + "headshot": { + "animations": { + "thinking": { + "intro": { "frames": [{ "ref": "a" }], "fps": 24, "loop": "once" }, + "loop": { "frames": [{ "ref": "b" }], "fps": 12, "loop": "infinite" }, + "outro": { "frames": [{ "ref": "c" }], "fps": 24, "loop": "once", "holdLastFrame": true } + } + }, + "transitions": { + "*->thinking": "thinking.intro", + "*->happy": { "blend": "crossfade", "ms": 150 } + } + } + }, + "assets": { "refs": { "a": "p/a", "b": "p/b", "c": "p/c" } } + } + """.trimIndent(), + ) + val thinking = manifest.content.getValue("headshot").animations.getValue("thinking") + assertNotNull(thinking.intro) + assertEquals(24, thinking.intro.fps) + assertEquals(LoopMode.ONCE, thinking.intro.loop) + assertTrue(thinking.outro?.holdLastFrame == true) + + val transitions = manifest.content.getValue("headshot").transitions.orEmpty() + val toThinking = transitions.getValue("*->thinking") + assertIs(toThinking) + assertEquals("thinking.intro", toThinking.value) + + val toHappy = transitions.getValue("*->happy") + assertIs(toHappy) + assertEquals(150, toHappy.ms) + } + + @Test + fun parsesAtlasContent() { + val manifest = json.decodeFromString( + """ + { + "version": 1, + "agentId": "ginger", + "modes": ["headshot"], + "stateMap": { "neutral": "neutral" }, + "content": { + "headshot": { + "atlas": { "image": "atlas.webp", "size": { "w": 1024, "h": 1024 }, "frameSize": { "w": 256, "h": 256 } }, + "animations": { + "neutral": { + "sequence": { + "frames": [ + { "ref": "atlas.webp", "x": 0, "y": 0, "w": 256, "h": 256 } + ], + "fps": 12, + "loop": "infinite" + } + } + } + } + }, + "assets": { "refs": { "atlas.webp": "avatars/ginger/atlas.webp" } } + } + """.trimIndent(), + ) + val headshot = manifest.content.getValue("headshot") + assertNotNull(headshot.atlas) + assertEquals(1024, headshot.atlas.size.w) + assertEquals(256, headshot.atlas.frameSize?.w) + val frame = headshot.animations.getValue("neutral").sequence!!.frames[0] + assertEquals("atlas.webp", frame.ref) + assertEquals(0, frame.x) + assertEquals(256, frame.w) + assertNull(headshot.transitions) + } + + @Test + fun animationEffectiveLoopFallsBackToSequence() { + val flat = Animation( + sequence = FrameSequence(listOf(FrameRef("a")), fps = 12, loop = LoopMode.INFINITE), + ) + assertEquals(flat.sequence, flat.effectiveLoop) + + val phased = Animation( + loop = FrameSequence(listOf(FrameRef("a")), fps = 12, loop = LoopMode.INFINITE), + ) + assertEquals(phased.loop, phased.effectiveLoop) + } +} diff --git a/packages/client-kotlin/core/src/test/kotlin/ai/openclaw/spritecore/client/SpriteAnimationPlayerTest.kt b/packages/client-kotlin/core/src/test/kotlin/ai/openclaw/spritecore/client/SpriteAnimationPlayerTest.kt new file mode 100644 index 0000000..965ee4d --- /dev/null +++ b/packages/client-kotlin/core/src/test/kotlin/ai/openclaw/spritecore/client/SpriteAnimationPlayerTest.kt @@ -0,0 +1,235 @@ +package ai.openclaw.spritecore.client + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +@OptIn(ExperimentalCoroutinesApi::class) +class SpriteAnimationPlayerTest { + // ---- helpers ---- + + private fun seq( + count: Int, + loop: LoopMode = LoopMode.INFINITE, + fps: Int = 10, // 100ms / frame → easy to reason about under advanceTimeBy + holdLastFrame: Boolean = false, + iterations: Int? = null, + ) = FrameSequence( + frames = (0 until count).map { FrameRef("f$it") }, + fps = fps, + loop = loop, + holdLastFrame = holdLastFrame, + iterations = iterations, + ) + + private fun anim( + sequence: FrameSequence? = null, + intro: FrameSequence? = null, + loop: FrameSequence? = null, + outro: FrameSequence? = null, + ) = Animation(sequence = sequence, intro = intro, loop = loop, outro = outro) + + private class TestTicker(private val scope: TestScope) : Ticker { + override fun nowMs(): Long = scope.testScheduler.currentTime + override suspend fun delay(ms: Long) { + if (ms > 0L) kotlinx.coroutines.delay(ms) + } + } + + private fun TestScope.newPlayer(graph: AnimationGraph): SpriteAnimationPlayer { + val scopeForPlayer = CoroutineScope(coroutineContext) + return SpriteAnimationPlayer(graph, TestTicker(this), scopeForPlayer) + } + + // ---- tests ---- + + @Test + fun playsInfiniteLoopAdvancingOneFramePerTick() = runTest(StandardTestDispatcher()) { + val g = AnimationGraph( + defaultState = "neutral", + animations = mapOf("neutral" to anim(sequence = seq(count = 3, loop = LoopMode.INFINITE))), + transitions = emptyMap(), + ) + val player = newPlayer(g) + + advanceTimeBy(10) // kick coroutine past init + assertEquals("f0", player.currentRef.value?.ref) + + advanceTimeBy(100) + assertEquals("f1", player.currentRef.value?.ref) + + advanceTimeBy(100) + assertEquals("f2", player.currentRef.value?.ref) + + advanceTimeBy(100) + assertEquals("f0", player.currentRef.value?.ref) // wrapped + + player.dispose() + } + + @Test + fun onceWithHoldLastFrameFreezesOnFinalFrame() = runTest(StandardTestDispatcher()) { + val g = AnimationGraph( + defaultState = "state", + animations = mapOf( + "state" to anim(sequence = seq(count = 3, loop = LoopMode.ONCE, holdLastFrame = true)), + ), + transitions = emptyMap(), + ) + val player = newPlayer(g) + + // Advance way past playback. + advanceTimeBy(10_000) + assertEquals("f2", player.currentRef.value?.ref) + + player.dispose() + } + + @Test + fun onceWithoutHoldClearsAfterLastFrame() = runTest(StandardTestDispatcher()) { + val g = AnimationGraph( + defaultState = "state", + animations = mapOf( + "state" to anim(sequence = seq(count = 2, loop = LoopMode.ONCE, holdLastFrame = false)), + ), + transitions = emptyMap(), + ) + val player = newPlayer(g) + + advanceTimeBy(10_000) + assertNull(player.currentRef.value) + + player.dispose() + } + + @Test + fun pingPongBouncesAndCapsAtIterations() = runTest(StandardTestDispatcher()) { + val g = AnimationGraph( + defaultState = "state", + animations = mapOf( + "state" to anim( + sequence = seq(count = 3, loop = LoopMode.PING_PONG, iterations = 1), + ), + ), + transitions = emptyMap(), + ) + val player = newPlayer(g) + + val seen = mutableListOf() + advanceTimeBy(10) + fun capture() { + val ref = player.currentRef.value?.ref + if (ref != null && (seen.isEmpty() || seen.last() != ref)) { + seen.add(ref) + } + } + + // Walk through: f0, f1, f2, f1 (bounce), then stops (iterations=1 → one round-trip). + capture() + repeat(30) { + advanceTimeBy(100) + capture() + } + + // After the bounce finishes the loop exits; the last ref stays visible. + // Minimum expected ordering within the round trip. + val indexF0 = seen.indexOf("f0") + val indexF2 = seen.indexOf("f2") + val indexF1Bounce = seen.lastIndexOf("f1") + assertEquals(0, indexF0) + assert(indexF2 > indexF0) + assert(indexF1Bounce > indexF2) { "expected ping-pong to return to f1 after f2, got $seen" } + + player.dispose() + } + + @Test + fun enteringAStateWithIntroPlaysIntroThenLoop() = runTest(StandardTestDispatcher()) { + val g = AnimationGraph( + defaultState = "thinking", + animations = mapOf( + "thinking" to anim( + intro = seq(count = 2, loop = LoopMode.ONCE), + loop = seq(count = 2, loop = LoopMode.INFINITE), + ), + ), + transitions = emptyMap(), + ) + val player = newPlayer(g) + + val order = mutableListOf() + advanceTimeBy(10) + order.add(player.currentRef.value!!.ref) // f0 intro + advanceTimeBy(100) + order.add(player.currentRef.value!!.ref) // f1 intro + advanceTimeBy(100) + order.add(player.currentRef.value!!.ref) // f0 loop + advanceTimeBy(100) + order.add(player.currentRef.value!!.ref) // f1 loop + advanceTimeBy(100) + order.add(player.currentRef.value!!.ref) // f0 loop (cycled) + + assertEquals(listOf("f0", "f1", "f0", "f1", "f0"), order) + + player.dispose() + } + + @Test + fun requestStatePlaysTransitionPhaseBeforeEnteringTargetState() = runTest(StandardTestDispatcher()) { + val g = AnimationGraph( + defaultState = "neutral", + animations = mapOf( + "neutral" to anim(sequence = seq(count = 1, loop = LoopMode.INFINITE)), + "thinking" to anim( + intro = seq(count = 1, loop = LoopMode.ONCE), + loop = seq(count = 1, loop = LoopMode.INFINITE), + ), + ), + transitions = mapOf("*->thinking" to TransitionRef.Phase("thinking.intro")), + ) + val player = newPlayer(g) + // Kick past init so the neutral loop has rendered its first frame. + // advanceUntilIdle() would hang here — default state is an infinite loop. + advanceTimeBy(10) + val before = player.currentRef.value?.ref + assertEquals("f0", before) + + player.requestState("thinking") + advanceTimeBy(10) + // First thing played on transition is thinking.intro f0 (via transition ref). + assertEquals("f0", player.currentRef.value?.ref) + // Advance past the transition's ONCE playback (100ms @ 10fps) into the + // target state's own loop; target state is now "thinking". + advanceTimeBy(300) + assertEquals("thinking", player.currentState.value) + + player.dispose() + } + + @Test + fun requestSameStateIsNoop() = runTest(StandardTestDispatcher()) { + val g = AnimationGraph( + defaultState = "neutral", + animations = mapOf("neutral" to anim(sequence = seq(count = 1, loop = LoopMode.INFINITE))), + transitions = mapOf("*->*" to TransitionRef.Phase("neutral.intro")), + ) + val player = newPlayer(g) + // Default state is infinite; advanceTimeBy is the only safe way to + // kick past init without hanging on the perpetual frame loop. + advanceTimeBy(10) + val before = player.currentState.value + + player.requestState(before) + advanceTimeBy(10) + + assertEquals(before, player.currentState.value) + player.dispose() + } +} diff --git a/packages/client-kotlin/core/src/test/kotlin/ai/openclaw/spritecore/client/SystemTickerTest.kt b/packages/client-kotlin/core/src/test/kotlin/ai/openclaw/spritecore/client/SystemTickerTest.kt new file mode 100644 index 0000000..f61e6eb --- /dev/null +++ b/packages/client-kotlin/core/src/test/kotlin/ai/openclaw/spritecore/client/SystemTickerTest.kt @@ -0,0 +1,39 @@ +package ai.openclaw.spritecore.client + +import kotlinx.coroutines.runBlocking +import kotlin.test.Test +import kotlin.test.assertTrue + +/** + * Regression test for the member-shadowing bug in SystemTicker.delay(): an + * unqualified call to `delay(ms)` inside the overridden method resolves to + * the same member and self-recurses until StackOverflowError. The bug is + * production-only because the rest of the suite injects TestTicker, so + * this file exists explicitly to exercise the real class with real time. + * + * Keep short delays here (single-digit ms) so the suite stays fast. + */ +class SystemTickerTest { + @Test + fun systemTickerDelayDoesNotSelfRecurse() = runBlocking { + val ticker = SystemTicker() + val start = ticker.nowMs() + ticker.delay(5L) + val elapsed = ticker.nowMs() - start + // We only care that it returns without StackOverflowError. Lower bound + // is loose because the scheduler isn't guaranteed-punctual; upper + // bound guards against a delay that accidentally dropped into a busy + // loop one day. + assertTrue(elapsed in 0..500, "unexpected elapsed=$elapsed ms for a 5ms delay") + } + + @Test + fun systemTickerDelayZeroOrNegativeIsNoop() = runBlocking { + val ticker = SystemTicker() + val start = ticker.nowMs() + ticker.delay(0L) + ticker.delay(-1L) + val elapsed = ticker.nowMs() - start + assertTrue(elapsed < 100, "noop delays shouldn't actually sleep, got $elapsed ms") + } +} diff --git a/packages/client-kotlin/gradle.properties b/packages/client-kotlin/gradle.properties new file mode 100644 index 0000000..07df4f5 --- /dev/null +++ b/packages/client-kotlin/gradle.properties @@ -0,0 +1,5 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +org.gradle.parallel=true +org.gradle.caching=true +kotlin.code.style=official +android.useAndroidX=true diff --git a/packages/client-kotlin/settings.gradle.kts b/packages/client-kotlin/settings.gradle.kts new file mode 100644 index 0000000..48fd8c0 --- /dev/null +++ b/packages/client-kotlin/settings.gradle.kts @@ -0,0 +1,20 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + google() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + mavenCentral() + google() + } +} + +rootProject.name = "sprite-core-client" + +include(":core") +include(":android") diff --git a/packages/client-swift/Package.swift b/packages/client-swift/Package.swift new file mode 100644 index 0000000..8378876 --- /dev/null +++ b/packages/client-swift/Package.swift @@ -0,0 +1,30 @@ +// swift-tools-version:5.9 +import PackageDescription + +let package = Package( + name: "SpriteCoreClient", + platforms: [ + .iOS(.v15), + .macOS(.v12), + .tvOS(.v15), + .watchOS(.v8), + ], + products: [ + .library( + name: "SpriteCoreClient", + targets: ["SpriteCoreClient"] + ), + ], + dependencies: [], + targets: [ + .target( + name: "SpriteCoreClient", + path: "Sources/SpriteCoreClient" + ), + .testTarget( + name: "SpriteCoreClientTests", + dependencies: ["SpriteCoreClient"], + path: "Tests/SpriteCoreClientTests" + ), + ] +) diff --git a/packages/client-swift/README.md b/packages/client-swift/README.md new file mode 100644 index 0000000..100bcb1 --- /dev/null +++ b/packages/client-swift/README.md @@ -0,0 +1,65 @@ +# SpriteCoreClient (Swift) + +Swift client SDK for the SpriteCore plugin. SwiftPM library, zero deps +beyond Foundation, supports iOS 15+, macOS 12+, tvOS 15+, watchOS 8+. + +## Install + +From a consuming Xcode project: + +```swift +// In Package.swift, or via Xcode: File → Add Package Dependencies +.package(path: "../sprite-core/packages/client-swift") // local dev +// or +.package(url: "https://github.com/Tyler-RNG/sprite-core.git", from: "1.0.0") +``` + +Then add `"SpriteCoreClient"` to your target dependencies. + +## Minimal usage + +```swift +import SpriteCoreClient +import UIKit + +let envelope = CharacterManifestJson.parse(jsonData)! +let graph = try AnimationGraph.fromManifest(envelope.manifest, mode: "headshot") + +let frameSource = InMemorySpriteSource { data in UIImage(data: data) } +for (refKey, bytes) in assetBytes { frameSource.put(refKey, bytes: bytes) } + +let player = SpriteAnimationPlayer(graph: graph) + +Task { + for await ref in await player.refStream() { + guard let ref, let img = frameSource.frame(for: ref) else { continue } + await MainActor.run { imageView.image = img } + } +} + +// When the model emits `<<>>`: +let parser = AvatarMarkerParser() +let parsed = parser.push(streamChunk) +for m in parsed.markers { + await player.requestState(m.state, playCount: m.count) +} +``` + +## Surface + +- `CharacterManifest` / `CharacterManifestEnvelope` — `Codable` wire types +- `CharacterManifestJson.parse(_:)` — envelope parser +- `AnimationGraph.fromManifest(_:mode:)` — projection + wildcard resolver +- `SpriteAnimationPlayer` — `actor`-isolated state machine, async-streams refs +- `FrameSource` protocol + `InMemorySpriteSource` +- `AvatarMarkerParser` / `parseAvatarMarkers(_:)` / `splitByMarkers(_:)` + +## Conformance + +This Swift implementation mirrors the Kotlin and TypeScript ports in the +sibling packages. The shared fixture suite at `../../fixtures/` is the +oracle. Run: + +``` +swift test +``` diff --git a/packages/client-swift/Sources/SpriteCoreClient/AnimationGraph.swift b/packages/client-swift/Sources/SpriteCoreClient/AnimationGraph.swift new file mode 100644 index 0000000..8e76407 --- /dev/null +++ b/packages/client-swift/Sources/SpriteCoreClient/AnimationGraph.swift @@ -0,0 +1,88 @@ +import Foundation + +/// Resolved animation table + transition graph for a single mode of a single +/// character. Both sprite and atlas manifests project into this shape so the +/// player stays format-agnostic. +public struct AnimationGraph: Sendable { + public let defaultState: String + public let animations: [String: Animation] + public let transitions: [String: TransitionRef] + + public init(defaultState: String, animations: [String: Animation], transitions: [String: TransitionRef]) { + self.defaultState = defaultState + self.animations = animations + self.transitions = transitions + } + + /// Resolve a state→state transition against the transitions table using + /// wildcard pattern matching. Specificity order (most→least specific): + /// + /// `"->"` → `"->*"` → `"*->"` → `"*->*"` + /// + /// Returns nil when nothing matches. + public func resolveTransition(from: String, to: String) -> TransitionRef? { + let keys = ["\(from)->\(to)", "\(from)->*", "*->\(to)", "*->*"] + for k in keys { + if let t = transitions[k] { return t } + } + return nil + } + + /// Extract a single mode's animation graph from a character manifest. + public static func fromManifest(_ manifest: CharacterManifest, mode: String) throws -> AnimationGraph { + guard let content = manifest.content[mode] else { + throw GraphError.modeNotFound(mode: mode, available: Array(manifest.content.keys)) + } + let defaultState = try resolveDefaultState(stateMap: manifest.stateMap, animations: content.animations) + return AnimationGraph( + defaultState: defaultState, + animations: content.animations, + transitions: content.transitions ?? [:] + ) + } +} + +public enum GraphError: Error, CustomStringConvertible { + case modeNotFound(mode: String, available: [String]) + case noAnimations + + public var description: String { + switch self { + case .modeNotFound(let mode, let available): + return "manifest has no content for mode '\(mode)'. Available: \(available)" + case .noAnimations: + return "manifest mode has no animations" + } + } +} + +private func resolveDefaultState(stateMap: [String: String], animations: [String: Animation]) throws -> String { + for (_, animName) in stateMap { + if animations[animName] != nil { return animName } + } + if let first = animations.keys.first { return first } + throw GraphError.noAnimations +} + +/// The three phases of a phased animation; flat animations use `.loop`. +public enum Phase: String, Sendable { + case intro + case loop + case outro +} + +/// A transition target resolved for playback: which animation + phase to +/// play once before entering the target state's own loop. +public struct ResolvedTransition: Sendable { + public let animation: String + public let phase: Phase + + /// Parse `"thinking.intro"` → `(thinking, .intro)`. Unqualified → `.loop`. + public static func parse(_ ref: String) -> ResolvedTransition { + if let dot = ref.firstIndex(of: ".") { + let phase = Phase(rawValue: String(ref[ref.index(after: dot)...])) ?? .loop + return ResolvedTransition(animation: String(ref[..>>` / `<<>>` avatar-state markers +/// embedded in assistant text. Matching markers are stripped from the visible +/// text and surfaced separately; invalid marker shapes are treated as literal +/// text. +/// +/// Mirrors the TS `createAvatarMarkerParser()` — the fixtures at the repo +/// root enforce byte-equivalent behaviour. + +public let AVATAR_MARKER_OPEN = "<<<" +public let AVATAR_MARKER_CLOSE = ">>>" + +public struct AvatarMarker: Equatable, Sendable { + public let state: String + /// `nil` for bare markers / zero-count; `N >= 1` for play-N-times markers. + public let count: Int? + public init(state: String, count: Int? = nil) { + self.state = state + self.count = count + } +} + +public struct AvatarMarkerParseResult: Equatable, Sendable { + public let cleanedText: String + public let markers: [AvatarMarker] + public init(cleanedText: String, markers: [AvatarMarker]) { + self.cleanedText = cleanedText + self.markers = markers + } +} + +public struct TextSegmentWithEmotion: Equatable, Sendable { + public let text: String + public let emotion: String? + public let emotionCount: Int? + public init(text: String, emotion: String?, emotionCount: Int?) { + self.text = text + self.emotion = emotion + self.emotionCount = emotionCount + } +} + +private let stateNameRegex = try! NSRegularExpression(pattern: "^[A-Za-z0-9_-]+$") + +private func isValidStateName(_ name: String) -> Bool { + if name.isEmpty { return false } + let range = NSRange(location: 0, length: name.utf16.count) + return stateNameRegex.firstMatch(in: name, options: [], range: range) != nil +} + +/// Split a raw marker body into (state, count). Triggers on the *last* dash +/// when the suffix is a non-negative integer. Exported for test coverage. +public func resolveStateAndCount(_ body: String) -> (state: String, count: Int?) { + guard let dashIdx = body.lastIndex(of: "-"), dashIdx != body.startIndex else { + return (body, nil) + } + let after = body.index(after: dashIdx) + guard after != body.endIndex else { return (body, nil) } + let countPart = String(body[after...]) + guard Int(countPart) != nil, countPart.allSatisfy(\.isNumber) else { + return (body, nil) + } + guard let count = Int(countPart), count >= 0 else { return (body, nil) } + let state = String(body[.. AvatarMarkerParseResult { + if chunk.isEmpty { return AvatarMarkerParseResult(cleanedText: "", markers: []) } + let combined = buffer + chunk + let (cleaned, markers, remainder) = processSafePrefix(combined) + buffer = remainder + return AvatarMarkerParseResult(cleanedText: cleaned, markers: markers) + } + + public func flush() -> AvatarMarkerParseResult { + if buffer.isEmpty { return AvatarMarkerParseResult(cleanedText: "", markers: []) } + let leftover = buffer + buffer = "" + return AvatarMarkerParseResult(cleanedText: leftover, markers: []) + } + + public func reset() { + buffer = "" + } +} + +/// Convenience: parse a complete (non-streamed) string in one shot. +public func parseAvatarMarkers(_ text: String) -> AvatarMarkerParseResult { + let parser = AvatarMarkerParser() + let a = parser.push(text) + let b = parser.flush() + return AvatarMarkerParseResult( + cleanedText: a.cleanedText + b.cleanedText, + markers: a.markers + b.markers + ) +} + +/// Split `text` into segments delimited by `<<>>` markers. Each segment +/// carries the preceding marker's state as its `emotion`. +public func splitByMarkers(_ text: String) -> [TextSegmentWithEmotion] { + if text.isEmpty { return [] } + var segments: [TextSegmentWithEmotion] = [] + var currentText = "" + var currentEmotion: String? = nil + var currentEmotionCount: Int? = nil + var i = text.startIndex + while i < text.endIndex { + guard let openAt = text.range(of: AVATAR_MARKER_OPEN, range: i.. (cleanedText: String, markers: [AvatarMarker], remainder: String) { + var markers: [AvatarMarker] = [] + var out = "" + var i = combined.startIndex + + while i < combined.endIndex { + guard let openRange = combined.range(of: AVATAR_MARKER_OPEN, range: i.. i, combined[combined.index(before: j)] == "<" { + j = combined.index(before: j) + } + out += combined[i.. String? { refs[ref.ref] } +} + +public struct EmotionEntry: Codable, Sendable, Equatable { + public let directive: EmotionDirective? + public init(directive: EmotionDirective? = nil) { self.directive = directive } +} + +public struct EmotionDirective: Codable, Sendable, Equatable { + public let voiceId: String? + public let stability: Double? + public let similarity: Double? + public let style: Double? + public let speakerBoost: Bool? + public let speed: Double? + public let audioTag: String? + + public init( + voiceId: String? = nil, + stability: Double? = nil, + similarity: Double? = nil, + style: Double? = nil, + speakerBoost: Bool? = nil, + speed: Double? = nil, + audioTag: String? = nil + ) { + self.voiceId = voiceId + self.stability = stability + self.similarity = similarity + self.style = style + self.speakerBoost = speakerBoost + self.speed = speed + self.audioTag = audioTag + } +} + +public struct CharacterManifestEnvelope: Codable, Sendable, Equatable { + public let manifest: CharacterManifest + public let revision: Int + public init(manifest: CharacterManifest, revision: Int) { + self.manifest = manifest + self.revision = revision + } +} + +/// JSON parser for the envelope published by `node.getCharacterManifest`. +public enum CharacterManifestJson { + public static func parse(_ data: Data) -> CharacterManifestEnvelope? { + try? JSONDecoder().decode(CharacterManifestEnvelope.self, from: data) + } + + public static func parse(_ text: String) -> CharacterManifestEnvelope? { + guard let data = text.data(using: .utf8) else { return nil } + return parse(data) + } + + /// Pick the first mode in `manifest.modes` whose content is present. + public static func pickMode(_ manifest: CharacterManifest) -> String? { + manifest.modes.first { manifest.content[$0] != nil } + } +} + +/// Returns true when every asset ref declared by `envelope.manifest.assets.refs` +/// has bytes in `assetBytes`. Empty refs returns true. +public func characterManifestBytesReady( + envelope: CharacterManifestEnvelope, + assetBytes: [String: Data] +) -> Bool { + let refs = envelope.manifest.assets.refs.keys + if refs.isEmpty { return true } + return refs.allSatisfy { assetBytes[$0] != nil } +} diff --git a/packages/client-swift/Sources/SpriteCoreClient/FrameSource.swift b/packages/client-swift/Sources/SpriteCoreClient/FrameSource.swift new file mode 100644 index 0000000..4c4fd08 --- /dev/null +++ b/packages/client-swift/Sources/SpriteCoreClient/FrameSource.swift @@ -0,0 +1,50 @@ +import Foundation + +/// Platform-specific resolver from a `FrameRef` to a concrete renderable +/// (e.g. `UIImage`, `CGImage`, or raw bytes). The kit itself never constructs +/// frames — callers own the pixel pipeline and feed the player's emitted +/// `FrameRef` into their own `FrameSource` when rendering. +/// +/// Atlas sources honor the optional `x/y/w/h` fields on `FrameRef`; sprite +/// sources ignore them and treat `ref` as the whole-image key. +public protocol FrameSource { + associatedtype Frame + func frame(for ref: FrameRef) -> Frame? +} + +/// Simple in-memory sprite source: callers prime bytes per ref key, decode +/// happens lazily through the closure. Useful for tests + thin clients that +/// don't need a platform-specific image type. +public final class InMemorySpriteSource: FrameSource, @unchecked Sendable { + private let decode: (Data) -> Frame? + private var bytesByRef: [String: Data] = [:] + private var cache: [String: Frame] = [:] + private let lock = NSLock() + + public init(decode: @escaping (Data) -> Frame?) { + self.decode = decode + } + + public func put(_ refKey: String, bytes: Data) { + lock.lock() + defer { lock.unlock() } + bytesByRef[refKey] = bytes + cache.removeValue(forKey: refKey) + } + + public func keys() -> Set { + lock.lock() + defer { lock.unlock() } + return Set(bytesByRef.keys) + } + + public func frame(for ref: FrameRef) -> Frame? { + lock.lock() + defer { lock.unlock() } + if let cached = cache[ref.ref] { return cached } + guard let bytes = bytesByRef[ref.ref] else { return nil } + guard let decoded = decode(bytes) else { return nil } + cache[ref.ref] = decoded + return decoded + } +} diff --git a/packages/client-swift/Sources/SpriteCoreClient/SpriteAnimationPlayer.swift b/packages/client-swift/Sources/SpriteCoreClient/SpriteAnimationPlayer.swift new file mode 100644 index 0000000..38a4139 --- /dev/null +++ b/packages/client-swift/Sources/SpriteCoreClient/SpriteAnimationPlayer.swift @@ -0,0 +1,218 @@ +import Foundation + +/// Platform-independent playback engine. One instance per character per mode. +/// Drives `currentRef` forward over time according to the `AnimationGraph`'s +/// animations and transitions; callers materialize frames via their own +/// `FrameSource`. +/// +/// Mirrors the Kotlin `SpriteAnimationPlayer` and the TS `SpriteAnimationPlayer`. +/// Observability is via `AsyncStream` and `AsyncStream` — a +/// SwiftUI consumer can also read the latest synchronous values through +/// `currentRef` / `currentState`. +public actor SpriteAnimationPlayer { + private let graph: AnimationGraph + private let ticker: Ticker + private let minFrameDelayMs = 16 + + private var _currentRef: FrameRef? + private var _currentState: String + private var refContinuations: [UUID: AsyncStream.Continuation] = [:] + private var stateContinuations: [UUID: AsyncStream.Continuation] = [:] + + private var runningTask: Task? + + public init(graph: AnimationGraph, ticker: Ticker = SystemTicker()) { + self.graph = graph + self.ticker = ticker + self._currentState = graph.defaultState + // Kick off default-state playback on actor init. Use a detached Task + // because the actor isn't fully initialized until this init returns. + Task { [weak self] in + await self?.startDefaultState() + } + } + + // MARK: - Public surface + + public var currentRef: FrameRef? { _currentRef } + public var currentState: String { _currentState } + + public func refStream() -> AsyncStream { + AsyncStream { continuation in + let id = UUID() + continuation.yield(_currentRef) + refContinuations[id] = continuation + continuation.onTermination = { [weak self] _ in + Task { await self?.removeRefContinuation(id) } + } + } + } + + public func stateStream() -> AsyncStream { + AsyncStream { continuation in + let id = UUID() + continuation.yield(_currentState) + stateContinuations[id] = continuation + continuation.onTermination = { [weak self] _ in + Task { await self?.removeStateContinuation(id) } + } + } + } + + /// Request a state change. If the graph's transitions table has a match + /// for `currentState → target`, that transition plays once before the + /// target state's own loop starts. + /// + /// `playCount` semantics (from `<<>>`): + /// - nil or 0 — loop indefinitely + /// - N >= 1 — play N times and hold last frame + public func requestState(_ target: String, playCount: Int? = nil) async { + let previousState = _currentState + let sameState = target == previousState + let effectiveCount: Int? = (playCount ?? 0) >= 1 ? playCount : nil + + await cancelRunning() + + if sameState && effectiveCount == nil { + return + } + + let task = Task { [weak self] in + guard let self else { return } + if !sameState { + if case let .phase(ref) = await self.graph.resolveTransition(from: previousState, to: target) ?? .phase("") { + if !ref.isEmpty { + let resolved = ResolvedTransition.parse(ref) + await self.playPhase(animName: resolved.animation, phase: resolved.phase, loopOverride: .once) + } + } + } + await self.playState(target, entering: !sameState, playCountOverride: effectiveCount) + } + runningTask = task + } + + public func dispose() async { + await cancelRunning() + for (_, c) in refContinuations { c.finish() } + for (_, c) in stateContinuations { c.finish() } + refContinuations.removeAll() + stateContinuations.removeAll() + } + + // MARK: - Internals + + private func removeRefContinuation(_ id: UUID) { + refContinuations.removeValue(forKey: id) + } + + private func removeStateContinuation(_ id: UUID) { + stateContinuations.removeValue(forKey: id) + } + + private func startDefaultState() async { + let task = Task { [weak self] in + guard let self else { return } + await self.playState(await self.graph.defaultState, entering: true, playCountOverride: nil) + } + runningTask = task + } + + private func cancelRunning() async { + runningTask?.cancel() + runningTask = nil + } + + private func setRef(_ ref: FrameRef?) { + _currentRef = ref + for (_, c) in refContinuations { c.yield(ref) } + } + + private func setState(_ state: String) { + if _currentState != state { + _currentState = state + for (_, c) in stateContinuations { c.yield(state) } + } + } + + private func playState(_ state: String, entering: Bool, playCountOverride: Int?) async { + setState(state) + guard let anim = graph.animations[state] else { return } + if entering, anim.intro != nil { + await playPhase(animName: state, phase: .intro, loopOverride: nil) + if Task.isCancelled { return } + } + if let count = playCountOverride, count >= 1 { + await playPhaseFinite(animName: state, phase: .loop, times: count) + return + } + await playPhase(animName: state, phase: .loop, loopOverride: nil) + } + + private func playPhaseFinite(animName: String, phase: Phase, times: Int) async { + guard let anim = graph.animations[animName], let seq = pickPhase(anim, phase), !seq.frames.isEmpty else { return } + let frameDelayMs = max(1000 / seq.fps, minFrameDelayMs) + for _ in 0.. 2 { + for i in stride(from: seq.frames.count - 2, through: 1, by: -1) { + if Task.isCancelled { return } + setRef(seq.frames[i]) + do { try await ticker.delay(ms: frameDelayMs) } catch { return } + } + } + rounds += 1 + } + case .infinite: + while !Task.isCancelled { + for ref in seq.frames { + if Task.isCancelled { return } + setRef(ref) + do { try await ticker.delay(ms: frameDelayMs) } catch { return } + } + } + } + } + + private func pickPhase(_ anim: Animation, _ phase: Phase) -> FrameSequence? { + switch phase { + case .intro: return anim.intro + case .outro: return anim.outro + case .loop: return anim.effectiveLoop + } + } +} diff --git a/packages/client-swift/Sources/SpriteCoreClient/Ticker.swift b/packages/client-swift/Sources/SpriteCoreClient/Ticker.swift new file mode 100644 index 0000000..c9a6509 --- /dev/null +++ b/packages/client-swift/Sources/SpriteCoreClient/Ticker.swift @@ -0,0 +1,14 @@ +import Foundation + +/// Timing abstraction for frame advancement. The default implementation uses +/// `Task.sleep`; tests inject a fake ticker that advances virtual time. +public protocol Ticker: Sendable { + func delay(ms: Int) async throws +} + +public struct SystemTicker: Ticker { + public init() {} + public func delay(ms: Int) async throws { + try await Task.sleep(nanoseconds: UInt64(ms) * 1_000_000) + } +} diff --git a/packages/client-swift/Tests/SpriteCoreClientTests/AvatarMarkerParserTests.swift b/packages/client-swift/Tests/SpriteCoreClientTests/AvatarMarkerParserTests.swift new file mode 100644 index 0000000..b005f6e --- /dev/null +++ b/packages/client-swift/Tests/SpriteCoreClientTests/AvatarMarkerParserTests.swift @@ -0,0 +1,53 @@ +import XCTest +@testable import SpriteCoreClient + +final class AvatarMarkerParserTests: XCTestCase { + func testResolveStateAndCount_bare() { + let r = resolveStateAndCount("happy") + XCTAssertEqual(r.state, "happy") + XCTAssertNil(r.count) + } + + func testResolveStateAndCount_withNumericSuffix() { + let r = resolveStateAndCount("happy-3") + XCTAssertEqual(r.state, "happy") + XCTAssertEqual(r.count, 3) + } + + func testResolveStateAndCount_dashHyphenatedName() { + let r = resolveStateAndCount("head-cocked") + XCTAssertEqual(r.state, "head-cocked") + XCTAssertNil(r.count) + } + + func testStripsSingleMarker() { + let p = AvatarMarkerParser() + let r = p.push("hello <<>> world") + XCTAssertEqual(r.cleanedText, "hello world") + XCTAssertEqual(r.markers, [AvatarMarker(state: "happy")]) + } + + func testRecognisesMarkerSplitAcrossChunks() { + let p = AvatarMarkerParser() + let a = p.push("start <<>> end") + XCTAssertEqual(a.cleanedText + b.cleanedText, "start end") + XCTAssertEqual(a.markers + b.markers, [AvatarMarker(state: "happy")]) + } + + func testPlayCountMarker() { + let r = parseAvatarMarkers("say <<>> it") + XCTAssertEqual(r.markers, [AvatarMarker(state: "wink", count: 1)]) + } + + func testInvalidMarkerStaysLiteral() { + let r = parseAvatarMarkers("bad <<>> marker") + XCTAssertEqual(r.cleanedText, "bad <<>> marker") + XCTAssertTrue(r.markers.isEmpty) + } + + func testSplitByMarkers_countForwarded() { + let segs = splitByMarkers("<<>> hello") + XCTAssertEqual(segs, [TextSegmentWithEmotion(text: " hello", emotion: "wink", emotionCount: 2)]) + } +} diff --git a/packages/client-swift/Tests/SpriteCoreClientTests/ManifestParseTests.swift b/packages/client-swift/Tests/SpriteCoreClientTests/ManifestParseTests.swift new file mode 100644 index 0000000..585a253 --- /dev/null +++ b/packages/client-swift/Tests/SpriteCoreClientTests/ManifestParseTests.swift @@ -0,0 +1,65 @@ +import XCTest +@testable import SpriteCoreClient + +final class ManifestParseTests: XCTestCase { + func testDecodesMinimalHeadshotManifest() throws { + let json = """ + { + "version": 1, + "agentId": "ginger", + "modes": ["headshot"], + "stateMap": { "idle": "idle" }, + "content": { + "headshot": { + "animations": { + "idle": { + "sequence": { + "frames": [{ "ref": "idle.00" }], + "fps": 12, + "loop": "infinite" + } + } + } + } + }, + "assets": { + "refs": { "idle.00": "atlas/idle_00.webp" } + } + } + """ + let manifest = try JSONDecoder().decode(CharacterManifest.self, from: Data(json.utf8)) + XCTAssertEqual(manifest.agentId, "ginger") + XCTAssertEqual(manifest.modes, ["headshot"]) + let content = try XCTUnwrap(manifest.content["headshot"]) + let idle = try XCTUnwrap(content.animations["idle"]) + XCTAssertEqual(idle.sequence?.frames.count, 1) + } + + func testDecodesTransitionRefStringAndCrossfade() throws { + let json = """ + { + "version": 1, + "agentId": "a", + "modes": ["m"], + "stateMap": {}, + "content": { + "m": { + "animations": { "x": { "sequence": { "frames": [{ "ref": "r" }], "fps": 12, "loop": "once" } } }, + "transitions": { + "*->*": "x.intro", + "a->b": { "blend": "crossfade", "ms": 150 } + } + } + }, + "assets": { "refs": { "r": "path" } } + } + """ + let m = try JSONDecoder().decode(CharacterManifest.self, from: Data(json.utf8)) + let content = try XCTUnwrap(m.content["m"]) + let t = try XCTUnwrap(content.transitions) + guard case .phase(let s) = t["*->*"]! else { return XCTFail("expected phase ref") } + XCTAssertEqual(s, "x.intro") + guard case .crossfade(let ms) = t["a->b"]! else { return XCTFail("expected crossfade") } + XCTAssertEqual(ms, 150) + } +} diff --git a/packages/plugin/README.md b/packages/plugin/README.md new file mode 100644 index 0000000..a95c17c --- /dev/null +++ b/packages/plugin/README.md @@ -0,0 +1,436 @@ +# SpriteCore + +OpenClaw plugin that owns the data plane for multi-state sprite avatars and +voice/TTS. Once enabled, SpriteCore is the single source of truth for: + +- per-agent avatar config (atlas image + manifest) +- per-agent voice descriptor (provider + voiceId for the watch / phone) +- the prompt block that teaches the model which avatar states exist (so it + knows when to emit `<<>>`, `<<>>`, etc., optionally with a + `-N` play-count suffix like `<<>>` or `<<>>`) +- HTTP asset serving (`/openclaw-assets/*`) +- streaming TTS proxy (`/stream/tts`) +- streaming STT proxy (`/stream/stt`) +- the gateway RPC `node.getCharacterManifest` that ships the watch a + ready-to-render manifest + +The agent's `identity.avatar` field in `openclaw.json` stays narrow: a +workspace-relative image path, an http(s) URL, a data URI, or a short string / +emoji. Anything richer (atlas, multiple states, prompting vocabulary, voice +selection) lives in this plugin's config block. + +## Install (private beta) + +This plugin is currently private. Installing it requires a GitHub Personal +Access Token and a one-time npm config. You must be a collaborator on the +`Tyler-RNG/sprite-core` GitHub repo. + +**1. Create a GitHub Personal Access Token.** Go to + and generate a classic token with +the `read:packages` scope (that's the only scope you need). Copy the token +(it starts with `ghp_`). + +**2. Point the `@tyler-rng` npm scope at GitHub Packages.** Add these two +lines to your `~/.npmrc` (create it if it doesn't exist): + +``` +@tyler-rng:registry=https://npm.pkg.github.com +//npm.pkg.github.com/:_authToken=ghp_YOUR_TOKEN_HERE +``` + +Replace `ghp_YOUR_TOKEN_HERE` with the token from step 1. Then +`chmod 600 ~/.npmrc` so other users on the machine can't read your token. + +**3. Install with the normal openclaw command.** + +```bash +openclaw plugin install @tyler-rng/sprite-core +``` + +OpenClaw resolves the `@tyler-rng` scope against GitHub Packages using your +token, downloads the tarball, and extracts it into your plugin directory. +Updates later: `openclaw plugin update @tyler-rng/sprite-core`. + +**4. Enable and configure it.** See [Enable](#enable) below for the +`openclaw.json` config block to paste in, then restart your gateway. + +**Troubleshooting:** + +- `401 Unauthorized` — your token is wrong, expired, or missing the + `read:packages` scope. Regenerate it. +- `404 Not Found` — either you're not a collaborator on + `Tyler-RNG/sprite-core`, or your `~/.npmrc` doesn't have the + `@tyler-rng:registry=...` line pointing at `npm.pkg.github.com`. +- Plugin installs but doesn't load — confirm + `plugins.entries["sprite-core"].enabled: true` is in your `openclaw.json` + and restart the gateway. + +## Enable + +```jsonc +{ + "plugins": { + "entries": { + "sprite-core": { + "enabled": true, + "config": { + "assets": { + "enabled": true, + "assetsDir": "./assets", + "publicAssets": false, + "maxAssetSizeBytes": 10485760, + "publicBaseUrl": "https://..ts.net", + }, + "streamTts": { + "enabled": true, + "provider": "elevenlabs", + "apiKey": { "source": "env", "id": "ELEVENLABS_API_KEY" }, + "defaultModel": "eleven_turbo_v2", + }, + "agents": { + "agent": { + "avatar": { + "kind": "atlas", + "default": "idle", + "manifest": "avatars/agent/agent.atlas.json", + }, + "voice": { + "provider": "elevenlabs", + "voiceId": "", + "label": "default", + }, + "prompting": { + "descriptions": { + "idle": "calm / listening", + "thinking": "processing the user's request", + "happy": "warm / pleased", + "sad": "sympathy / disappointment", + }, + }, + }, + }, + }, + }, + }, + }, +} +``` + +## Default `agent` template + +Ships under `template/agent/` in this repo. It declares four states +(`idle`, `thinking`, `happy`, `sad`) and includes a placeholder atlas image +(four solid-colored squares) so the runtime works the moment you enable the +plugin — no art required. + +To use the template: + +1. Copy `template/agent/` from this repo into + `~/.openclaw/assets/avatars/agent/` (or wherever your `assetsDir` resolves + to under the `avatars//` convention). +2. Paste the config block from `template/agent/README.md` into your + `openclaw.json` under `plugins.entries["sprite-core"].config.agents.agent`. +3. Restart the gateway. The watch will fetch the manifest, render the four + placeholder colors, and auto-swap to `thinking` on every send. + +Replace the placeholder image with real art whenever you have it; the manifest +schema does not need to change. See `template/agent/README.md` for the swap +procedure. + +## Config reference + +### `assets` + +Static asset serving for atlas images, frame trees, audio clips. + +| Field | Type | Notes | +| ------------------- | --------- | ------------------------------------------------------------------------------------------------- | +| `enabled` | `boolean` | Required to be `true` for the route to register. | +| `assetsDir` | `string` | Path the route serves from. Relative paths resolve under `~/.openclaw/state`. Default `./assets`. | +| `publicAssets` | `boolean` | When `true`, `/openclaw-assets/*` skips gateway auth. Use only when intentional. | +| `maxAssetSizeBytes` | `number` | Hard cap on per-file size. Default 10 MiB. | +| `publicBaseUrl` | `string` | URL the plugin advertises to clients in `/sprite-core/agents`. Useful for Tailscale endpoints. | + +Path traversal (`..`), symlinks pointing outside `assetsDir`, and dotfiles are +rejected. ETag + 24 h `Cache-Control` are set automatically. + +### `streamTts` + +Streaming TTS proxy. Today only ElevenLabs is wired. + +| Field | Type | Notes | +| -------------- | -------------- | -------------------------------------------------------------------------------------------------- | +| `enabled` | `boolean` | Required to be `true` for the route to register. | +| `provider` | `"elevenlabs"` | Only value supported today. | +| `apiKey` | `SecretInput` | Use `{ "source": "env", "id": "ELEVENLABS_API_KEY" }`. Plain strings are accepted but discouraged. | +| `defaultModel` | `string` | ElevenLabs model id. Default `eleven_turbo_v2`. Override per request via `?model=` query param. | + +> **The plugin ships without an ElevenLabs key.** You provide your own. +> Without `streamTts.enabled = true` and a valid `apiKey`, `/stream/tts` +> returns 503 and the watch falls back silently — agents still work, the +> avatar still animates, just no spoken audio. See [ElevenLabs setup](#elevenlabs-setup). +> +> For the full wire protocol of `/stream/tts` (query params, streaming MP3 +> response, how emotion directives map to ElevenLabs `voice_settings`, client +> composition examples) see [`docs/tts-integration.md`](docs/tts-integration.md). + +### `streamStt` + +Streaming STT proxy. Parallel to `streamTts` — same provider, same key, same +auth model. Clients POST raw audio; the plugin wraps it in multipart and +forwards to ElevenLabs's `/v1/speech-to-text`. + +| Field | Type | Notes | +| -------------- | -------------- | ----------------------------------------------------------------------------------------------- | +| `enabled` | `boolean` | Required to be `true` for the route to register. | +| `provider` | `"elevenlabs"` | Only value supported today. | +| `apiKey` | `SecretInput` | Same key as TTS — ElevenLabs uses one key for both. Reuse `{ "source": "env", "id": "ELEVENLABS_API_KEY" }`. | +| `defaultModel` | `string` | ElevenLabs model id. Default `scribe_v1`. Override per request via `?model=`. | +| `maxBodyBytes` | `number` | Optional plugin-level cap on inbound body size (checked against `Content-Length`). No default. | + +> For the full wire protocol of `/stream/stt` (accepted audio formats, query +> params → multipart field mapping, response JSON shape, error codes, curl +> example, phone-side press-and-hold flow) see +> [`docs/stt-integration.md`](docs/stt-integration.md). + +### `agents.` + +Per-agent rich descriptor that supersedes the legacy +`agents.list[].identity.avatar` object form and `agents.list[].voice` block. + +| Field | Type | Notes | +| ----------- | ----------------- | ------------------------------------------------------------------------------------------------- | +| `avatar` | `AvatarConfig` | Atlas descriptor — see below. | +| `voice` | `VoiceConfig` | `{ provider, voiceId, label, … }` — extra keys passed through to the watch. | +| `prompting` | `PromptingConfig` | Per-state descriptions used to build the model-side instruction. Optional `instruction` override. | + +#### `AvatarConfig` — `kind: "atlas"` (only kind currently supported) + +| Field | Type | Notes | +| ---------- | --------- | ------------------------------------------------------------ | +| `kind` | `"atlas"` | Discriminator. | +| `default` | `string` | State the agent holds when idle. Conventionally `idle`. | +| `manifest` | `string` | Path to the atlas JSON manifest, resolved under `assetsDir`. | + +The manifest itself owns frame rects, animations, and transitions — see +`docs/avatars/formats.md` for the full atlas schema. + +#### `VoiceConfig` + +Pass-through descriptor surfaced to the watch / phone via +`/sprite-core/agents`. Extra keys are allowed. + +```jsonc +"voice": { + "provider": "elevenlabs", + "voiceId": "21m00Tcm4TlvDq8ikWAM", + "label": "default" +} +``` + +#### `PromptingConfig` + +Drives the system-prompt block that teaches the model the avatar's emotion +vocabulary. + +| Field | Type | Notes | +| -------------- | ----------------------- | ------------------------------------------------------------------------------------------------- | +| `descriptions` | `Record` | One entry per state. Used to render `- : ` lines in the injected instruction. | +| `instruction` | `string` (optional) | Explicit override. When set, replaces the auto-generated text entirely. | + +The state names you list here must match keys in the atlas manifest's +`animations` table — that's how the watch maps a model-emitted +`<<>>` marker to the right animation. + +The keyword vocabulary (state names) lives in the gateway plugin; the parsing +of `<<>>` markers from the model output stays on the gateway side +(`src/gateway/avatar-marker-parser.ts`) and the playback code stays on the +edge devices (Wear OS DisplayKit). So edge devices stay generic — any state +name in the manifest just works. + +## Routes + +| Path | Auth | Purpose | +| ----------------------------- | --------- | ---------------------------------------------------------------------- | +| `GET /openclaw-assets/` | gateway\* | Static asset serving. \*`auth: "plugin"` when `publicAssets: true`. | +| `GET /stream/tts` | gateway | Streaming TTS proxy (ElevenLabs). | +| `POST /stream/stt` | gateway | Streaming STT proxy (ElevenLabs). | +| `GET /sprite-core/agents` | gateway | `{ agents: { : { avatar, voice } }, publicBaseUrl? }` for clients. | + +## Gateway RPC + +| Method | Purpose | +| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `node.getCharacterManifest` | Returns `{ manifest, revision }` — a ready-to-render manifest assembled from the plugin's per-agent atlas config + on-disk atlas JSON. The watch calls this through the phone relay. | + +`node.getCharacterManifest` is registered by this plugin via +`api.registerGatewayMethod` from `index.ts`. When the +plugin is disabled, the method is unregistered and returns "method not found" +naturally — operators get a graceful degradation rather than a stale handler. + +## How `thinking` auto-plays + +The Wear OS phone-relay (`apps/android/app/src/main/java/ai/openclaw/app/wear/WearRelayService.kt`) +publishes a `state: "thinking"` cue on the `/openclaw/avatars//state` +DataClient path the moment the user sends a message. If your manifest declares +a `thinking` animation, DisplayKit swaps to it. If it doesn't, the watch +no-ops and stays on the previous state. + +Model-emitted `<<>>` markers (parsed by +`src/gateway/avatar-marker-parser.ts`) override this state mid-reply — last +write wins. + +## ElevenLabs setup + +The plugin **does not** ship with a key. Steps for an operator: + +1. Create an ElevenLabs account at . +2. Get your API key from the profile menu. +3. Export it in your shell environment (the gateway must inherit it): + ```bash + export ELEVENLABS_API_KEY="sk_..." + ``` +4. Pick a voice id from your ElevenLabs voice library. +5. Wire both into `openclaw.json` under `plugins.entries["sprite-core"].config`: + - `streamTts.apiKey = { "source": "env", "id": "ELEVENLABS_API_KEY" }` + - `agents..voice = { "provider": "elevenlabs", "voiceId": "" }` +6. Restart the gateway. + +If you don't enable `streamTts`, agents still work normally — the watch's +TTS playback path falls back silently. + +## Security + +- Asset serving rejects path traversal (`..`), symlinks pointing outside + `assetsDir`, and dotfile access. +- File size capped by `maxAssetSizeBytes`. +- `publicAssets: true` skips gateway auth — only set this when you intentionally + serve operator-chosen files to anonymous clients (e.g. avatars on a public web page). +- The ElevenLabs API key should be a `SecretRef` (env, file, keychain), never + inlined as a plain string in committed config. + +## Plugin self-containment + +Everything avatar / character-manifest now lives in this plugin: + +- `src/prompting.ts` owns `buildPromptingInstruction` + `isAtlasAvatarConfig`. +- `src/character-manifest.ts` owns `buildCharacterManifest` and the wire-shape + inlined `CharacterManifest` type. +- `index.ts` registers `node.getCharacterManifest` via + `api.registerGatewayMethod` and reads fresh plugin config per call. + +Core has no atlas-shaped types: `IdentityConfig.avatar` is narrowed back to +`string` (path / URL / data URI / emoji), `AgentAvatarAtlasConfig` and +friends are deleted, the gateway agent row no longer carries an `avatarAtlas` +block. Disable the plugin and the only thing that stops working is the +multi-state sprite avatar (the simple string avatar still resolves through +core's `resolveAgentAvatar`). + +### Open follow-ups + +- None of substance. The prompt instruction is live (wired via + `api.registerSystemPromptContribution` from `index.ts`), and per-agent + `voice` has been removed from core — the plugin is the sole owner. + +## Pixellab.ai pipeline + +The plugin ships two Node scripts. Together they cover the create → animate +→ package flow end to end (once the animate step has its own script). + +### Create a character + +```bash +node scripts/pixellab-create.mjs \ + --name "elf" \ + --description "a magical elf with pointed ears" +``` + +Queues a 4-direction character on pixellab, polls the background job, and +prints the new `character_id` plus the four rotation URLs so you can eyeball +the look before adding animations. `--json` emits just the id + rotations +for scripting. + +### Add animations + +Not yet ported. Use the pixellab.ai web UI or the animate-character script +(operator-supplied). + +### Export into SpriteCore + +The plugin ships a Node exporter that downloads a finished pixellab.ai +character bundle by UUID and writes a SpriteCore-compatible atlas + manifest +directly into `/avatars//`: + +```bash +# Quick path — assumes pixellab key is in `pass` or exported as PIXELLAB_API_KEY +node scripts/pixellab-export.mjs \ + --uid + +# Explicit key command + custom output root +PIXELLAB_API_KEY="$(op read op://vault/pixellab/api-key)" \ + node scripts/pixellab-export.mjs \ + --uid \ + --assets-root ~/.openclaw/state/assets/avatars \ + --overwrite + +# Dry-run the plan without touching pixellab or disk +node scripts/pixellab-export.mjs --uid --dry-run +``` + +Auth resolution order: `PIXELLAB_API_KEY` env → `--api-key-command ""` +→ `pass show pixellab/api-key`. Pick whichever matches your secret store. + +Output: + +- `/avatars//.atlas.webp` — packed atlas image. +- `/avatars//.atlas.json` — manifest. +- `/avatars//frames//NN.webp` — per-state frame + tree (useful for re-packing via `pnpm avatar:pack`). + +The exporter pairs zip-folder hashes with the pixellab API's +`animation_type` field (via `GET /characters//animations`) to emit clean +canonical SpriteCore state names — `happy`, `sad`, `thinking`, `idle` — and +generates descriptions from the animation's `display_name` (or the original +emotion prompt when no display name is set). Duplicate canonical names (e.g. +two `idle` animations of different lengths) get `_2`/`_3` suffixes. If the +metadata fetch fails, it falls back to verbose slug names. + +For the end-to-end create → approve → animate → export flow, see the +`openclaw-pixellab-avatar` skill at +`.agents/skills/openclaw-pixellab-avatar/SKILL.md`. + +The `pixellab.ai` online pixel-sprite generator is a candidate art pipeline +for the template. The intent is: + +1. Operator runs a Claude Code skill (`.agents/skills/openclaw-pixellab-avatar/SKILL.md`). +2. Skill walks them through pixellab signup + API key extraction. +3. Skill prompts pixellab to generate a character + the emotions/states the + operator wants. +4. A packaging script (`scripts/avatars/pixellab-import.mjs`) downloads the + results and wires them into the SpriteCore template layout + (`avatars//.atlas.{webp,json}`). + +The skill exists as a stub. The packaging script is not yet implemented (the +upstream pixellab.ai API contract needs to be confirmed first); see +`scripts/avatars/pixellab-import.mjs` for the placeholder. + +## Open follow-ups + +- **Pixellab exporter transition cleanup.** `scripts/pixellab-export.mjs` + unconditionally writes `*->thinking` / `thinking->*` transitions into + every atlas manifest, even when the `thinking` animation has no phased + `.intro` / `.outro` sub-sequences (the common case for v3-mode outputs). + Lint noise in the generated manifest; the runtime silently no-ops on the + missing phases. Only emit those transitions when the thinking animation + actually has intro/outro phases. ~10-line fix. +- **Pixellab `animate` template-mode investigation.** `scripts/pixellab-animate.mjs` + uses `mode: "v3"`, which produces `animation_type: "custom-"` names + instead of canonical `happy` / `sad` / `thinking` names. The exporter + currently papers over this with a `--rename` mapping. Pixellab's API may + expose a `template_animation_id` path (or a PATCH for `display_name`) + that would eliminate the workaround — confirm against + `https://api.pixellab.ai/v2/openapi.json` and migrate if available. +- **Authenticated end-to-end smoke against ElevenLabs.** Unit tests cover + the handler logic exhaustively, but nothing has sent real audio through + `POST /stream/stt` + real text through `GET /stream/tts` on a paired + device end-to-end recently. Worth one credit-burning pass periodically. diff --git a/index.ts b/packages/plugin/index.ts similarity index 100% rename from index.ts rename to packages/plugin/index.ts diff --git a/openclaw.plugin.json b/packages/plugin/openclaw.plugin.json similarity index 100% rename from openclaw.plugin.json rename to packages/plugin/openclaw.plugin.json diff --git a/packages/plugin/package.json b/packages/plugin/package.json new file mode 100644 index 0000000..1ef2b5a --- /dev/null +++ b/packages/plugin/package.json @@ -0,0 +1,39 @@ +{ + "name": "@tyler-rng/sprite-core", + "version": "1.0.0", + "description": "OpenClaw SpriteCore plugin — in-gateway asset + TTS + STT data plane for multi-state avatars", + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/Tyler-RNG/sprite-core.git" + }, + "license": "MIT", + "devDependencies": { + "@openclaw/plugin-sdk": "2026.4.15-beta.1", + "openclaw": "2026.4.15-beta.1" + }, + "peerDependencies": { + "openclaw": ">=2026.4.15-beta.1" + }, + "peerDependenciesMeta": { + "openclaw": { + "optional": true + } + }, + "openclaw": { + "extensions": [ + "./index.ts" + ], + "install": { + "npmSpec": "@tyler-rng/sprite-core", + "defaultChoice": "npm", + "minHostVersion": ">=2026.4.10" + }, + "compat": { + "pluginApi": ">=2026.4.15-beta.1" + }, + "build": { + "openclawVersion": "2026.4.15-beta.1" + } + } +} diff --git a/scripts/pixellab-animate.mjs b/packages/plugin/scripts/pixellab-animate.mjs similarity index 100% rename from scripts/pixellab-animate.mjs rename to packages/plugin/scripts/pixellab-animate.mjs diff --git a/scripts/pixellab-create.mjs b/packages/plugin/scripts/pixellab-create.mjs similarity index 100% rename from scripts/pixellab-create.mjs rename to packages/plugin/scripts/pixellab-create.mjs diff --git a/scripts/pixellab-export.mjs b/packages/plugin/scripts/pixellab-export.mjs similarity index 100% rename from scripts/pixellab-export.mjs rename to packages/plugin/scripts/pixellab-export.mjs diff --git a/src/agents-route.test.ts b/packages/plugin/src/agents-route.test.ts similarity index 100% rename from src/agents-route.test.ts rename to packages/plugin/src/agents-route.test.ts diff --git a/src/agents-route.ts b/packages/plugin/src/agents-route.ts similarity index 100% rename from src/agents-route.ts rename to packages/plugin/src/agents-route.ts diff --git a/src/assets-route.test.ts b/packages/plugin/src/assets-route.test.ts similarity index 100% rename from src/assets-route.test.ts rename to packages/plugin/src/assets-route.test.ts diff --git a/src/assets-route.ts b/packages/plugin/src/assets-route.ts similarity index 100% rename from src/assets-route.ts rename to packages/plugin/src/assets-route.ts diff --git a/src/character-manifest.test.ts b/packages/plugin/src/character-manifest.test.ts similarity index 100% rename from src/character-manifest.test.ts rename to packages/plugin/src/character-manifest.test.ts diff --git a/src/character-manifest.ts b/packages/plugin/src/character-manifest.ts similarity index 100% rename from src/character-manifest.ts rename to packages/plugin/src/character-manifest.ts diff --git a/src/http-helpers.ts b/packages/plugin/src/http-helpers.ts similarity index 100% rename from src/http-helpers.ts rename to packages/plugin/src/http-helpers.ts diff --git a/src/prompting.test.ts b/packages/plugin/src/prompting.test.ts similarity index 100% rename from src/prompting.test.ts rename to packages/plugin/src/prompting.test.ts diff --git a/src/prompting.ts b/packages/plugin/src/prompting.ts similarity index 100% rename from src/prompting.ts rename to packages/plugin/src/prompting.ts diff --git a/src/provider-auth.test.ts b/packages/plugin/src/provider-auth.test.ts similarity index 100% rename from src/provider-auth.test.ts rename to packages/plugin/src/provider-auth.test.ts diff --git a/src/provider-auth.ts b/packages/plugin/src/provider-auth.ts similarity index 100% rename from src/provider-auth.ts rename to packages/plugin/src/provider-auth.ts diff --git a/src/stt-route.test.ts b/packages/plugin/src/stt-route.test.ts similarity index 100% rename from src/stt-route.test.ts rename to packages/plugin/src/stt-route.test.ts diff --git a/src/stt-route.ts b/packages/plugin/src/stt-route.ts similarity index 100% rename from src/stt-route.ts rename to packages/plugin/src/stt-route.ts diff --git a/src/tts-route.test.ts b/packages/plugin/src/tts-route.test.ts similarity index 100% rename from src/tts-route.test.ts rename to packages/plugin/src/tts-route.test.ts diff --git a/src/tts-route.ts b/packages/plugin/src/tts-route.ts similarity index 100% rename from src/tts-route.ts rename to packages/plugin/src/tts-route.ts diff --git a/src/types.ts b/packages/plugin/src/types.ts similarity index 100% rename from src/types.ts rename to packages/plugin/src/types.ts diff --git a/template/agent/README.md b/packages/plugin/template/agent/README.md similarity index 100% rename from template/agent/README.md rename to packages/plugin/template/agent/README.md diff --git a/template/agent/agent.atlas.json b/packages/plugin/template/agent/agent.atlas.json similarity index 100% rename from template/agent/agent.atlas.json rename to packages/plugin/template/agent/agent.atlas.json diff --git a/template/agent/agent.atlas.webp b/packages/plugin/template/agent/agent.atlas.webp similarity index 100% rename from template/agent/agent.atlas.webp rename to packages/plugin/template/agent/agent.atlas.webp diff --git a/template/agent/regenerate-placeholder-atlas.mjs b/packages/plugin/template/agent/regenerate-placeholder-atlas.mjs similarity index 100% rename from template/agent/regenerate-placeholder-atlas.mjs rename to packages/plugin/template/agent/regenerate-placeholder-atlas.mjs diff --git a/packages/plugin/tsconfig.json b/packages/plugin/tsconfig.json new file mode 100644 index 0000000..09f33b5 --- /dev/null +++ b/packages/plugin/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "allowImportingTsExtensions": false, + "resolveJsonModule": true, + "strict": true, + "noImplicitOverride": true, + "noUncheckedIndexedAccess": true, + "esModuleInterop": true, + "skipLibCheck": true, + "isolatedModules": true, + "declaration": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "." + }, + "include": ["./*.ts", "./src/**/*.ts"], + "exclude": [ + "./**/*.test.ts", + "./dist/**", + "./node_modules/**" + ] +} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..9f930f5 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +packages: + - "packages/plugin" + - "packages/client-js" + - "schema" diff --git a/schema/README.md b/schema/README.md new file mode 100644 index 0000000..cd5aee6 --- /dev/null +++ b/schema/README.md @@ -0,0 +1,32 @@ +# @tyler-rng/sprite-core-schema + +Wire-protocol source of truth for the SpriteCore plugin and all client SDKs. + +This package is the single TypeBox definition of: + +- `CharacterManifest` — the shape of `node.getCharacterManifest` responses +- `NodeGetCharacterManifestParams` / `Result` — RPC envelopes +- `DISPLAY_CAP_*` / `DISPLAY_MODE_*` — capability + mode string constants +- The `<<>>` / `<<>>` marker grammar + +All four downstream packages — `plugin`, `client-js`, `client-kotlin`, +`client-swift` — derive their types from this file. Kotlin and Swift types +are code-generated from the TypeBox schemas (see `../scripts/`); TS packages +import directly. + +**Never hand-edit Kotlin or Swift wire-type files to add a field.** Edit +`src/display.ts` here, regenerate, and commit all the language outputs +together as one atomic change. + +## Exports + +```ts +import { + CharacterManifestSchema, + CharacterManifest, + NodeGetCharacterManifestResult, + DISPLAY_CAP_SPRITE_HEADSHOT, +} from "@tyler-rng/sprite-core-schema"; + +import { createAvatarMarkerParser, splitByMarkers } from "@tyler-rng/sprite-core-schema/marker"; +``` diff --git a/schema/package.json b/schema/package.json new file mode 100644 index 0000000..e2a5e73 --- /dev/null +++ b/schema/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tyler-rng/sprite-core-schema", + "version": "1.0.0", + "description": "Wire-protocol schema for the SpriteCore plugin + client SDKs (TypeBox)", + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": "./src/index.ts", + "./display": "./src/display.ts", + "./marker": "./src/marker.ts" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Tyler-RNG/sprite-core.git", + "directory": "schema" + }, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.33.0" + }, + "private": true +} diff --git a/schema/src/display.ts b/schema/src/display.ts new file mode 100644 index 0000000..fa0b063 --- /dev/null +++ b/schema/src/display.ts @@ -0,0 +1,215 @@ +import { Type } from "@sinclair/typebox"; + +// Non-empty string primitive. Inlined here so this package has no dependency +// on openclaw core — this is the root of the client contract. +const NonEmptyString = Type.String({ minLength: 1 }); + +// Display capabilities a client advertises in `caps` at pair time. The gateway +// uses these to decide which manifest modes to populate for that client. +export const DISPLAY_CAP_SPRITE_HEADSHOT = "display:sprite-headshot" as const; +export const DISPLAY_CAP_SPRITE_FULLBODY = "display:sprite-fullbody" as const; +export const DISPLAY_CAP_TEXT = "display:text" as const; +export const DISPLAY_CAP_TTS = "display:tts" as const; + +export const DISPLAY_CAPS = [ + DISPLAY_CAP_SPRITE_HEADSHOT, + DISPLAY_CAP_SPRITE_FULLBODY, + DISPLAY_CAP_TEXT, + DISPLAY_CAP_TTS, +] as const; + +// Render modes a character manifest can describe. Clients pick the ones that +// match their caps. Open string so future modes (e.g. "rig-skeletal") don't +// break the schema; DISPLAY_MODE_* constants below are the recommended set. +export const DISPLAY_MODE_HEADSHOT = "headshot" as const; +export const DISPLAY_MODE_FULLBODY = "fullbody" as const; + +const LoopModeSchema = Type.String({ enum: ["infinite", "once", "ping-pong"] }); + +// A single source rectangle inside an atlas image. For non-atlas frame sources +// only `ref` is set (points at a whole-image asset) and rect coords are omitted. +const FrameRefSchema = Type.Object( + { + ref: NonEmptyString, + x: Type.Optional(Type.Integer({ minimum: 0 })), + y: Type.Optional(Type.Integer({ minimum: 0 })), + w: Type.Optional(Type.Integer({ minimum: 1 })), + h: Type.Optional(Type.Integer({ minimum: 1 })), + }, + { additionalProperties: false }, +); + +const FrameSequenceSchema = Type.Object( + { + frames: Type.Array(FrameRefSchema, { minItems: 1 }), + fps: Type.Number({ minimum: 1, maximum: 120 }), + loop: LoopModeSchema, + holdLastFrame: Type.Optional(Type.Boolean()), + iterations: Type.Optional(Type.Integer({ minimum: 1 })), + }, + { additionalProperties: false }, +); + +// An animation is either a single sequence (flat) or a phased trio. The flat +// form is the common case; phases are for states that need smooth entry/exit. +const AnimationSchema = Type.Object( + { + description: Type.Optional(NonEmptyString), + sequence: Type.Optional(FrameSequenceSchema), + intro: Type.Optional(FrameSequenceSchema), + loop: Type.Optional(FrameSequenceSchema), + outro: Type.Optional(FrameSequenceSchema), + }, + { additionalProperties: false }, +); + +// Transition descriptor that runtimes play while swapping animations. Either +// a named phase ("thinking.intro") or an inline blend directive. +const TransitionRefSchema = Type.Union([ + NonEmptyString, + Type.Object( + { + blend: Type.String({ enum: ["crossfade"] }), + ms: Type.Integer({ minimum: 1, maximum: 10_000 }), + }, + { additionalProperties: false }, + ), +]); + +// Per-mode data carried by the manifest. Each mode bundles an optional atlas +// image ref, a per-animation table, and a state-to-animation defaults map. +const ModeContentSchema = Type.Object( + { + atlas: Type.Optional( + Type.Object( + { + image: NonEmptyString, + size: Type.Object( + { + w: Type.Integer({ minimum: 1 }), + h: Type.Integer({ minimum: 1 }), + }, + { additionalProperties: false }, + ), + frameSize: Type.Optional( + Type.Object( + { + w: Type.Integer({ minimum: 1 }), + h: Type.Integer({ minimum: 1 }), + }, + { additionalProperties: false }, + ), + ), + }, + { additionalProperties: false }, + ), + ), + animations: Type.Record(NonEmptyString, AnimationSchema), + transitions: Type.Optional(Type.Record(NonEmptyString, TransitionRefSchema)), + }, + { additionalProperties: false }, +); + +// Asset bundle the client should fetch to render the manifest. Paths are +// gateway-asset-endpoint relative (served under `/openclaw-assets/`). +const AssetBundleSchema = Type.Object( + { + refs: Type.Record(NonEmptyString, NonEmptyString), + }, + { additionalProperties: false }, +); + +// Per-emotion TTS voice-directive override. Applied by clients after they +// parse `<<>>` markers out of assistant text — the text segment that +// follows a marker inherits the base TalkDirective merged field-by-field with +// this override. Fields omitted here fall back to the base directive. +// +// Server-authored (lives in the SpriteCore plugin config); clients never +// invent overrides of their own. Prompt-visible descriptions are intentionally +// NOT shipped on the wire — they're server-only because the plugin is the +// single author of prompt text. +const EmotionDirectiveSchema = Type.Object( + { + voiceId: Type.Optional(NonEmptyString), + stability: Type.Optional(Type.Number({ minimum: 0, maximum: 1 })), + similarity: Type.Optional(Type.Number({ minimum: 0, maximum: 1 })), + style: Type.Optional(Type.Number({ minimum: 0, maximum: 1 })), + speakerBoost: Type.Optional(Type.Boolean()), + speed: Type.Optional(Type.Number({ minimum: 0.25, maximum: 4 })), + audioTag: Type.Optional(NonEmptyString), + }, + { additionalProperties: false }, +); + +const EmotionEntrySchema = Type.Object( + { + directive: Type.Optional(EmotionDirectiveSchema), + }, + { additionalProperties: false }, +); + +export const CharacterManifestSchema = Type.Object( + { + version: Type.Literal(1), + agentId: NonEmptyString, + name: Type.Optional(NonEmptyString), + modes: Type.Array(NonEmptyString, { minItems: 1 }), + stateMap: Type.Record(NonEmptyString, NonEmptyString), + content: Type.Record(NonEmptyString, ModeContentSchema), + assets: AssetBundleSchema, + emotions: Type.Optional(Type.Record(NonEmptyString, EmotionEntrySchema)), + }, + { additionalProperties: false }, +); + +export const NodeGetCharacterManifestParamsSchema = Type.Object( + { + agentId: NonEmptyString, + modes: Type.Optional(Type.Array(NonEmptyString, { minItems: 1 })), + }, + { additionalProperties: false }, +); + +export const NodeGetCharacterManifestResultSchema = Type.Object( + { + manifest: CharacterManifestSchema, + revision: Type.Integer({ minimum: 0 }), + }, + { additionalProperties: false }, +); + +// Re-exported individual schemas for downstream use (AJV compile, codegen). +export { + FrameRefSchema, + FrameSequenceSchema, + AnimationSchema, + TransitionRefSchema, + ModeContentSchema, + AssetBundleSchema, + EmotionDirectiveSchema, + EmotionEntrySchema, + LoopModeSchema, +}; + +import type { Static } from "@sinclair/typebox"; + +export type CharacterManifest = Static; +export type NodeGetCharacterManifestParams = Static< + typeof NodeGetCharacterManifestParamsSchema +>; +export type NodeGetCharacterManifestResult = Static< + typeof NodeGetCharacterManifestResultSchema +>; +export type FrameRef = Static; +export type FrameSequence = Static; +export type Animation = Static; +export type TransitionRef = Static; +export type ModeContent = Static; +export type AssetBundle = Static; +export type EmotionEntry = Static; +export type EmotionDirective = Static; +export type LoopMode = "infinite" | "once" | "ping-pong"; + +// Wire version literal for the CharacterManifest envelope. Bump + fanout to +// every client when making a breaking change to the shape. +export const CHARACTER_MANIFEST_VERSION = 1 as const; diff --git a/schema/src/index.ts b/schema/src/index.ts new file mode 100644 index 0000000..ec77bf0 --- /dev/null +++ b/schema/src/index.ts @@ -0,0 +1,2 @@ +export * from "./display.js"; +export * from "./marker.js"; diff --git a/schema/src/marker.ts b/schema/src/marker.ts new file mode 100644 index 0000000..5dd278b --- /dev/null +++ b/schema/src/marker.ts @@ -0,0 +1,215 @@ +/** + * Streaming parser for avatar-state markers embedded in assistant text. + * + * A marker is the literal text `<<>>` or `<<>>` appearing + * inline anywhere in the reply (not restricted to its own line). Matching + * markers are stripped from the visible text and surfaced separately; invalid + * marker shapes (empty or disallowed state names) are treated as literal text. + * + * The triple-angle-bracket escape is deliberately unusual so the model is + * unlikely to produce it by accident. + * + * The parser is stateful across chunks: a marker split mid-token across two + * chunks is still recognized. Non-marker content is emitted immediately when + * possible so streaming UX isn't delayed. + * + * Play-count semantics: + * - bare `<<>>` — `count = null`, loop until next marker + * - `<<>>` — `count = 0`, equivalent to bare + * - `<<>>` — `count = N >= 1`, play N times then hold last frame + * + * This file is the canonical reference implementation — Kotlin and Swift ports + * must match its semantics. + */ + +export const AVATAR_MARKER_OPEN = "<<<"; +export const AVATAR_MARKER_CLOSE = ">>>"; + +export type AvatarMarker = { + state: string; + count: number | null; +}; + +export type AvatarMarkerParseResult = { + cleanedText: string; + markers: AvatarMarker[]; +}; + +export type AvatarMarkerParser = { + push(chunk: string): AvatarMarkerParseResult; + flush(): AvatarMarkerParseResult; + reset(): void; +}; + +const STATE_NAME_RE = /^[a-zA-Z0-9_-]+$/; + +function isValidStateName(name: string): boolean { + return name.length > 0 && STATE_NAME_RE.test(name); +} + +/** + * Split a raw marker body into (state, count). Triggers on the *last* dash + * when the suffix is a non-negative integer — `head_cocked-1` (N=1) becomes + * `head_cocked` + 1, but `head-cocked` (no digits after dash) stays as + * `head-cocked` + null. Returns null count when the body has no numeric + * suffix. Exported for test coverage. + */ +export function resolveStateAndCount(body: string): { state: string; count: number | null } { + const dashIdx = body.lastIndexOf("-"); + if (dashIdx <= 0 || dashIdx === body.length - 1) return { state: body, count: null }; + const countPart = body.slice(dashIdx + 1); + if (!/^\d+$/.test(countPart)) return { state: body, count: null }; + const count = Number.parseInt(countPart, 10); + if (count < 0) return { state: body, count: null }; + const state = body.slice(0, dashIdx); + if (state.length === 0) return { state: body, count: null }; + return { state, count }; +} + +function processSafePrefix( + combined: string, +): AvatarMarkerParseResult & { remainder: string } { + const markers: AvatarMarker[] = []; + let out = ""; + let i = 0; + + while (i < combined.length) { + const openAt = combined.indexOf(AVATAR_MARKER_OPEN, i); + if (openAt === -1) { + // No complete `<<<` left. But the tail might be a partial start + // (`<` or `<<`) that could extend into a marker with more input; buffer + // those trailing `<` characters so the next chunk can complete them. + let j = combined.length; + while (j > i && combined[j - 1] === "<") { + j -= 1; + } + out += combined.slice(i, j); + return { cleanedText: out, markers, remainder: combined.slice(j) }; + } + out += combined.slice(i, openAt); + const closeAt = combined.indexOf( + AVATAR_MARKER_CLOSE, + openAt + AVATAR_MARKER_OPEN.length, + ); + if (closeAt === -1) { + return { cleanedText: out, markers, remainder: combined.slice(openAt) }; + } + const rawBody = combined.slice(openAt + AVATAR_MARKER_OPEN.length, closeAt); + if (isValidStateName(rawBody)) { + const { state, count } = resolveStateAndCount(rawBody); + markers.push({ state, count }); + } else { + out += combined.slice(openAt, closeAt + AVATAR_MARKER_CLOSE.length); + } + i = closeAt + AVATAR_MARKER_CLOSE.length; + } + + return { cleanedText: out, markers, remainder: "" }; +} + +export function createAvatarMarkerParser(): AvatarMarkerParser { + let buffer = ""; + + return { + push(chunk) { + if (chunk.length === 0) { + return { cleanedText: "", markers: [] }; + } + const combined = buffer + chunk; + const { cleanedText, markers, remainder } = processSafePrefix(combined); + buffer = remainder; + return { cleanedText, markers }; + }, + flush() { + if (buffer.length === 0) { + return { cleanedText: "", markers: [] }; + } + const leftover = buffer; + buffer = ""; + return { cleanedText: leftover, markers: [] }; + }, + reset() { + buffer = ""; + }, + }; +} + +export function parseAvatarMarkers(text: string): AvatarMarkerParseResult { + const parser = createAvatarMarkerParser(); + const first = parser.push(text); + const last = parser.flush(); + return { + cleanedText: first.cleanedText + last.cleanedText, + markers: [...first.markers, ...last.markers], + }; +} + +/** + * Text segment produced by [splitByMarkers]. `emotion` is the state name of + * the marker immediately preceding this segment, or `null` for the leading + * segment (before any marker) and for segments introduced by an invalid + * marker shape (emitted as literal text). + */ +export type TextSegmentWithEmotion = { + text: string; + emotion: string | null; + emotionCount: number | null; +}; + +/** + * Split `text` into segments delimited by `<<>>` markers. Each + * segment carries the preceding marker's state as its `emotion` (null for + * the leading segment before any marker). + * + * Invalid marker shapes are treated as literal text and merged into the + * enclosing segment. Empty-text segments are dropped. + */ +export function splitByMarkers(text: string): TextSegmentWithEmotion[] { + if (text.length === 0) return []; + const segments: TextSegmentWithEmotion[] = []; + let currentText = ""; + let currentEmotion: string | null = null; + let currentEmotionCount: number | null = null; + let i = 0; + while (i < text.length) { + const openAt = text.indexOf(AVATAR_MARKER_OPEN, i); + if (openAt === -1) { + currentText += text.slice(i); + break; + } + currentText += text.slice(i, openAt); + const closeAt = text.indexOf( + AVATAR_MARKER_CLOSE, + openAt + AVATAR_MARKER_OPEN.length, + ); + if (closeAt === -1) { + currentText += text.slice(openAt); + break; + } + const rawBody = text.slice(openAt + AVATAR_MARKER_OPEN.length, closeAt); + if (isValidStateName(rawBody)) { + const { state, count } = resolveStateAndCount(rawBody); + if (currentText.length > 0) { + segments.push({ + text: currentText, + emotion: currentEmotion, + emotionCount: currentEmotionCount, + }); + currentText = ""; + } + currentEmotion = state; + currentEmotionCount = count; + } else { + currentText += text.slice(openAt, closeAt + AVATAR_MARKER_CLOSE.length); + } + i = closeAt + AVATAR_MARKER_CLOSE.length; + } + if (currentText.length > 0) { + segments.push({ + text: currentText, + emotion: currentEmotion, + emotionCount: currentEmotionCount, + }); + } + return segments; +} diff --git a/schema/tsconfig.json b/schema/tsconfig.json new file mode 100644 index 0000000..7dde006 --- /dev/null +++ b/schema/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "esModuleInterop": true, + "skipLibCheck": true, + "isolatedModules": true, + "declaration": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["./src/**/*.ts"], + "exclude": ["./dist/**", "./node_modules/**", "./src/**/*.test.ts"] +} diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index b8a85a9..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "../tsconfig.package-boundary.base.json", - "compilerOptions": { - "rootDir": "." - }, - "include": ["./*.ts", "./src/**/*.ts"], - "exclude": [ - "./**/*.test.ts", - "./dist/**", - "./node_modules/**", - "./src/test-support/**", - "./src/**/*test-helpers.ts", - "./src/**/*test-harness.ts", - "./src/**/*test-support.ts" - ] -} From 6be78f01b24af24c2aaa833dd839b5db4bb23f09 Mon Sep 17 00:00:00 2001 From: Tyler-RNG Date: Thu, 23 Apr 2026 15:55:23 -0400 Subject: [PATCH 05/10] Wire build + release pipeline for all four languages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full CI + release setup so a single v tag publishes every artifact to GitHub Packages (plus SwiftPM via the tag itself). **Build:** - TS typecheck + LoopMode schema narrowed with Type.Literal so downstream Static-derived types are a string union, not a wide string. - Plugin pinned back to openclaw@2026.4.15-beta.1 (the version its code was originally written against); pre-existing plugin typecheck drift against that SDK is noted in ci.yml and deferred to a follow-up. - pnpm-lock.yaml generated; @openclaw/plugin-sdk vestigial devDep removed (plugin imports from openclaw/plugin-sdk/* subpaths only). **CI (ci.yml):** - metadata job: validates JSON across workspace + fixtures + runs check-versions.mjs to catch drift early. - typescript: pnpm build + test + typecheck. - kotlin: Gradle :core + :android build + test via setup-gradle (no wrapper committed). - swift: swift test on macos-latest. **Release (release.yml):** - verify-versions gates everything on the git tag matching every package's declared version. - publish-plugin + publish-client-js npm-publish to GitHub Packages. - publish-client-kotlin runs `gradle :core:publish :android:publish -Pversion=` against maven.pkg.github.com. - validate-client-swift runs `swift test` on macos — no publish; SwiftPM consumes the git tag. **Kotlin publish config:** - Both modules now declare a GitHubPackages maven repository using GITHUB_ACTOR / GITHUB_TOKEN (auto-provided in CI, gpr.user/gpr.key fallback for local publishes). - SCM blocks added to POMs. **Version sync:** - scripts/check-versions.mjs walks all four packages' declared versions and the Gradle version fallbacks, asserts agreement. Called from CI and the release workflow. **Fixture runner:** - packages/client-js/src/fixture-runner.test.ts walks ../../fixtures/, dispatches on `kind`, runs every case. 23 fixture cases now enforce the TS implementation matches the shared oracle. Kotlin + Swift equivalents to come in follow-up commits. **Docs:** - CHANGELOG.md seeded with unreleased notes. - docs/RELEASING.md documents the tag flow, consumer install instructions for each language, local publish fallbacks, rollback/yank, and the secrets checklist (no extra secrets needed beyond the auto GITHUB_TOKEN). All 42 client-js tests pass, including the 23 fixture-loader tests. --- .github/workflows/ci.yml | 92 +- .github/workflows/release.yml | 107 +- CHANGELOG.md | 48 + docs/RELEASING.md | 146 + packages/client-js/src/fixture-runner.test.ts | 200 + .../client-kotlin/android/build.gradle.kts | 14 + packages/client-kotlin/core/build.gradle.kts | 16 +- packages/plugin/package.json | 26 +- pnpm-lock.yaml | 8489 +++++++++++++++++ schema/src/display.ts | 10 +- scripts/check-versions.mjs | 78 + 11 files changed, 9179 insertions(+), 47 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 docs/RELEASING.md create mode 100644 packages/client-js/src/fixture-runner.test.ts create mode 100644 pnpm-lock.yaml create mode 100755 scripts/check-versions.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7a87b4..9a0d5d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,21 +6,32 @@ on: pull_request: jobs: - plugin-smoke: + # Fast "does the metadata parse" job that gates everything else. + metadata: runs-on: ubuntu-latest - defaults: - run: - working-directory: packages/plugin steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 - - name: Validate package.json + - name: Validate root package.json run: node -e "require('./package.json')" - - name: Validate openclaw.plugin.json - run: node -e "require('./openclaw.plugin.json')" - - name: Verify name matches install.npmSpec + - name: Validate plugin package.json + openclaw.plugin.json + run: | + node -e "require('./packages/plugin/package.json')" + node -e "require('./packages/plugin/openclaw.plugin.json')" + - name: Validate client-js package.json + run: node -e "require('./packages/client-js/package.json')" + - name: Validate schema package.json + run: node -e "require('./schema/package.json')" + - name: Validate fixture JSON + run: | + set -e + find fixtures -name '*.json' | while read f; do + node -e "JSON.parse(require('fs').readFileSync('$f', 'utf8'))" + done + - name: Verify plugin name matches install.npmSpec + working-directory: packages/plugin run: | node -e " const pkg = require('./package.json'); @@ -30,25 +41,60 @@ jobs: process.exit(1); } " + - name: Verify version alignment across packages + run: node scripts/check-versions.mjs - # Client SDKs — deeper build/test wiring lands in a follow-up once the - # build conversation settles (pnpm install deps, Gradle wrapper, swift test - # container). For now, just validate JSON shape on every push. - workspace-smoke: + typescript: runs-on: ubuntu-latest + needs: metadata steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 - - name: Validate workspace root package.json - run: node -e "require('./package.json')" - - name: Validate client-js package.json - run: node -e "require('./packages/client-js/package.json')" - - name: Validate schema package.json - run: node -e "require('./schema/package.json')" - - name: Validate fixture JSON files - run: | - find fixtures -name '*.json' | while read f; do - node -e "JSON.parse(require('fs').readFileSync('$f', 'utf8'))" || exit 1 - done + - uses: pnpm/action-setup@v4 + with: + version: 9 + - name: pnpm install + run: pnpm install --frozen-lockfile + - name: Build schema + client-js + run: pnpm --filter ./schema --filter ./packages/client-js run --if-present build + - name: Test client-js + run: pnpm --filter ./packages/client-js test + - name: Typecheck client-js + run: pnpm --filter ./packages/client-js typecheck + # Plugin typecheck has pre-existing drift against the pinned openclaw + # version (see PR description). Skipped until that's sorted in a + # follow-up; uncomment once the plugin is re-aligned with the openclaw + # plugin-sdk it targets. + # - name: Typecheck plugin + # run: pnpm --filter ./packages/plugin typecheck + + kotlin: + runs-on: ubuntu-latest + needs: metadata + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + - uses: gradle/actions/setup-gradle@v4 + - name: Gradle build + test (:core, :android) + working-directory: packages/client-kotlin + run: gradle :core:build :core:test :android:build :android:test --no-daemon + + swift: + runs-on: macos-latest + needs: metadata + steps: + - uses: actions/checkout@v4 + - uses: swift-actions/setup-swift@v2 + with: + swift-version: "5.10" + - name: swift build + working-directory: packages/client-swift + run: swift build + - name: swift test + working-directory: packages/client-swift + run: swift test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 061282b..066d9e5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,14 +5,36 @@ on: tags: - 'v*' -# TODO (build conversation): the client SDKs (client-js, client-kotlin, -# client-swift) need their own publish paths — Maven Central / Maven GitHub -# Packages for Kotlin, tagged Git for SwiftPM, and a separate npm publish for -# client-js. Today this workflow only publishes the plugin to GitHub Packages -# from packages/plugin/. See the repo-root README for the plan. +# A single v tag publishes all four artifacts from one commit: +# - @tyler-rng/sprite-core (npm — GitHub Packages) +# - @tyler-rng/sprite-core-client (npm — GitHub Packages) +# - ai.openclaw.spritecore:sprite-core-client(-android) (Maven — GitHub Packages) +# - SpriteCoreClient (SwiftPM consumes the git tag directly) +# +# All publish jobs depend on `verify-versions`, which fails the whole release +# if any package.json / Gradle version disagrees with the tag. jobs: + verify-versions: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.resolve.outputs.version }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - id: resolve + name: Resolve tag version + run: | + TAG_VERSION="${GITHUB_REF_NAME#v}" + echo "version=$TAG_VERSION" >> "$GITHUB_OUTPUT" + echo "TAG_VERSION=$TAG_VERSION" >> "$GITHUB_ENV" + - name: Enforce all packages match tag + run: node scripts/check-versions.mjs "$TAG_VERSION" + publish-plugin: + needs: verify-versions runs-on: ubuntu-latest permissions: contents: read @@ -27,17 +49,72 @@ jobs: node-version: 22 registry-url: 'https://npm.pkg.github.com' scope: '@tyler-rng' - - - name: Verify tag matches plugin package version - run: | - TAG_VERSION="${GITHUB_REF_NAME#v}" - PKG_VERSION=$(node -p "require('./package.json').version") - if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then - echo "Tag $GITHUB_REF_NAME (version $TAG_VERSION) does not match plugin package.json version $PKG_VERSION" - exit 1 - fi - - name: Publish plugin to GitHub Packages run: npm publish env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + publish-client-js: + needs: verify-versions + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + registry-url: 'https://npm.pkg.github.com' + scope: '@tyler-rng' + - uses: pnpm/action-setup@v4 + with: + version: 9 + - name: pnpm install + run: pnpm install --frozen-lockfile + - name: Build schema + client-js + run: pnpm --filter ./schema --filter ./packages/client-js run --if-present build + - name: Publish client-js to GitHub Packages + working-directory: packages/client-js + run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + publish-client-kotlin: + needs: verify-versions + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + - uses: gradle/actions/setup-gradle@v4 + - name: Publish :core + :android to GitHub Packages + working-directory: packages/client-kotlin + env: + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gradle \ + :core:publishMavenPublicationToGitHubPackagesRepository \ + :android:publishReleasePublicationToGitHubPackagesRepository \ + -Pversion="${{ needs.verify-versions.outputs.version }}" \ + --no-daemon + + validate-client-swift: + needs: verify-versions + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - uses: swift-actions/setup-swift@v2 + with: + swift-version: "5.10" + - name: swift test + working-directory: packages/client-swift + run: swift test + # SwiftPM consumers pull by git tag — no publish step. This job exists + # purely to gate the tag: if swift test fails, the tag shouldn't stand. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d07e134 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,48 @@ +# Changelog + +All four packages in this repo (`@tyler-rng/sprite-core`, +`@tyler-rng/sprite-core-client`, `ai.openclaw.spritecore:sprite-core-client` +and `-android`, `SpriteCoreClient`) release together at one version. Tag +format: `v` (e.g. `v1.0.0`). + +## Unreleased + +### Added + +- Workspace layout: `packages/plugin`, `packages/client-js`, + `packages/client-kotlin`, `packages/client-swift`, plus shared `schema/` + and `fixtures/`. +- Cross-language client SDKs (TypeScript, Kotlin, Swift) implementing the + `CharacterManifest` wire protocol: `AnimationGraph` projection, coroutine / + async-iterator / `actor` sprite player, `FrameSource` adapter seam, + streaming `<<>>` / `<<>>` marker parser, asset cache. +- `schema/` package publishing TypeBox definitions as the single source of + truth for wire types (downstream Kotlin and Swift types kept in lockstep + via the `fixtures/` conformance suite). +- Release workflow publishing all four artifacts from a single `v*` tag: + - `@tyler-rng/sprite-core` → npm (GitHub Packages) + - `@tyler-rng/sprite-core-client` → npm (GitHub Packages) + - `ai.openclaw.spritecore:sprite-core-client` + `-android` → Maven + (GitHub Packages) + - `SpriteCoreClient` — consumed via git tag by SwiftPM +- `scripts/check-versions.mjs` — pre-flight gate that fails the release if + any package's declared version disagrees with the tag. + +### Changed + +- Plugin moved from repo root into `packages/plugin/`. Package name and + `install.npmSpec` unchanged — operators continue to install + `@tyler-rng/sprite-core` via the existing instructions. + +### Fixed + +- TypeScript marker parser upgraded to match Kotlin semantics (`<<>>` + play-count suffix). All three language ports now produce identical parse + results for the same inputs. +- `emotions` field added to the Kotlin `CharacterManifest` data class + (previously drifted from the TypeScript wire schema). + +## 1.0.0 — 2026-04-23 + +Initial release of the plugin extracted from `openclaw-src/extensions/sprite-core/`. +See `packages/plugin/README.md` for plugin-specific documentation. diff --git a/docs/RELEASING.md b/docs/RELEASING.md new file mode 100644 index 0000000..fed05eb --- /dev/null +++ b/docs/RELEASING.md @@ -0,0 +1,146 @@ +# Releasing + +This repo ships four artifacts from one git tag. Everything publishes to +GitHub Packages (npm + Maven) under the `Tyler-RNG/sprite-core` repository +scope, plus SwiftPM consumes the git tag directly. + +## One-time setup (done once per repo) + +No additional secrets are required. GitHub Actions automatically provides a +`GITHUB_TOKEN` with `packages: write` to the release workflow — that's the +only credential needed to publish to GitHub Packages from CI. + +For SwiftPM, there is nothing to configure — consumers pull by git tag. + +## Cutting a release + +1. Ensure `main` is green on CI. +2. Bump every `version` field to the new version (must match exactly): + - `packages/plugin/package.json` + - `packages/client-js/package.json` + - `schema/package.json` + - `packages/client-kotlin/core/build.gradle.kts` (the literal in the + `version = findProperty("version")?.toString() ?: "X.Y.Z"` fallback) + - `packages/client-kotlin/android/build.gradle.kts` (same) + - Swift has no declared version — the git tag IS the Swift version. +3. Verify locally: + ``` + node scripts/check-versions.mjs + ``` +4. Update `CHANGELOG.md`: move items from **Unreleased** into a new + `## X.Y.Z — YYYY-MM-DD` heading. +5. Commit: + ``` + git commit -am "Release vX.Y.Z" + ``` +6. Tag: + ``` + git tag vX.Y.Z + ``` +7. Push both: + ``` + git push origin main --follow-tags + ``` +8. Watch the Release workflow. The order is: + 1. `verify-versions` — fails the release if `check-versions.mjs` disagrees + with the tag. + 2. In parallel: `publish-plugin`, `publish-client-js`, + `publish-client-kotlin`, `validate-client-swift`. + + A single failure in any of those jobs stops that artifact from publishing + (the others may already have shipped; this is an intentional tradeoff — + each artifact publish is idempotent per-version, so a retry after fix is + safe). + +## Consuming the releases + +### npm packages (plugin + client-js) + +Both are scoped to `@tyler-rng` and live on GitHub Packages. A consumer +needs to point the scope at GitHub Packages in their `.npmrc`: + +``` +@tyler-rng:registry=https://npm.pkg.github.com +//npm.pkg.github.com/:_authToken=${GITHUB_READ_PACKAGES_TOKEN} +``` + +The token only needs the `read:packages` scope. See +`packages/plugin/README.md` for the private-beta install recipe (this is +already documented for the plugin). + +### Maven (Kotlin core + android) + +From a consuming Gradle project: + +```kotlin +// settings.gradle.kts +dependencyResolutionManagement { + repositories { + mavenCentral() + google() + maven { + url = uri("https://maven.pkg.github.com/Tyler-RNG/sprite-core") + credentials { + username = providers.gradleProperty("gpr.user").orNull + ?: System.getenv("GITHUB_ACTOR") + password = providers.gradleProperty("gpr.key").orNull + ?: System.getenv("GITHUB_TOKEN") + } + } + } +} + +// app/build.gradle.kts +dependencies { + implementation("ai.openclaw.spritecore:sprite-core-client:1.0.0") + implementation("ai.openclaw.spritecore:sprite-core-client-android:1.0.0") +} +``` + +Put `gpr.user` + `gpr.key` in `~/.gradle/gradle.properties` (with +`gpr.key` = a PAT with `read:packages`) so local builds can resolve +without env vars. + +### SwiftPM (Swift client) + +```swift +.package(url: "https://github.com/Tyler-RNG/sprite-core.git", from: "1.0.0") +``` + +Then depend on the `"SpriteCoreClient"` product. SwiftPM resolves by git +tag, so no auth is needed beyond the repo clone — if your consuming Xcode +project is on a machine that can already git-clone the repo, it'll work. + +## Publishing locally (rarely needed) + +Only useful for ad-hoc testing. Don't cut real releases this way — use the +workflow so every artifact goes out at the same version. + +- **npm**: `cd packages/plugin && npm publish` (requires + `NODE_AUTH_TOKEN` in env, `write:packages` scope PAT). +- **npm (client-js)**: `cd packages/client-js && pnpm build && npm publish`. +- **Gradle**: `cd packages/client-kotlin && GITHUB_ACTOR=you GITHUB_TOKEN=... gradle :core:publish :android:publish -Pversion=X.Y.Z`. +- **SwiftPM**: no local publish — just tag + push. + +## Rollback / yank + +- **npm (GitHub Packages)**: `npm unpublish @tyler-rng/sprite-core@X.Y.Z` is + allowed for 72h after publish. After that, publish a `X.Y.(Z+1)` that + supersedes it. +- **Maven (GitHub Packages)**: versions are immutable once published. Bump + and ship a new one. +- **SwiftPM (git tag)**: `git tag -d vX.Y.Z && git push --delete origin vX.Y.Z` + plus any SPM cache buster. Don't rewrite history — create a superseding + tag instead. + +## Secrets checklist + +| Secret | Where | Why | +|---|---|---| +| `GITHUB_TOKEN` | auto-provided by GitHub Actions | npm + Maven publish to GitHub Packages | +| `read:packages` PAT | operator local `~/.npmrc` | pull `@tyler-rng/*` from GitHub Packages | +| `read:packages` PAT | operator local `~/.gradle/gradle.properties` | pull `ai.openclaw.spritecore:*` from Maven GitHub Packages | +| `write:packages` PAT | *only* if publishing locally | not needed for normal CI releases | + +No extra org/team secrets need to be configured in the repo's Settings → +Secrets and variables. diff --git a/packages/client-js/src/fixture-runner.test.ts b/packages/client-js/src/fixture-runner.test.ts new file mode 100644 index 0000000..f725b35 --- /dev/null +++ b/packages/client-js/src/fixture-runner.test.ts @@ -0,0 +1,200 @@ +import { describe, expect, it } from "vitest"; +import { readFileSync, readdirSync, statSync } from "node:fs"; +import { join, dirname, relative } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { + AnimationGraph, + InMemorySpriteSource, + SpriteAnimationPlayer, + createAvatarMarkerParser, +} from "./index.js"; +import type { CharacterManifest, FrameRef } from "./schema.js"; +import type { Ticker } from "./ticker.js"; + +// Walk up to the repo root (two levels above packages/client-js). +const fixturesRoot = join( + dirname(fileURLToPath(import.meta.url)), + "..", + "..", + "..", + "fixtures", +); + +type AnyFixture = + | ManifestFixture + | MarkerFixture + | AnimationGraphFixture + | SpritePlayerFixture; + +type ManifestFixture = { + kind: "manifest"; + description: string; + manifest: CharacterManifest; +}; + +type MarkerFixture = { + kind: "marker"; + description: string; + cases: { + name: string; + chunks: string[]; + expectedCleanedText: string; + expectedMarkers: { state: string; count: number | null }[]; + }[]; +}; + +type TransitionExpectation = string | { blend: string; ms: number } | null; + +type AnimationGraphFixture = { + kind: "animation-graph"; + description: string; + manifest: CharacterManifest; + mode: string; + cases: { + name: string; + resolveTransition: { from: string; to: string }; + expected: TransitionExpectation; + }[]; +}; + +type SpritePlayerFixture = { + kind: "sprite-player"; + description: string; + manifest: CharacterManifest; + mode: string; + requests: { target: string; playCount: number | null; advanceMs: number }[]; + expectedRefSequencePrefix: string[]; + expectedHoldRef?: string; +}; + +function listFixtureFiles(dir: string): string[] { + const out: string[] = []; + for (const entry of readdirSync(dir)) { + const full = join(dir, entry); + const st = statSync(full); + if (st.isDirectory()) out.push(...listFixtureFiles(full)); + else if (entry.endsWith(".json")) out.push(full); + } + return out; +} + +function loadFixture(path: string): AnyFixture { + return JSON.parse(readFileSync(path, "utf8")) as AnyFixture; +} + +/** Fake ticker that resolves each delay on the next microtask. */ +class MicrotaskTicker implements Ticker { + async delay(_ms: number): Promise { + await Promise.resolve(); + } +} + +async function flushMicrotasks(n: number): Promise { + for (let i = 0; i < n; i++) await Promise.resolve(); +} + +async function collectPlayerRefs( + fixture: SpritePlayerFixture, +): Promise<{ emitted: FrameRef[]; holdRef: FrameRef | null }> { + const graph = AnimationGraph.fromManifest(fixture.manifest, fixture.mode); + const player = new SpriteAnimationPlayer(graph, new MicrotaskTicker()); + const emitted: FrameRef[] = []; + let lastRef: FrameRef | null = null; + const unsub = player.currentRef.subscribe((ref) => { + if (ref && ref !== lastRef) { + emitted.push(ref); + lastRef = ref; + } + }); + // Let the default-state start-up run. + await flushMicrotasks(40); + for (const req of fixture.requests) { + await player.requestState(req.target, req.playCount); + await flushMicrotasks(Math.max(20, Math.floor(req.advanceMs / 8))); + } + const holdRef = player.currentRef.value; + unsub(); + await player.dispose(); + return { emitted, holdRef }; +} + +function runMarkerCase(c: MarkerFixture["cases"][number]): void { + const parser = createAvatarMarkerParser(); + let cleaned = ""; + const markers: { state: string; count: number | null }[] = []; + for (const chunk of c.chunks) { + const r = parser.push(chunk); + cleaned += r.cleanedText; + markers.push(...r.markers); + } + const tail = parser.flush(); + cleaned += tail.cleanedText; + markers.push(...tail.markers); + expect(cleaned).toBe(c.expectedCleanedText); + expect(markers).toEqual(c.expectedMarkers); +} + +function runAnimationGraphCase( + fixture: AnimationGraphFixture, + c: AnimationGraphFixture["cases"][number], +): void { + const graph = AnimationGraph.fromManifest(fixture.manifest, fixture.mode); + const actual = graph.resolveTransition(c.resolveTransition.from, c.resolveTransition.to); + if (c.expected === null) { + expect(actual).toBeNull(); + } else if (typeof c.expected === "string") { + expect(actual).toBe(c.expected); + } else { + expect(actual).toEqual(c.expected); + } +} + +describe("fixtures", () => { + const files = listFixtureFiles(fixturesRoot); + for (const file of files) { + const rel = relative(fixturesRoot, file); + const fixture = loadFixture(file); + describe(rel, () => { + if (fixture.kind === "manifest") { + it("decodes", () => { + // Round-trip through JSON.parse → schema types. The TS types are + // structural, so just asserting the fields exist is enough; the + // AJV validators live in the schema package and aren't needed + // here. + expect(fixture.manifest.version).toBeTypeOf("number"); + expect(fixture.manifest.agentId.length).toBeGreaterThan(0); + expect(fixture.manifest.modes.length).toBeGreaterThan(0); + }); + } else if (fixture.kind === "marker") { + for (const c of fixture.cases) { + it(c.name, () => { + runMarkerCase(c); + }); + } + } else if (fixture.kind === "animation-graph") { + for (const c of fixture.cases) { + it(c.name, () => { + runAnimationGraphCase(fixture, c); + }); + } + } else if (fixture.kind === "sprite-player") { + it("emits the expected prefix", async () => { + const { emitted, holdRef } = await collectPlayerRefs(fixture); + const emittedRefs = emitted.map((r) => r.ref); + expect(emittedRefs.slice(0, fixture.expectedRefSequencePrefix.length)).toEqual( + fixture.expectedRefSequencePrefix, + ); + if (fixture.expectedHoldRef !== undefined) { + expect(holdRef?.ref).toBe(fixture.expectedHoldRef); + } + }); + } else { + const anyFixture = fixture as { kind: string }; + it("unknown kind", () => { + throw new Error(`unknown fixture kind: ${anyFixture.kind}`); + }); + } + }); + } +}); diff --git a/packages/client-kotlin/android/build.gradle.kts b/packages/client-kotlin/android/build.gradle.kts index 88e99bb..617f593 100644 --- a/packages/client-kotlin/android/build.gradle.kts +++ b/packages/client-kotlin/android/build.gradle.kts @@ -63,6 +63,20 @@ afterEvaluate { url.set("https://opensource.org/licenses/MIT") } } + scm { + connection.set("scm:git:git://github.com/Tyler-RNG/sprite-core.git") + url.set("https://github.com/Tyler-RNG/sprite-core") + } + } + } + } + repositories { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/Tyler-RNG/sprite-core") + credentials { + username = System.getenv("GITHUB_ACTOR") ?: findProperty("gpr.user")?.toString() + password = System.getenv("GITHUB_TOKEN") ?: findProperty("gpr.key")?.toString() } } } diff --git a/packages/client-kotlin/core/build.gradle.kts b/packages/client-kotlin/core/build.gradle.kts index 2178d0d..2cf42b8 100644 --- a/packages/client-kotlin/core/build.gradle.kts +++ b/packages/client-kotlin/core/build.gradle.kts @@ -46,9 +46,21 @@ publishing { url.set("https://opensource.org/licenses/MIT") } } + scm { + connection.set("scm:git:git://github.com/Tyler-RNG/sprite-core.git") + url.set("https://github.com/Tyler-RNG/sprite-core") + } + } + } + } + repositories { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/Tyler-RNG/sprite-core") + credentials { + username = System.getenv("GITHUB_ACTOR") ?: findProperty("gpr.user")?.toString() + password = System.getenv("GITHUB_TOKEN") ?: findProperty("gpr.key")?.toString() } } } - // Target registry is configured via -Pregistry= or env in CI; see - // packages/client-kotlin/README.md. } diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 1ef2b5a..8243ecd 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -5,15 +5,30 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://github.com/Tyler-RNG/sprite-core.git" + "url": "git+https://github.com/Tyler-RNG/sprite-core.git", + "directory": "packages/plugin" }, "license": "MIT", + "files": [ + "index.ts", + "src", + "template", + "openclaw.plugin.json", + "tsconfig.json", + "LICENSE", + "README.md" + ], + "scripts": { + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, "devDependencies": { - "@openclaw/plugin-sdk": "2026.4.15-beta.1", - "openclaw": "2026.4.15-beta.1" + "openclaw": "2026.4.15-beta.1", + "typescript": "^5.6.0", + "vitest": "^2.0.0" }, "peerDependencies": { - "openclaw": ">=2026.4.15-beta.1" + "openclaw": ">=2026.4.10" }, "peerDependenciesMeta": { "openclaw": { @@ -35,5 +50,8 @@ "build": { "openclawVersion": "2026.4.15-beta.1" } + }, + "publishConfig": { + "registry": "https://npm.pkg.github.com" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..5f056b9 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,8489 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: {} + + packages/client-js: + dependencies: + '@tyler-rng/sprite-core-schema': + specifier: workspace:* + version: link:../../schema + devDependencies: + typescript: + specifier: ^5.6.0 + version: 5.9.3 + vitest: + specifier: ^2.0.0 + version: 2.1.9(@types/node@25.6.0) + + packages/plugin: + devDependencies: + openclaw: + specifier: 2026.4.15-beta.1 + version: 2026.4.15-beta.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@napi-rs/canvas@0.1.99)(@types/express@5.0.6)(apache-arrow@18.1.0)(hono@4.12.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3) + typescript: + specifier: ^5.6.0 + version: 5.9.3 + vitest: + specifier: ^2.0.0 + version: 2.1.9(@types/node@25.6.0) + + schema: + dependencies: + '@sinclair/typebox': + specifier: ^0.33.0 + version: 0.33.22 + +packages: + + '@agentclientprotocol/sdk@0.18.2': + resolution: {integrity: sha512-l/o9NKvUc00GPa6RFJ4AccQq2O/PAf83xQ75mThHuL3H571iN4+PEdwnTBez67sS8Nv2aSA373xCZ5CbTXEwzA==} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + '@anthropic-ai/sdk@0.73.0': + resolution: {integrity: sha512-URURVzhxXGJDGUGFunIOtBlSl7KWvZiAAKY/ttTkZAkXT9bTPqdk2eK0b8qqSxXpikh3QKPnPYpiyX98zf5ebw==} + hasBin: true + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + + '@anthropic-ai/sdk@0.90.0': + resolution: {integrity: sha512-MzZtPabJF1b0FTDl6Z6H5ljphPwACLGP13lu8MTiB8jXaW/YXlpOp+Po2cVou3MPM5+f5toyLnul9whKCy7fBg==} + hasBin: true + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + + '@anthropic-ai/vertex-sdk@0.15.0': + resolution: {integrity: sha512-i2LDdu6VB8Lqqip+kbNSXRxQgFsCg6GPBO/X2zRJwLl99dNzf28nb6Rdi0EodONXsyJfY2TKdGR+y5l1/AKFEg==} + + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-bedrock-runtime@3.1028.0': + resolution: {integrity: sha512-FFdtkxWFmKX1Ka/vjDRKpYsm0/HTlab5qpHl8LAXRmJjhSSiLGiCnJYsYFN+zp3NucL02kM1DlpFU8Xnm7d8Ng==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-bedrock-runtime@3.1036.0': + resolution: {integrity: sha512-kAShlMn923dTxsrwFM5huDcjMGGg6R5+wlr1XQxFUKrm4i2IBZ8h4UMQmthpfJTkxfjznCwTB8pa117QSh/gyA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-bedrock@3.1028.0': + resolution: {integrity: sha512-YEUikjoImgUjv2UEpnD/WP0JiLdoLRnkajnSQR9LPCa8+BGy3+j879jimPlAuypOux1/CgqMA7Fwt13IpF2+UA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-cognito-identity@3.1036.0': + resolution: {integrity: sha512-MQjdqPph5ZwaG6bGHeEr480NLFskTdr+ZEqXOoiBlwJUCy1sXHT3S/xrUAIVvGX93OetjOMbC81BHxNUHd6TkA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.974.5': + resolution: {integrity: sha512-lMPlYlYfQdNZhlkJgnkmESwrY+hNh3PljmZ+37oAqLNdJ6rnILAwFSyc6B3bJeDOtMORNnMQIej0aTRuOlDyhQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-cognito-identity@3.972.28': + resolution: {integrity: sha512-UXhc4FfxbfNaIqycDnIZ+W8CMAoCtcJJfZkq+cWSUwQRN0V0d0uAoN2qCFyKZip8inlHeKJmNQsPliKKcElP8Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.31': + resolution: {integrity: sha512-X/yGB73LmDW/6MdDJGCDzZBUXnM3ys4vs9l+5ZTJmiEswDdP1OjeoAFlFjVGS9o4KB2wZWQ9KOfdVNSSK6Ep3w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.33': + resolution: {integrity: sha512-c0ZF+lwoWVvX5iCaGKL5T/4DnIw88CGqxA0BcBs3U86mIp5EZYPVg+KSPkMXOyokmADvNewiMUfSG2uFwjRp0g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.972.35': + resolution: {integrity: sha512-jsU4u/cRkKFLKQS0k918FQ27fzXLG5ENiLWQMYE6581zLeI2hWh04ptlrvZMB3wJT/5d+vSzJk74X1CMFr4y8Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.35': + resolution: {integrity: sha512-5oa3j0cA50jPqgNhZ9XdJVopuzUf1klRb28/2MfLYWWiPi9DRVvbrBWT+DidbHTT36520VuXZJahQwR+YgSjrg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.30': + resolution: {integrity: sha512-FMnAnWxc8PG+ZrZ2OBKzY4luCUJhe9CG0B9YwYr4pzrYGLXBS2rl+UoUvjGbAwiptxRL6hyA3lFn03Bv1TLqTw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.36': + resolution: {integrity: sha512-4nT2T8Z7vH8KE9EdjEsuIlHpZSlcaK2PrKbQBjuUGU46BCCzF3WvP0u0Uiosni3Ykmmn4rWLVawoOCLotUtCbg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.31': + resolution: {integrity: sha512-eKeT4MXumpBJsrDLCYcSzIkFPVTFn/es7It2oogp2OhU/ic7P/+xzFpQx9ZhwtXS57Mc5S42BPWi7lHmvs/nYg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.972.35': + resolution: {integrity: sha512-bCuBdfnj0KGDMdLp6utMTLiJcFN2ek9EgZinxQZZSc3FxjJ/HSqeqab2cjbnoNfy8RM6suDCsRkmVY1izp9I+A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.35': + resolution: {integrity: sha512-swW6Bwvl8lanyEMtZOWE/oR6yqcRQH4HTQZUVsnDVgoXvRjRywpYpLv2BWwjUFyjPrqsdX6FeTkf4tMSe/qFTQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-providers@3.1036.0': + resolution: {integrity: sha512-7ZIp9c9MXhBhTHLsdKluREogxoazjenIUERGmoXj6Y2GtpgCqpUYqk5550sA4BytLE1mDExbUqKWEBY6jvTwmw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/eventstream-handler-node@3.972.14': + resolution: {integrity: sha512-m4X56gxG76/CKfxNVbOFuYwnAZcHgS6HOH8lgp15HoGHIAVTcZfZrXvcYzJFOMLEJgVn+JHBu6EiNV+xSNXXFg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-eventstream@3.972.10': + resolution: {integrity: sha512-QUqLs7Af1II9X4fCRAu+EGHG3KHyOp4RkuLhRKoA3NuFlh6TL8i+zXBl8w2LUxqm44B/Kom45hgSlwA1SpTsXQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-host-header@3.972.10': + resolution: {integrity: sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-logger@3.972.10': + resolution: {integrity: sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-recursion-detection@3.972.11': + resolution: {integrity: sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.972.34': + resolution: {integrity: sha512-/UL96JKjsjdodcRRMKl99tLQvK6Oi9ptLC9iU1yiTF/ruaDX0mtBBtnLNZDxIZRJOCVOtB49ed1YaTadqygk8Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-user-agent@3.972.35': + resolution: {integrity: sha512-hOFWNOjVmOocpRlrU04nYxjMOeoe0Obu5AXEuhB8zblMCPl3cG1hdluQCZERRKFyhMQjwZnDbhSHjoMUjetFGw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-websocket@3.972.16': + resolution: {integrity: sha512-86+S9oCyRVGzoMRpQhxkArp7kD2K75GPmaNevd9B6EyNhWoNvnCZZ3WbgN4j7ZT+jvtvBCGZvI2XHsWZJ+BRIg==} + engines: {node: '>= 14.0.0'} + + '@aws-sdk/nested-clients@3.997.3': + resolution: {integrity: sha512-SivE6GP228IVgfsrr2c/vqTg95X0Qj39Yw4uIrcddpkUzIltNMoNOR62leHOLhODfjv9K8X2mPTwS69A5kT0nQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/region-config-resolver@3.972.13': + resolution: {integrity: sha512-CvJ2ZIjK/jVD/lbOpowBVElJyC1YxLTIJ13yM0AEo0t2v7swOzGjSA6lJGH+DwZXQhcjUjoYwc8bVYCX5MDr1A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.22': + resolution: {integrity: sha512-/rXhMXteD+BqhFd0nYprAgcZ/KtU+963uftPqd3tiFcFfooHZINXUGtOmo2SQjRVauCTNqIEzkwuSETdZFqTTA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1028.0': + resolution: {integrity: sha512-2vDFrEhJDlUHyvDxqDyOk97cejMM8GJDyQbFfOCEWclGwhTjlj1mdyj36xsxh7DYyuquhjqfbvhpl6ZzsVol0w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1036.0': + resolution: {integrity: sha512-aNSJ6jjDYayxN9ZA1JpycVScX93Lx03kKZ1EXt3DGOTahcWVLJj3oLAlop0xKP+vP2Ga2t49p1tEaMkTbCCaZA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.973.8': + resolution: {integrity: sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-arn-parser@3.972.3': + resolution: {integrity: sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-endpoints@3.996.8': + resolution: {integrity: sha512-oOZHcRDihk5iEe5V25NVWg45b3qEA8OpHWVdU/XQh8Zj4heVPAJqWvMphQnU7LkufmUo10EpvFPZuQMiFLJK3g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-format-url@3.972.10': + resolution: {integrity: sha512-DEKiHNJVtNxdyTeQspzY+15Po/kHm6sF0Cs4HV9Q2+lplB63+DrvdeiSoOSdWEWAoO2RcY1veoXVDz2tWxWCgQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.5': + resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-user-agent-browser@3.972.10': + resolution: {integrity: sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g==} + + '@aws-sdk/util-user-agent-node@3.973.21': + resolution: {integrity: sha512-Av4UHTcAWgdvbN0IP9pbtf4Qa1+6LtJqQdZWj5pLn5J67w0pnJJAZZ+7JPPcj2KN3378zD2JDM9DwJKEyvyMTQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + + '@aws-sdk/xml-builder@3.972.19': + resolution: {integrity: sha512-Cw8IOMdBUEIl8ZlhRC3Dc/E64D5B5/8JhV6vhPLiPfJwcRC84S6F8aBOIi/N4vR9ZyA4I5Cc0Ateb/9EHaJXeQ==} + engines: {node: '>=20.0.0'} + + '@aws/bedrock-token-generator@1.1.0': + resolution: {integrity: sha512-i+DkWnfdA4j4sffy9dI4k3OGoOWqN8CTGdtO4IZ3c0kpKYFr6KyqzqLQmoRNrF3ACFcWj6u+J6cbBQ97j9wx5w==} + engines: {node: '>=16.0.0'} + + '@aws/lambda-invoke-store@0.2.4': + resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} + engines: {node: '>=18.0.0'} + + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@borewit/text-codec@0.2.2': + resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==} + + '@buape/carbon@0.15.0': + resolution: {integrity: sha512-3V3XXIqtBzU5vSpCp4avX0RKbYyCIh493XDS/nRJvL7Num/9gB8Ylhd1ywt39gBGaNJScJW1hoWxRyN6Il6thw==} + + '@cacheable/memory@2.0.8': + resolution: {integrity: sha512-FvEb29x5wVwu/Kf93IWwsOOEuhHh6dYCJF3vcKLzXc0KXIW181AOzv6ceT4ZpBHDvAfG60eqb+ekmrnLHIy+jw==} + + '@cacheable/node-cache@1.7.6': + resolution: {integrity: sha512-6Omk2SgNnjtxB5f/E6bTIWIt5xhdpx39fGNRQgU9lojvRxU68v+qY+SXXLsp3ZGukqoPjsK21wZ6XABFr/Ge3A==} + engines: {node: '>=18'} + + '@cacheable/utils@2.4.1': + resolution: {integrity: sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==} + + '@clack/core@1.2.0': + resolution: {integrity: sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg==} + + '@clack/prompts@1.2.0': + resolution: {integrity: sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w==} + + '@cloudflare/workers-types@4.20260405.1': + resolution: {integrity: sha512-PokTmySa+D6MY01R1UfYH48korsN462NK/fl3aw47Hg7XuLuSo/RTpjT0vtWaJhJoFY5tHGOBBIbDcIc8wltLg==} + + '@discordjs/node-pre-gyp@0.4.5': + resolution: {integrity: sha512-YJOVVZ545x24mHzANfYoy0BJX5PDyeZlpiJjDkUBM/V/Ao7TFX9lcUvCN4nr0tbr5ubeaXxtEBILUrHtTphVeQ==} + hasBin: true + + '@discordjs/opus@0.10.0': + resolution: {integrity: sha512-HHEnSNrSPmFEyndRdQBJN2YE6egyXS9JUnJWyP6jficK0Y+qKMEZXyYTgmzpjrxXP1exM/hKaNP7BRBUEWkU5w==} + engines: {node: '>=12.0.0'} + + '@discordjs/voice@0.19.2': + resolution: {integrity: sha512-3yJ255e4ag3wfZu/DSxeOZK1UtnqNxnspmLaQetGT0pDkThNZoHs+Zg6dgZZ19JEVomXygvfHn9lNpICZuYtEA==} + engines: {node: '>=22.12.0'} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@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] + + '@eshaz/web-worker@1.2.2': + resolution: {integrity: sha512-WxXiHFmD9u/owrzempiDlBB1ZYqiLnm9s6aPc8AlFQalq2tKmqdmMr9GXOupDgzXtqnBipj8Un0gkIm7Sjf8mw==} + + '@google/genai@1.50.1': + resolution: {integrity: sha512-YbkX7H9+1Pt8wOt7DDREy8XSoiL6fRDzZQRyaVBarFf8MR3zHGqVdvM4cLbDXqPhxqvegZShgfxb8kw9C7YhAQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.25.2 + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true + + '@grammyjs/runner@2.0.3': + resolution: {integrity: sha512-nckmTs1dPWfVQteK9cxqxzE+0m1VRvluLWB8UgFzsjg62w3qthPJt0TYtJBEdG7OedvfQq4vnFAyE6iaMkR42A==} + engines: {node: '>=12.20.0 || >=14.13.1'} + peerDependencies: + grammy: ^1.13.1 + + '@grammyjs/transformer-throttler@1.2.1': + resolution: {integrity: sha512-CpWB0F3rJdUiKsq7826QhQsxbZi4wqfz1ccKX+fr+AOC+o8K7ZvS+wqX0suSu1QCsyUq2MDpNiKhyL2ZOJUS4w==} + engines: {node: ^12.20.0 || >=14.13.1} + peerDependencies: + grammy: ^1.0.0 + + '@grammyjs/types@3.26.0': + resolution: {integrity: sha512-jlnyfxfev/2o68HlvAGRocAXgdPPX5QabG7jZlbqC2r9DZyWBfzTlg+nu3O3Fy4EhgLWu28hZ/8wr7DsNamP9A==} + + '@hapi/boom@9.1.4': + resolution: {integrity: sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==} + + '@hapi/hoek@9.3.0': + resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} + + '@homebridge/ciao@1.3.6': + resolution: {integrity: sha512-2F9N/15Q/GnoBXimr8PFg7fb1QrAQBvuZpaW2kseWOOy14Lzc3yZB1mT9N1Ju/4hlkboU33uHxtOxZkvkPoE/w==} + hasBin: true + + '@hono/node-server@1.19.13': + resolution: {integrity: sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@hono/node-server@1.19.14': + resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@jimp/core@1.6.1': + resolution: {integrity: sha512-+BoKC5G6hkrSy501zcJ2EpfnllP+avPevcBfRcZe/CW+EwEfY6X1EZ8QWyT7NpDIvEEJb1fdJnMMfUnFkxmw9A==} + engines: {node: '>=18'} + + '@jimp/diff@1.6.1': + resolution: {integrity: sha512-YkKDPdHjLgo1Api3+Bhc0GLAygldlpt97NfOKoNg1U6IUNXA6X2MgosCjPfSBiSvJvrrz1fsIR+/4cfYXBI/HQ==} + engines: {node: '>=18'} + + '@jimp/file-ops@1.6.1': + resolution: {integrity: sha512-T+gX6osHjprbDRad0/B71Evyre7ZdVY1z/gFGEG9Z8KOtZPKboWvPeP2UjbZYWQLy9UKCPQX1FNAnDiOPkJL7w==} + engines: {node: '>=18'} + + '@jimp/js-bmp@1.6.1': + resolution: {integrity: sha512-xzWzNT4/u5zGrTT3Tme9sGU7YzIKxi13+BCQwLqACbt5DXf9SAfdzRkopZQnmDko+6In5nqaT89Gjs43/WdnYQ==} + engines: {node: '>=18'} + + '@jimp/js-gif@1.6.1': + resolution: {integrity: sha512-YjY2W26rQa05XhanYhRZ7dingCiNN+T2Ymb1JiigIbABY0B28wHE3v3Cf1/HZPWGu0hOg36ylaKgV5KxF2M58w==} + engines: {node: '>=18'} + + '@jimp/js-jpeg@1.6.1': + resolution: {integrity: sha512-HT9H3yOmlOFzYmdI15IYdfy6ggQhSRIaHeA+OTJSEORXBqEo97sUZu/DsgHIcX5NJ7TkJBTgZ9BZXsV6UbsyMg==} + engines: {node: '>=18'} + + '@jimp/js-png@1.6.1': + resolution: {integrity: sha512-SZ/KVhI5UjcSzzlXsXdIi/LhJ7UShf2NkMOtVrbZQcGzsqNtynAelrOXeoTxcanfVqmNhAoVHg8yR2cYoqrYjA==} + engines: {node: '>=18'} + + '@jimp/js-tiff@1.6.1': + resolution: {integrity: sha512-jDG/eJquID1M4MBlKMmDRBmz2TpXMv7TUyu2nIRUxhlUc2ogC82T+VQUkca9GJH1BBJ9dx5sSE5dGkWNjIbZxw==} + engines: {node: '>=18'} + + '@jimp/plugin-blit@1.6.1': + resolution: {integrity: sha512-MwnI7C7K81uWddY9FLw1fCOIy6SsPIUftUz36Spt7jisCn8/40DhQMlSxpxTNelnZb/2SnloFimQfRZAmHLOqQ==} + engines: {node: '>=18'} + + '@jimp/plugin-blur@1.6.1': + resolution: {integrity: sha512-lIo7Tzp5jQu30EFFSK/phXANK3citKVEjepDjQ6ljHoIFtuMRrnybnmI2Md24ulvWlDaz+hh3n6qrMb8ydwhZQ==} + engines: {node: '>=18'} + + '@jimp/plugin-circle@1.6.1': + resolution: {integrity: sha512-kK1PavY6cKHNNKce37vdV4Tmpc1/zDKngGoeOV3j+EMatoHFZUinV3s6F9aWryPs3A0xhCLZgdJ6Zeea1d5LCQ==} + engines: {node: '>=18'} + + '@jimp/plugin-color@1.6.1': + resolution: {integrity: sha512-LtUN1vAP+LRlZAtTNVhDRSiXx+26Kbz3zJaG6a5k59gQ95jgT5mknnF8lxkHcqJthM4MEk3/tPxkdJpEybyF/A==} + engines: {node: '>=18'} + + '@jimp/plugin-contain@1.6.1': + resolution: {integrity: sha512-m0qhrfA8jkTqretGv4w+T/ADFR4GwBpE0sCOC2uJ0dzr44/ddOMsIdrpi89kabqYiPYIrxkgdCVCLm3zn1Vkkg==} + engines: {node: '>=18'} + + '@jimp/plugin-cover@1.6.1': + resolution: {integrity: sha512-hZytnsth0zoll6cPf434BrT+p/v569Wr5tyO6Dp0dH1IDPhzhB5F38sZGMLDo7bzQiN9JFVB3fxkcJ/WYCJ3Mg==} + engines: {node: '>=18'} + + '@jimp/plugin-crop@1.6.1': + resolution: {integrity: sha512-EerRSLlclXyKDnYc/H9w/1amZW7b7v3OGi/VlerPd2M/pAu5X8TkyYWtfqYCXnNp1Ixtd8oCo9zGfY9zoXT4rg==} + engines: {node: '>=18'} + + '@jimp/plugin-displace@1.6.1': + resolution: {integrity: sha512-K07QVl7xQwIfD6KfxRV/c3E9e7ZBXxUXdWuvoTWcKHL2qV48MOF5Nqbz/aJW4ThnQARIsxvYlZjPFiqkCjlU+g==} + engines: {node: '>=18'} + + '@jimp/plugin-dither@1.6.1': + resolution: {integrity: sha512-+2V+GCV2WycMoX1/z977TkZ8Zq/4MVSKElHYatgUqtwXMi2fDK2gKYU2g9V39IqFvTJsTIsK0+58VFz/ROBVew==} + engines: {node: '>=18'} + + '@jimp/plugin-fisheye@1.6.1': + resolution: {integrity: sha512-XtS5ZyoZ0vxZxJ6gkqI63SivhtI58vX95foMPM+cyzYkRsJXMOYCr8DScxF5bp4Xr003NjYm/P+7+08tibwzHA==} + engines: {node: '>=18'} + + '@jimp/plugin-flip@1.6.1': + resolution: {integrity: sha512-ws38W/sGj7LobNRayQ83garxiktOyWxM5vO/y4a/2cy9v65SLEUzVkrj+oeAaUSSObdz4HcCEla7XtGlnAGAaA==} + engines: {node: '>=18'} + + '@jimp/plugin-hash@1.6.1': + resolution: {integrity: sha512-sZt6ZcMX6i8vFWb4GYnw0pR/o9++ef0dTVcboTB5B/g7nrxCODIB4wfEkJ/YqZM5wUvol77K1qeS0/rVO6z21A==} + engines: {node: '>=18'} + + '@jimp/plugin-mask@1.6.1': + resolution: {integrity: sha512-SIG0/FcmEj3tkwFxc7fAGLO8o4uNzMpSOdQOhbCgxefQKq5wOVMk9BQx/sdMPBwtMLr9WLq0GzLA/rk6t2v20A==} + engines: {node: '>=18'} + + '@jimp/plugin-print@1.6.1': + resolution: {integrity: sha512-BYVz/X3Xzv8XYilVeDy11NOp0h7BTDjlOtu0BekIFHP1yHVd24AXNzbOy52XlzYZWQ0Dl36HOHEpl/nSNrzc6w==} + engines: {node: '>=18'} + + '@jimp/plugin-quantize@1.6.1': + resolution: {integrity: sha512-J2En9PLURfP+vwYDtuZ9T8yBW6BWYZBScydAjRiPBmJfEhTcNQqiiQODrZf7EqbbX/Sy5H6dAeRiqkgoV9N6Ww==} + engines: {node: '>=18'} + + '@jimp/plugin-resize@1.6.1': + resolution: {integrity: sha512-CLkrtJoIz2HdWnpYiN6p8KYcPc00rCH/SUu6o+lfZL05Q4uhecJlnvXuj9x+U6mDn3ldPmJj6aZqMHuUJzdVqg==} + engines: {node: '>=18'} + + '@jimp/plugin-rotate@1.6.1': + resolution: {integrity: sha512-nOjVjbbj705B02ksysKnh0POAwEBXZtJ9zQ5qC+X7Tavl3JNn+P3BzQovbBxLPSbUSld6XID9z5ijin4PtOAUg==} + engines: {node: '>=18'} + + '@jimp/plugin-threshold@1.6.1': + resolution: {integrity: sha512-JOKv9F8s6tnVLf4sB/2fF0F339EFnHvgEdFYugO6VhowKLsap0pEZmLyE/DlRnYtIj2RddHZVxVMp/eKJ04l2Q==} + engines: {node: '>=18'} + + '@jimp/types@1.6.1': + resolution: {integrity: sha512-leI7YbveTNi565m910XgIOwXyuu074H5qazAD1357HImJSv2hqxnWXpwxQbadGWZ7goZRYBDZy5lpqud0p7q5w==} + engines: {node: '>=18'} + + '@jimp/utils@1.6.1': + resolution: {integrity: sha512-veFPRd93FCnS7AgmCkPgARVGoDRrJ9cm1ujuNyA+UfQ5VKbED2002sm5XfFLFwTsKC8j04heTrwe+tU1dluXOw==} + engines: {node: '>=18'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@keyv/bigmap@1.3.1': + resolution: {integrity: sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==} + engines: {node: '>= 18'} + peerDependencies: + keyv: ^5.6.0 + + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + + '@lancedb/lancedb-darwin-arm64@0.27.2': + resolution: {integrity: sha512-+XM68V/Rou8kKWDnUeKvg9ChKS0zGeQC2sKAop+06Ty4LwIjEGkeYBYrK0vMhZkBN5EFaOjTOp8E8hGQxdFwXA==} + engines: {node: '>= 18'} + cpu: [arm64] + os: [darwin] + + '@lancedb/lancedb-linux-arm64-gnu@0.27.2': + resolution: {integrity: sha512-laiTTDeMUTzm7t+t6ME5nNQMDoERjmkeuWAFWekbXiFdmp62Dqu34Lvf2BvpWnKwxLMZ5JcBJFIw32WS8/8Jnw==} + engines: {node: '>= 18'} + cpu: [arm64] + os: [linux] + + '@lancedb/lancedb-linux-arm64-musl@0.27.2': + resolution: {integrity: sha512-bK5Mc50EvwGZaaiym5CoPu8Y4GNSyEEvTQ0dTC2AUIm83qdQu1rGw6kkYtc/rTH/hbvAvPQot4agHDZfMVxfYw==} + engines: {node: '>= 18'} + cpu: [arm64] + os: [linux] + + '@lancedb/lancedb-linux-x64-gnu@0.27.2': + resolution: {integrity: sha512-qe+ML0YmPru0o84f33RBHqoNk6zsHBjiXTLKsEBDiiFYKks/XMsrkKy9NQYcTxShBrg/nx/MLzCzd7dihqgNYw==} + engines: {node: '>= 18'} + cpu: [x64] + os: [linux] + + '@lancedb/lancedb-linux-x64-musl@0.27.2': + resolution: {integrity: sha512-ZpX6Oxn06qvzAdm+D/gNb3SRp/A9lgRAPvPg6nnMmSQk5XamC/hbGO07uK1wwop7nlqXUH/thk4is2y2ieWdTw==} + engines: {node: '>= 18'} + cpu: [x64] + os: [linux] + + '@lancedb/lancedb-win32-arm64-msvc@0.27.2': + resolution: {integrity: sha512-4ffpFvh49MiUtkdFJOmBytXEbgUPXORphTOuExnJAgT1VAKwQcu4ZzdsgNoK6mumKBaU+pYQU/MedNkgTzx/Lw==} + engines: {node: '>= 18'} + cpu: [arm64] + os: [win32] + + '@lancedb/lancedb-win32-x64-msvc@0.27.2': + resolution: {integrity: sha512-XlwiI6CK2Gkqq+FFVAStHojao/XjIJpDPTm7Tb9SpLL64IlwGw3yaT2hnWKTm90W4KlSrpfSldPly+s+y4U7JQ==} + engines: {node: '>= 18'} + cpu: [x64] + os: [win32] + + '@lancedb/lancedb@0.27.2': + resolution: {integrity: sha512-JQpZHV5KzUzDI3flYCjtZcfHlEbL8lM54E0NT+jrRYe29aKYegfavvPsAsuZp0VdcMwFMZcpMkaBhjQMo/fwvg==} + engines: {node: '>= 18'} + cpu: [x64, arm64] + os: [darwin, linux, win32] + peerDependencies: + apache-arrow: '>=15.0.0 <=18.1.0' + + '@larksuiteoapi/node-sdk@1.61.1': + resolution: {integrity: sha512-BxLBCXk/652I0nWduQbiIrTH2TPe/i4ZD6UhW3VCTVFzrOq5Y9SKvAwanBE6z1ZyEPL6iLnXg/TfGvGSzG6MLw==} + + '@lydell/node-pty-darwin-arm64@1.2.0-beta.12': + resolution: {integrity: sha512-tqaifcY9Cr41SblO1+FLzh8oxxtkNhuW9Dhl22lKme9BreYvKvxEZcdPIXTuqkJc5tagOEC4QHShKmJjLyLXLQ==} + cpu: [arm64] + os: [darwin] + + '@lydell/node-pty-darwin-x64@1.2.0-beta.12': + resolution: {integrity: sha512-4LrS5pCJwqHKDVf1zS2gyNV0m4hKAXch+XZNhbZ6LY8uwVL8BhchzQBO40Os5anuRxRCWzHpw4Sp64Ie8q7E4Q==} + cpu: [x64] + os: [darwin] + + '@lydell/node-pty-linux-arm64@1.2.0-beta.12': + resolution: {integrity: sha512-Sx+A71x5BDGHt9ansfrtGxwq2VFVDWvJUAdlUL0Hv0qeiJUfts+hgopx+CgT4PSwahKjdEgtu0+FAfY9rICKRw==} + cpu: [arm64] + os: [linux] + + '@lydell/node-pty-linux-x64@1.2.0-beta.12': + resolution: {integrity: sha512-bJzs94njofYhGg/UDqW1nj0dtvvu+2OvxMY+RlLS1T17VgcktKoIR6PuenTwE5HJ/D6StCPADmXcT0nNsCKmIQ==} + cpu: [x64] + os: [linux] + + '@lydell/node-pty-win32-arm64@1.2.0-beta.12': + resolution: {integrity: sha512-p7POgjVEiFaBC3/y+AKuV1FzePCsJ6HmZDv2XK+jBZSfwP8+uBAw181ZiKYN1YuRa/XpmBGaWezcI8hZkbW++g==} + cpu: [arm64] + os: [win32] + + '@lydell/node-pty-win32-x64@1.2.0-beta.12': + resolution: {integrity: sha512-IDFa00g7qUDGUYgByrUBJtC+mOjYVt/8KYyWivCg5JjGOHbBUACUQZLl0jTWmnr+tld/UyTpX90a2PY6oTVtRw==} + cpu: [x64] + os: [win32] + + '@lydell/node-pty@1.2.0-beta.12': + resolution: {integrity: sha512-qIK890UwPupoj07osVvgOIa++1mxeHbcGry4PKRHhNVNs81V2SCG34eJr46GybiOmBtc8Sj5PB1/GGM5PL549g==} + + '@mariozechner/clipboard-darwin-arm64@0.3.2': + resolution: {integrity: sha512-uBf6K7Je1ihsgvmWxA8UCGCeI+nbRVRXoarZdLjl6slz94Zs1tNKFZqx7aCI5O1i3e0B6ja82zZ06BWrl0MCVw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@mariozechner/clipboard-darwin-universal@0.3.2': + resolution: {integrity: sha512-mxSheKTW2U9LsBdXy0SdmdCAE5HqNS9QUmpNHLnfJ+SsbFKALjEZc5oRrVMXxGQSirDvYf5bjmRyT0QYYonnlg==} + engines: {node: '>= 10'} + os: [darwin] + + '@mariozechner/clipboard-darwin-x64@0.3.2': + resolution: {integrity: sha512-U1BcVEoidvwIp95+HJswSW+xr28EQiHR7rZjH6pn8Sja5yO4Yoe3yCN0Zm8Lo72BbSOK/fTSq0je7CJpaPCspg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@mariozechner/clipboard-linux-arm64-gnu@0.3.2': + resolution: {integrity: sha512-BsinwG3yWTIjdgNCxsFlip7LkfwPk+ruw/aFCXHUg/fb5XC/Ksp+YMQ7u0LUtiKzIv/7LMXgZInJQH6gxbAaqQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@mariozechner/clipboard-linux-arm64-musl@0.3.2': + resolution: {integrity: sha512-0/Gi5Xq2V6goXBop19ePoHvXsmJD9SzFlO3S+d6+T2b+BlPcpOu3Oa0wTjl+cZrLAAEzA86aPNBI+VVAFDFPKw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@mariozechner/clipboard-linux-riscv64-gnu@0.3.2': + resolution: {integrity: sha512-2AFFiXB24qf0zOZsxI1GJGb9wQGlOJyN6UwoXqmKS3dpQi/l6ix30IzDDA4c4ZcCcx4D+9HLYXhC1w7Sov8pXA==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + + '@mariozechner/clipboard-linux-x64-gnu@0.3.2': + resolution: {integrity: sha512-v6fVnsn7WMGg73Dab8QMwyFce7tzGfgEixKgzLP8f1GJqkJZi5zO4k4FOHzSgUufgLil63gnxvMpjWkgfeQN7A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@mariozechner/clipboard-linux-x64-musl@0.3.2': + resolution: {integrity: sha512-xVUtnoMQ8v2JVyfJLKKXACA6avdnchdbBkTsZs8BgJQo29qwCp5NIHAUO8gbJ40iaEGToW5RlmVk2M9V0HsHEw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@mariozechner/clipboard-win32-arm64-msvc@0.3.2': + resolution: {integrity: sha512-AEgg95TNi8TGgak2wSXZkXKCvAUTjWoU1Pqb0ON7JHrX78p616XUFNTJohtIon3e0w6k0pYPZeCuqRCza/Tqeg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@mariozechner/clipboard-win32-x64-msvc@0.3.2': + resolution: {integrity: sha512-tGRuYpZwDOD7HBrCpyRuhGnHHSCknELvqwKKUG4JSfSB7JIU7LKRh6zx6fMUOQd8uISK35TjFg5UcNih+vJhFA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@mariozechner/clipboard@0.3.2': + resolution: {integrity: sha512-IHQpksNjo7EAtGuHFU+tbWDp5LarH3HU/8WiB9O70ZEoBPHOg0/6afwSLK0QyNMMmx4Bpi/zl6+DcBXe95nWYA==} + engines: {node: '>= 10'} + + '@mariozechner/jiti@2.6.5': + resolution: {integrity: sha512-faGUlTcXka5l7rv0lP3K3vGW/ejRuOS24RR2aSFWREUQqzjgdsuWNo/IiPqL3kWRGt6Ahl2+qcDAwtdeWeuGUw==} + hasBin: true + + '@mariozechner/pi-agent-core@0.66.1': + resolution: {integrity: sha512-Nj54A7SuB/EQi8r3Gs+glFOr9wz/a9uxYFf0pCLf2DE7VmzA9O7WSejrvArna17K6auftLSdNyRRe2bIO0qezg==} + engines: {node: '>=20.0.0'} + + '@mariozechner/pi-ai@0.66.1': + resolution: {integrity: sha512-7IZHvpsFdKEBkTmjNrdVL7JLUJVIpha6bwTr12cZ5XyDrxij06wP6Ncpnf4HT5BXAzD5w2JnoqTOSbMEIZj3dg==} + engines: {node: '>=20.0.0'} + hasBin: true + + '@mariozechner/pi-coding-agent@0.66.1': + resolution: {integrity: sha512-cNmatT+5HvYzQ78cRhRih00wCeUTH/fFx9ecJh5AbN7axgWU+bwiZYy0cjrTsGVgMGF4xMYlPRn/Nze9JEB+/w==} + engines: {node: '>=20.6.0'} + hasBin: true + + '@mariozechner/pi-tui@0.66.1': + resolution: {integrity: sha512-hNFN42ebjwtfGooqoUwM+QaPR1XCyqPuueuP3aLOWS1bZ2nZP/jq8MBuGNrmMw1cgiDcotvOlSNj3BatzEOGsw==} + engines: {node: '>=20.0.0'} + + '@matrix-org/matrix-sdk-crypto-nodejs@0.4.0': + resolution: {integrity: sha512-+qqgpn39XFSbsD0dFjssGO9vHEP7sTyfs8yTpt8vuqWpUpF20QMwpCZi0jpYw7GxjErNTsMshopuo8677DfGEA==} + engines: {node: '>= 22'} + + '@matrix-org/matrix-sdk-crypto-wasm@18.0.0': + resolution: {integrity: sha512-88+n+dvxLI1cjS10UIlKXVYK7TGWbpAnnaDC9fow7ch/hCvdu3dFhJ3tS3/13N9s9+1QFXB4FFuommj+tHJPhQ==} + engines: {node: '>= 18'} + + '@mistralai/mistralai@1.14.1': + resolution: {integrity: sha512-IiLmmZFCCTReQgPAT33r7KQ1nYo5JPdvGkrkZqA8qQ2qB1GHgs5LoP5K2ICyrjnpw2n8oSxMM/VP+liiKcGNlQ==} + + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + + '@mozilla/readability@0.6.0': + resolution: {integrity: sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==} + engines: {node: '>=14.0.0'} + + '@napi-rs/canvas-android-arm64@0.1.99': + resolution: {integrity: sha512-9OCRt8VVxA17m32NWZKyNC2qamdaS/SC5CEOIQwFngRq0DIeVm4PDal+6Ljnhqm2whZiC63DNuKZ4xSp2nbj9w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@napi-rs/canvas-darwin-arm64@0.1.99': + resolution: {integrity: sha512-lupMDMy1+H38dhyCcLirOKKVUyzzlxi7j7rGPLI3vViMHOoPjcXO1b10ivy+ad+q6MiwHfoLjKTCoLke5ySOBg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@napi-rs/canvas-darwin-x64@0.1.99': + resolution: {integrity: sha512-fdz02t4w8n6Ii/rYhWig6STb/zcTmCC/6YZTGmjoDeidDwn9Wf0ukQVynhCPEs29vqUc66wHZKsuIgMs9tycCg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.99': + resolution: {integrity: sha512-w4FwVwlNo00ezeRhfY62IVIyt6G3u8wodkPtiqWc52BUHx+VDBUM2vkS3ogfANaLI7hnf3s6WK4LyZVUjBg1lA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@napi-rs/canvas-linux-arm64-gnu@0.1.99': + resolution: {integrity: sha512-8JvHeexKQ8c7g0q7YJ29NVQwnf1ePghP9ys9ZN0R0qzyqJQ9Uw6N9qnDINArlm3IYHexB7LjzArIfhQiqSDGvQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@napi-rs/canvas-linux-arm64-musl@0.1.99': + resolution: {integrity: sha512-Z+6nyLdJXWzLPVxi4H6g9TJop4DwN3KSgHWto5JCbZV5/uKoVqcSynPs0tGlUHOoWI8S8tEvJspz51GQkvr07w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@napi-rs/canvas-linux-riscv64-gnu@0.1.99': + resolution: {integrity: sha512-jAnfOUv4IO1l8Levk5t85oVtEBOXLa07KnIUgWo1CDlPxiqpxS3uBfiE38Lvj/CQgHaNF6Nxk/SaemwLgsVJgw==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + + '@napi-rs/canvas-linux-x64-gnu@0.1.99': + resolution: {integrity: sha512-mIkXw3fGmbYyFjSmfWEvty4jN+rwEOmv0+Dy9bRvvTzLYWCgm3RMgUEQVfAKFw96nIRFnyNZiK83KNQaVVFjng==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@napi-rs/canvas-linux-x64-musl@0.1.99': + resolution: {integrity: sha512-f3Uz2P0RgrtBHISxZqr6yiYXJlTDyCVBumDacxo+4AmSg7z0HiqYZKGWC/gszq3fbPhyQUya1W2AEteKxT9Y6A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@napi-rs/canvas-win32-arm64-msvc@0.1.99': + resolution: {integrity: sha512-XE6KUkfqRsCNejcoRMiMr3RaUeObxNf6y7dut3hrq2rn7PzfRTZgrjF1F/B2C7FcdgqY/vSHWpQeMuNz1vTNHg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@napi-rs/canvas-win32-x64-msvc@0.1.99': + resolution: {integrity: sha512-plMYGVbc/vmmPF9MtmHbwNk1rL1Aj53vQZt+Gnv1oZn6gmd9jEHHJ0n9Nd2nxa5sKH7TS5IjkCDM6289O0d6PQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@napi-rs/canvas@0.1.99': + resolution: {integrity: sha512-zN4eQlK3eBf7aJBcTHZilpBH3tDekBzPMIWC8r0s94Ecl73XfOyFi4w7yKFMRVUT0lvNQjtOL8YSrwqQj6mZFg==} + engines: {node: '>= 10'} + + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@noble/ciphers@2.1.1': + resolution: {integrity: sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==} + engines: {node: '>= 20.19.0'} + + '@noble/curves@2.0.1': + resolution: {integrity: sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==} + engines: {node: '>= 20.19.0'} + + '@noble/hashes@2.0.1': + resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} + engines: {node: '>= 20.19.0'} + + '@nodable/entities@2.1.0': + resolution: {integrity: sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==} + + '@pierre/diffs@1.1.13': + resolution: {integrity: sha512-lnX9Fy5eC+07b8g+D8krC3txOY6LRN5VNR1qr9bph9XEyLxbwwfGN7SFRu4HGozpkDdA76JARgxgWHN+uAihmg==} + peerDependencies: + react: ^18.3.1 || ^19.0.0 + react-dom: ^18.3.1 || ^19.0.0 + + '@pierre/theme@0.0.28': + resolution: {integrity: sha512-1j/H/fECBuc9dEvntdWI+l435HZapw+RCJTlqCA6BboQ5TjlnE005j/ROWutXIs8aq5OAc82JI2Kwk4A1WWBgw==} + engines: {vscode: ^1.0.0} + + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + + '@rollup/rollup-android-arm-eabi@4.60.2': + resolution: {integrity: sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.2': + resolution: {integrity: sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.2': + resolution: {integrity: sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.2': + resolution: {integrity: sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.2': + resolution: {integrity: sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.2': + resolution: {integrity: sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.2': + resolution: {integrity: sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.60.2': + resolution: {integrity: sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.60.2': + resolution: {integrity: sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.60.2': + resolution: {integrity: sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.60.2': + resolution: {integrity: sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.60.2': + resolution: {integrity: sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.60.2': + resolution: {integrity: sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.60.2': + resolution: {integrity: sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.60.2': + resolution: {integrity: sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.60.2': + resolution: {integrity: sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.60.2': + resolution: {integrity: sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.60.2': + resolution: {integrity: sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.60.2': + resolution: {integrity: sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.60.2': + resolution: {integrity: sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.2': + resolution: {integrity: sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.2': + resolution: {integrity: sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.2': + resolution: {integrity: sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.2': + resolution: {integrity: sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.2': + resolution: {integrity: sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==} + cpu: [x64] + os: [win32] + + '@scure/base@2.0.0': + resolution: {integrity: sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==} + + '@scure/bip32@2.0.1': + resolution: {integrity: sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA==} + + '@scure/bip39@2.0.1': + resolution: {integrity: sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg==} + + '@shikijs/core@3.23.0': + resolution: {integrity: sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==} + + '@shikijs/engine-javascript@3.23.0': + resolution: {integrity: sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==} + + '@shikijs/engine-oniguruma@3.23.0': + resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==} + + '@shikijs/langs@3.23.0': + resolution: {integrity: sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==} + + '@shikijs/themes@3.23.0': + resolution: {integrity: sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==} + + '@shikijs/transformers@3.23.0': + resolution: {integrity: sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ==} + + '@shikijs/types@3.23.0': + resolution: {integrity: sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + + '@silvia-odwyer/photon-node@0.3.4': + resolution: {integrity: sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==} + + '@sinclair/typebox@0.33.22': + resolution: {integrity: sha512-auUj4k+f4pyrIVf4GW5UKquSZFHJWri06QgARy9C0t9ZTjJLIuNIrr1yl9bWcJWJ1Gz1vOvYN1D+QPaIlNMVkQ==} + + '@sinclair/typebox@0.34.49': + resolution: {integrity: sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==} + + '@slack/bolt@4.7.1': + resolution: {integrity: sha512-CIyVjvHm/gY/e6n/xsJibcQFh2+S0WrlaV4LzpwXDlsmWuDrhLzAqBcOP/i9vgyFklO+DXD9Pzbz2uSPCctnZQ==} + engines: {node: '>=18', npm: '>=8.6.0'} + peerDependencies: + '@types/express': ^5.0.0 + + '@slack/logger@4.0.1': + resolution: {integrity: sha512-6cmdPrV/RYfd2U0mDGiMK8S7OJqpCTm7enMLRR3edccsPX8j7zXTLnaEF4fhxxJJTAIOil6+qZrnUPTuaLvwrQ==} + engines: {node: '>= 18', npm: '>= 8.6.0'} + + '@slack/oauth@3.0.5': + resolution: {integrity: sha512-exqFQySKhNDptWYSWhvRUJ4/+ndu2gayIy7vg/JfmJq3wGtGdHk531P96fAZyBm5c1Le3yaPYqv92rL4COlU3A==} + engines: {node: '>=18', npm: '>=8.6.0'} + + '@slack/socket-mode@2.0.6': + resolution: {integrity: sha512-Aj5RO3MoYVJ+b2tUjHUXuA3tiIaCUMOf1Ss5tPiz29XYVUi6qNac2A8ulcU1pUPERpXVHTmT1XW6HzQIO74daQ==} + engines: {node: '>= 18', npm: '>= 8.6.0'} + + '@slack/types@2.20.1': + resolution: {integrity: sha512-eWX2mdt1ktpn8+40iiMc404uGrih+2fxiky3zBcPjtXKj6HLRdYlmhrPkJi7JTJm8dpXR6BWVWEDBXtaWMKD6A==} + engines: {node: '>= 12.13.0', npm: '>= 6.12.0'} + + '@slack/web-api@7.15.1': + resolution: {integrity: sha512-y+TAF7TszcmFzbVtBkFqAdBwKSoD+8shkNxhp4WIfFwXmCKdFje9WD6evROApPa2FTy1v1uc9yBaJs3609PPgg==} + engines: {node: '>= 18', npm: '>= 8.6.0'} + + '@smithy/config-resolver@4.4.17': + resolution: {integrity: sha512-TzDZcAnhTyAHbXVxWZo7/tEcrIeFq20IBk8So3OLOetWpR8EwY/yEqBMBFaJMeyEiREDq4NfEl+qO3OAUD+vbQ==} + engines: {node: '>=18.0.0'} + + '@smithy/core@3.23.17': + resolution: {integrity: sha512-x7BlLbUFL8NWCGjMF9C+1N5cVCxcPa7g6Tv9B4A2luWx3be3oU8hQ96wIwxe/s7OhIzvoJH73HAUSg5JXVlEtQ==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.2.14': + resolution: {integrity: sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-codec@4.2.14': + resolution: {integrity: sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-browser@4.2.14': + resolution: {integrity: sha512-8IelTCtTctWRbb+0Dcy+C0aICh1qa0qWXqgjcXDmMuCvPJRnv26hiDZoAau2ILOniki65mCPKqOQs/BaWvO4CQ==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-config-resolver@4.3.14': + resolution: {integrity: sha512-sqHiHpYRYo3FJlaIxD1J8PhbcmJAm7IuM16mVnwSkCToD7g00IBZzKuiLNMGmftULmEUX6/UAz8/NN5uMP8bVA==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-node@4.2.14': + resolution: {integrity: sha512-Ht/8BuGlKfFTy0H3+8eEu0vdpwGztCnaLLXtpXNdQqiR7Hj4vFScU3T436vRAjATglOIPjJXronY+1WxxNLSiw==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-universal@4.2.14': + resolution: {integrity: sha512-lWyt4T2XQZUZgK3tQ3Wn0w3XBvZsK/vjTuJl6bXbnGZBHH0ZUSONTYiK9TgjTTzU54xQr3DRFwpjmhp0oLm3gg==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.3.17': + resolution: {integrity: sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-node@4.2.14': + resolution: {integrity: sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g==} + engines: {node: '>=18.0.0'} + + '@smithy/invalid-dependency@4.2.14': + resolution: {integrity: sha512-c21qJiTSb25xvvOp+H2TNZzPCngrvl5vIPqPB8zQ/DmJF4QWXO19x1dWfMJZ6wZuuWUPPm0gV8C0cU3+ifcWuw==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/is-array-buffer@4.2.2': + resolution: {integrity: sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-content-length@4.2.14': + resolution: {integrity: sha512-xhHq7fX4/3lv5NHxLUk3OeEvl0xZ+Ek3qIbWaCL4f9JwgDZEclPBElljaZCAItdGPQl/kSM4LPMOpy1MYgprpw==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-endpoint@4.4.32': + resolution: {integrity: sha512-ZZkgyjnJppiZbIm6Qbx92pbXYi1uzenIvGhBSCDlc7NwuAkiqSgS75j1czAD25ZLs2FjMjYy1q7gyRVWG6JA0Q==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-retry@4.5.5': + resolution: {integrity: sha512-wnYOpB5vATFKWrY2Z9Alb0KhjZI6AbzU6Fbz3Hq2GnURdRYWB4q+qWivQtSTwXcmWUA3MZ6krfwL6Cq5MAbxsA==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-serde@4.2.20': + resolution: {integrity: sha512-Lx9JMO9vArPtiChE3wbEZ5akMIDQpWQtlu90lhACQmNOXcGXRbaDywMHDzuDZ2OkZzP+9wQfZi3YJT9F67zTQQ==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-stack@4.2.14': + resolution: {integrity: sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA==} + engines: {node: '>=18.0.0'} + + '@smithy/node-config-provider@4.3.14': + resolution: {integrity: sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.6.1': + resolution: {integrity: sha512-iB+orM4x3xrr57X3YaXazfKnntl0LHlZB1kcXSGzMV1Tt0+YwEjGlbjk/44qEGtBzXAz6yFDzkYTKSV6Pj2HUg==} + engines: {node: '>=18.0.0'} + + '@smithy/property-provider@4.2.14': + resolution: {integrity: sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ==} + engines: {node: '>=18.0.0'} + + '@smithy/protocol-http@5.3.14': + resolution: {integrity: sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-builder@4.2.14': + resolution: {integrity: sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-parser@4.2.14': + resolution: {integrity: sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw==} + engines: {node: '>=18.0.0'} + + '@smithy/service-error-classification@4.3.0': + resolution: {integrity: sha512-9jKsBYQRPR0xBLgc2415RsA5PIcP2sis4oBdN9s0D13cg1B1284mNTjx9Yc+BEERXzuPm5ObktI96OxsKh8E9A==} + engines: {node: '>=18.0.0'} + + '@smithy/shared-ini-file-loader@4.4.9': + resolution: {integrity: sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.3.14': + resolution: {integrity: sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA==} + engines: {node: '>=18.0.0'} + + '@smithy/smithy-client@4.12.13': + resolution: {integrity: sha512-y/Pcj1V9+qG98gyu1gvftHB7rDpdh+7kIBIggs55yGm3JdtBV8GT8IFF3a1qxZ79QnaJHX9GXzvBG6tAd+czJA==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.14.1': + resolution: {integrity: sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==} + engines: {node: '>=18.0.0'} + + '@smithy/url-parser@4.2.14': + resolution: {integrity: sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-base64@4.3.2': + resolution: {integrity: sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-browser@4.2.2': + resolution: {integrity: sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-node@4.2.3': + resolution: {integrity: sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-buffer-from@4.2.2': + resolution: {integrity: sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==} + engines: {node: '>=18.0.0'} + + '@smithy/util-config-provider@4.2.2': + resolution: {integrity: sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-browser@4.3.49': + resolution: {integrity: sha512-a5bNrdiONYB/qE2BuKegvUMd/+ZDwdg4vsNuuSzYE8qs2EYAdK9CynL+Rzn29PbPiUqoz/cbpRbcLzD5lEevHw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-node@4.2.54': + resolution: {integrity: sha512-g1cvrJvOnzeJgEdf7AE4luI7gp6L8weE0y9a9wQUSGtjb8QRHDbCJYuE4Sy0SD9N8RrnNPFsPltAz/OSoBR9Zw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-endpoints@3.4.2': + resolution: {integrity: sha512-a55Tr+3OKld4TTtnT+RhKOQHyPxm3j/xL4OR83WBUhLJaKDS9dnJ7arRMOp3t31dcLhApwG9bgvrRXBHlLdIkg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-hex-encoding@4.2.2': + resolution: {integrity: sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-middleware@4.2.14': + resolution: {integrity: sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-retry@4.3.4': + resolution: {integrity: sha512-FY1UQQ1VFmMwiYp1GVS4MeaGD5O0blLNYK0xCRHU+mJgeoH/hSY8Ld8sJWKQ6uznkh14HveRGQJncgPyNl9J+A==} + engines: {node: '>=18.0.0'} + + '@smithy/util-stream@4.5.25': + resolution: {integrity: sha512-/PFpG4k8Ze8Ei+mMKj3oiPICYekthuzePZMgZbCqMiXIHHf4n2aZ4Ps0aSRShycFTGuj/J6XldmC0x0DwednIA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-uri-escape@4.2.2': + resolution: {integrity: sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@4.2.2': + resolution: {integrity: sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==} + engines: {node: '>=18.0.0'} + + '@smithy/uuid@1.1.2': + resolution: {integrity: sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==} + engines: {node: '>=18.0.0'} + + '@snazzah/davey-android-arm-eabi@0.1.11': + resolution: {integrity: sha512-T1RYbNYKN6tLOcGIDKJd8OI6FBSEemwL7DOYdTMmhqfhhMr3YVN8WOhfoxGg63OcnpTN2e2c5tdY2bAx25RmQQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + + '@snazzah/davey-android-arm64@0.1.11': + resolution: {integrity: sha512-ksJn/x2VU8h6w9eku1HT96ugSRZ7lKVkKNKbFleaFN+U99DJaPM+gMu2YvnFU4V54HR06ZBnRihnVG6VLXQpDw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@snazzah/davey-darwin-arm64@0.1.11': + resolution: {integrity: sha512-E1d7PbaaVMO3Lj9EiAPqOVbuV0xg5+PsHzHH097DDXiD1+zUDXvJaTnUWsnm5z50pJniHpi4GtaYmk+ieB/guA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@snazzah/davey-darwin-x64@0.1.11': + resolution: {integrity: sha512-Tl4TI/LTmgJZepgbgVMYDi8RqlAkPtPg1OEBPl7a9Tn3AwR36Vs6lyIT1cs/lGy/ds/+B+mKI4rPObN1cyILTw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@snazzah/davey-freebsd-x64@0.1.11': + resolution: {integrity: sha512-T8Iw9FXkuI1T+YBAFzh9v/TXf9IOTOSqnd/BFpTRTrlW72PR2lhIidzSmg027VxO7r5pX47iFwiOkb9I/NU/EA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@snazzah/davey-linux-arm-gnueabihf@0.1.11': + resolution: {integrity: sha512-1Txj+8pqA8uq/OGtaUaBFWAPnNMQzFgIywj0iA7EI4xZl+mab48/pv+YZ1pNb/suC6ynsW44oB9efiXSdcUAgA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@snazzah/davey-linux-arm64-gnu@0.1.11': + resolution: {integrity: sha512-ERzF5nM/IYW1BcN3wLXpEwBCGLFf0kGJUVhaV6yfiInz0tkU8UmvrrgpaMaACfMjIhfWdq5CcX+aTkXo/saNcg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@snazzah/davey-linux-arm64-musl@0.1.11': + resolution: {integrity: sha512-e6pX6Hiabtz99q+H/YHNkm9JVlpqN8HGh0qPib8G2+UY4/SSH8WvqWipk3v581dMy2oyCHt7MOoY1aU1P1N/xA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@snazzah/davey-linux-x64-gnu@0.1.11': + resolution: {integrity: sha512-TW5bSoqChOJMbvsDb4wAATYrxmAXuNnse7wFNVSAJUaZKSeRfZbu3UAiPWSNn7GwLwSfU6hg322KZUn8IWCuvg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@snazzah/davey-linux-x64-musl@0.1.11': + resolution: {integrity: sha512-5j6Pmc+Wzv5lSxVP6quA7teYRJXibkZqQyYGfTDnTsUOO5dPpcojpqlXlkhyvsA1OAQTj4uxbOCciN3cVWwzug==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@snazzah/davey-wasm32-wasi@0.1.11': + resolution: {integrity: sha512-rKOwZ/0J8lp+4VEyOdMDBRP9KR+PksZpa9V1Qn0veMzy4FqTVKthkxwGqewheFe0SFg9fdvt798l/PBFrfDeZw==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@snazzah/davey-win32-arm64-msvc@0.1.11': + resolution: {integrity: sha512-5fptJU4tX901m3mj0SHiBljMrPT4ZEsynbBhR7bK1yn9TY1jjyhN8EFi7QF5IWtUEni+0mia2BCMHZ5ZkmFZqQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@snazzah/davey-win32-ia32-msvc@0.1.11': + resolution: {integrity: sha512-ualexn8SeLsiMHhWfzVrzRcjHgcBapg++FPaVgJJxoh2S/jCRiklXOu3luqIZdJdNKvhe2V9SwO/cImPeIIBKw==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@snazzah/davey-win32-x64-msvc@0.1.11': + resolution: {integrity: sha512-muNhc8UKXtknzsH/w4AIkbPR2I8BuvApn0pDXar0IEvY8PCjqU/M8MPbOOEYwQVvQRMwVTgExtxzrkBPSXB4nA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@snazzah/davey@0.1.11': + resolution: {integrity: sha512-oBN+msHzPnm1M5DDx3wVD7iBwpNXFUtkh2MrAbUJu0OhKjliLChi28hq++mu1+qdMpAVQO5JKAvQQxYVbyneiw==} + engines: {node: '>= 10'} + + '@swc/helpers@0.5.21': + resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==} + + '@tokenizer/inflate@0.4.1': + resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} + engines: {node: '>=18'} + + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + + '@tootallnate/quickjs-emscripten@0.23.0': + resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/bun@1.3.11': + resolution: {integrity: sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg==} + + '@types/command-line-args@5.2.3': + resolution: {integrity: sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==} + + '@types/command-line-usage@5.0.4': + resolution: {integrity: sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/events@3.0.3': + resolution: {integrity: sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==} + + '@types/express-serve-static-core@5.1.1': + resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} + + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + + '@types/long@4.0.2': + resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mime-types@2.1.4': + resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@10.17.60': + resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==} + + '@types/node@16.9.1': + resolution: {integrity: sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==} + + '@types/node@20.19.39': + resolution: {integrity: sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==} + + '@types/node@25.6.0': + resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} + + '@types/qs@6.15.0': + resolution: {integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/retry@0.12.0': + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + + '@wasm-audio-decoders/common@9.0.7': + resolution: {integrity: sha512-WRaUuWSKV7pkttBygml/a6dIEpatq2nnZGFIoPTc5yPLkxL6Wk4YaslPM98OPQvWacvNZ+Py9xROGDtrFBDzag==} + + '@whiskeysockets/baileys@7.0.0-rc.9': + resolution: {integrity: sha512-YFm5gKXfDP9byCXCW3OPHKXLzrAKzolzgVUlRosHHgwbnf2YOO3XknkMm6J7+F0ns8OA0uuSBhgkRHTDtqkacw==} + engines: {node: '>=20.0.0'} + peerDependencies: + audio-decode: ^2.1.3 + jimp: ^1.6.0 + link-preview-js: ^3.0.0 + sharp: '*' + peerDependenciesMeta: + audio-decode: + optional: true + jimp: + optional: true + link-preview-js: + optional: true + + '@whiskeysockets/libsignal-node@https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67': + resolution: {tarball: https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67} + version: 2.0.1 + + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + agent-base@9.0.0: + resolution: {integrity: sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==} + engines: {node: '>= 20'} + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + another-json@0.2.0: + resolution: {integrity: sha512-/Ndrl68UQLhnCdsAzEXLMFuOR546o2qbYRqCglaNHbjXrwG1ayTcdwr3zkSGOGtGXDyR5X9nCFfnyG2AFJIsqg==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + any-base@1.1.0: + resolution: {integrity: sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + apache-arrow@18.1.0: + resolution: {integrity: sha512-v/ShMp57iBnBp4lDgV8Jx3d3Q5/Hac25FWmQ98eMahUiHPXcvwIMKJD0hBIgclm/FCG+LwPkAKtkRO1O/W0YGg==} + hasBin: true + + aproba@2.1.0: + resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==} + + are-we-there-yet@2.0.0: + resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-back@3.1.0: + resolution: {integrity: sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==} + engines: {node: '>=6'} + + array-back@6.2.3: + resolution: {integrity: sha512-SGDvmg6QTYiTxCBkYVmThcoa67uLl35pyzRHdpCGBOcqFy6BtwnphoFPk7LhJshD+Yk1Kt35WGWeZPTgwR4Fhw==} + engines: {node: '>=12.17'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-types@0.13.4: + resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} + engines: {node: '>=4'} + + async-mutex@0.5.0: + resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + await-to-js@3.0.0: + resolution: {integrity: sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==} + engines: {node: '>=6.0.0'} + + axios@1.13.6: + resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==} + + axios@1.15.2: + resolution: {integrity: sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base-x@5.0.1: + resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + basic-ftp@5.3.0: + resolution: {integrity: sha512-5K9eNNn7ywHPsYnFwjKgYH8Hf8B5emh7JKcPaVjjrMJFQQwGpwowEnZNEtHs7DfR7hCZsmaK3VA4HUK0YarT+w==} + engines: {node: '>=10.0.0'} + + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + + bmp-ts@1.0.9: + resolution: {integrity: sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw==} + + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + bottleneck@2.19.5: + resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} + + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + + brace-expansion@1.1.14: + resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + + bs58@6.0.0: + resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + bun-types@1.3.11: + resolution: {integrity: sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg==} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + cacheable@2.3.4: + resolution: {integrity: sha512-djgxybDbw9fL/ZWMI3+CE8ZilNxcwFkVtDc1gJ+IlOSSWkSMPQabhV/XCHTQ6pwwN6aivXPZ43omTooZiX06Ew==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chalk-template@0.4.0: + resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==} + engines: {node: '>=12'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + cli-highlight@2.1.11: + resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==} + engines: {node: '>=8.0.0', npm: '>=5.0.0'} + hasBin: true + + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + command-line-args@5.2.1: + resolution: {integrity: sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==} + engines: {node: '>=4.0.0'} + + command-line-usage@7.0.4: + resolution: {integrity: sha512-85UdvzTNx/+s5CkSgBm/0hzP80RFHAa7PsfeADE5ezZF3uHz3/Tqj9gIKGT9PTtpycc3Ua64T0oVulGfKxzfqg==} + engines: {node: '>=12.20.0'} + + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + croner@10.0.1: + resolution: {integrity: sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g==} + engines: {node: '>=18.0'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + + cssom@0.5.0: + resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} + + curve25519-js@0.0.4: + resolution: {integrity: sha512-axn2UMEnkhyDUPWOwVKBMVIzSQy2ejH2xRGy1wq81dqRwApXfIzfbE3hIX0ZRFBIihf/KDqK158DLwESu4AK1w==} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + data-uri-to-buffer@6.0.2: + resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} + engines: {node: '>= 14'} + + data-uri-to-buffer@8.0.0: + resolution: {integrity: sha512-6UHfyCux51b8PTGDgveqtz1tvphBku5DrMKKJbFAZAJOI2zsjDpDoYE1+QGj7FOMS4BdTFNJsJiR3zEB0xH0yQ==} + engines: {node: '>= 20'} + + 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'} + + degenerator@5.0.1: + resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} + engines: {node: '>= 14'} + + degenerator@7.0.1: + resolution: {integrity: sha512-ABErK0IefDSyHjlPH7WUEenIAX2rPPnrDcDM+TS3z3+zu9TfyKKi07BQM+8rmxpdE2y1v5fjjdoAS/x4D2U60w==} + engines: {node: '>= 20'} + peerDependencies: + quickjs-wasi: ^2.2.0 + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + diff@8.0.3: + resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} + engines: {node: '>=0.3.1'} + + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + + discord-api-types@0.38.45: + resolution: {integrity: sha512-DiI01i00FPv6n+hXcFkFxK8Y/rFRpKs6U6aP32N4T73nTbj37Eua3H/95TBpLktLWB6xnLXhYDGvyLq6zzYY2w==} + + discord-api-types@0.38.47: + resolution: {integrity: sha512-XgXQodHQBAE6kfD7kMvVo30863iHX1LHSqNq6MGUTDwIFCCvHva13+rwxyxVXDqudyApMNAd32PGjgVETi5rjA==} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + 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'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + eventsource-parser@3.0.8: + resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + exif-parser@0.1.12: + resolution: {integrity: sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + express-rate-limit@8.4.0: + resolution: {integrity: sha512-gDK8yiqKxrGta+3WtON59arrrw6GLmadA1qoFgYXzdcch8fmKDID2XqO8itsi3f1wufXYPT51387dN6cvVBS3Q==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + fake-indexeddb@6.2.5: + resolution: {integrity: sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w==} + engines: {node: '>=18'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-string-truncated-width@1.2.1: + resolution: {integrity: sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow==} + + fast-string-width@1.1.0: + resolution: {integrity: sha512-O3fwIVIH5gKB38QNbdg+3760ZmGz0SZMgvwJbA1b2TGXceKE6A2cOlfogh1iw8lr049zPyd7YADHy+B7U4W9bQ==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fast-wrap-ansi@0.1.6: + resolution: {integrity: sha512-HlUwET7a5gqjURj70D5jl7aC3Zmy4weA1SHUfM0JFI0Ptq987NH2TwbBFLoERhfwk+E+eaq4EK3jXoT+R3yp3w==} + + fast-xml-builder@1.1.5: + resolution: {integrity: sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==} + + fast-xml-parser@5.7.1: + resolution: {integrity: sha512-8Cc3f8GUGUULg34pBch/KGyPLglS+OFs05deyOlY7fL2MTagYPKrVQNmR1fLF/yJ9PH5ZSTd3YDF6pnmeZU+zA==} + hasBin: true + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + file-type@21.3.4: + resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==} + engines: {node: '>=20'} + + file-type@22.0.1: + resolution: {integrity: sha512-ww5Mhre0EE+jmBvOXTmXAbEMuZE7uX4a3+oRCQFNj8w++g3ev913N6tXQz0XTXbueQ5TWQfm6BdaViEHHn8bhA==} + engines: {node: '>=22'} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + find-replace@3.0.0: + resolution: {integrity: sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==} + engines: {node: '>=4.0.0'} + + flatbuffers@24.12.23: + resolution: {integrity: sha512-dLVCAISd5mhls514keQzmEG6QHmUUsNuWsb4tFafIUwvvgDjXhtfAYSKOzt5SWOy+qByV5pbsDZ+Vb7HUOBEdA==} + + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gauge@3.0.2: + resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + + gaxios@6.7.1: + resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} + engines: {node: '>=14'} + + gaxios@7.1.4: + resolution: {integrity: sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==} + engines: {node: '>=18'} + + gcp-metadata@6.1.1: + resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} + engines: {node: '>=14'} + + gcp-metadata@8.1.2: + resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} + engines: {node: '>=18'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.5.0: + resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + get-uri@6.0.5: + resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} + engines: {node: '>= 14'} + + get-uri@8.0.0: + resolution: {integrity: sha512-CqtZlMKvfJeY0Zxv8wazDwXmSKmnMnsmNy8j8+wudi8EyG/pMUB1NqHc+Tv1QaNtpYsK9nOYjb7r7Ufu32RPSw==} + engines: {node: '>= 20'} + + gifwrap@0.10.1: + resolution: {integrity: sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw==} + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + google-auth-library@10.6.2: + resolution: {integrity: sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==} + engines: {node: '>=18'} + + google-auth-library@9.15.1: + resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} + engines: {node: '>=14'} + + google-logging-utils@0.0.2: + resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} + engines: {node: '>=14'} + + google-logging-utils@1.1.3: + resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} + engines: {node: '>=14'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + grammy@1.42.0: + resolution: {integrity: sha512-1AdCge+AkjSdp2FwfICSFnVbl8Mq3KVHJDy+DgTI9+D6keJ0zWALPRKas5jv/8psiCzL4N2cEOcGW7O45Kn39g==} + engines: {node: ^12.20.0 || >=14.13.1} + + gtoken@7.1.0: + resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} + engines: {node: '>=14.0.0'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + + hashery@1.5.1: + resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} + engines: {node: '>=20'} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + + hono@4.12.14: + resolution: {integrity: sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==} + engines: {node: '>=16.9.0'} + + hookified@1.15.1: + resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} + + hookified@2.1.1: + resolution: {integrity: sha512-AHb76R16GB5EsPBE2J7Ko5kiEyXwviB9P5SMrAKcuAu4vJPZttViAbj9+tZeaQE5zjDme+1vcHP78Yj/WoAveA==} + + hosted-git-info@9.0.2: + resolution: {integrity: sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==} + engines: {node: ^20.17.0 || >=22.9.0} + + html-escaper@3.0.3: + resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + htmlparser2@10.1.0: + resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + http-proxy-agent@9.0.0: + resolution: {integrity: sha512-FcF8VhXYLQcxWCnt/cCpT2apKsRDUGeVEeMqGu4HSTu29U8Yw0TLOjdYIlDsYk3IkUh+taX4IDWpPcCqKDhCjA==} + engines: {node: '>= 20'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + https-proxy-agent@9.0.0: + resolution: {integrity: sha512-/MVmHp58WkOypgFhCLk4fzpPcFQvTJ/e6LBI7irpIO2HfxUbpmYoHF+KzipzJpxxzJu7aJNWQ0xojJ/dzV2G5g==} + engines: {node: '>= 20'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + image-q@4.0.0: + resolution: {integrity: sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==} + + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + ipaddr.js@2.3.0: + resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} + engines: {node: '>= 10'} + + is-electron@2.2.2: + resolution: {integrity: sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-network-error@1.3.1: + resolution: {integrity: sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw==} + engines: {node: '>=16'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jimp@1.6.1: + resolution: {integrity: sha512-hNQh6rZtWfSVWSNVmvq87N5BPJsNH7k7I7qyrXf9DOma9xATQk3fsyHazCQe51nCjdkoWdTmh0vD7bjVSLoxxw==} + engines: {node: '>=18'} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + jose@6.2.2: + resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==} + + jpeg-js@0.4.4: + resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} + + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + + json-bignum@0.0.3: + resolution: {integrity: sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==} + engines: {node: '>=0.8'} + + json-schema-to-ts@3.1.1: + resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} + engines: {node: '>=16'} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + jwt-decode@4.0.0: + resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} + engines: {node: '>=18'} + + keyv@5.6.0: + resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} + + koffi@2.16.1: + resolution: {integrity: sha512-0Ie6CfD026dNfWSosDw9dPxPzO9Rlyo0N8m5r05S8YjytIpuilzMFDMY4IDy/8xQsTwpuVinhncD+S8n3bcYZQ==} + + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + + linkedom@0.18.12: + resolution: {integrity: sha512-jalJsOwIKuQJSeTvsgzPe9iJzyfVaEJiEXl+25EkKevsULHvMJzpNqwvj1jOESWdmgKDiXObyjOYwlUqG7wo1Q==} + engines: {node: '>=16'} + peerDependencies: + canvas: '>= 2' + peerDependenciesMeta: + canvas: + optional: true + + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.identity@3.0.0: + resolution: {integrity: sha512-AupTIzdLQxJS5wIYUQlgGyk2XRTfGXA+MCghDHqZk0pzUNYvd3EESS6dkChNauNYVIutcb0dfHw1ri9Q1yPV8Q==} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + lodash.pickby@4.6.0: + resolution: {integrity: sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q==} + + loglevel@1.9.2: + resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==} + engines: {node: '>= 0.6.0'} + + long@4.0.0: + resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@11.3.5: + resolution: {integrity: sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==} + engines: {node: 20 || >=22} + + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + lru_map@0.4.1: + resolution: {integrity: sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + + markdown-it@14.1.1: + resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} + hasBin: true + + marked@15.0.12: + resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==} + engines: {node: '>= 18'} + hasBin: true + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + matrix-events-sdk@0.0.1: + resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==} + + matrix-js-sdk@41.3.0: + resolution: {integrity: sha512-QTNHpBQEKPH3WS4O92CBfFj6GxeyijT8osI/QxNvOrM3rE6CySXRtRRKnzR0ntFSdrk1CxrDGV6h2wmk7B3peQ==} + engines: {node: '>=22.0.0'} + + matrix-widget-api@1.17.0: + resolution: {integrity: sha512-5FHoo3iEP3Bdlv5jsYPWOqj+pGdFQNLWnJLiB0V7Ygne7bb+Gsj3ibyFyHWC6BVw+Z+tSW4ljHpO17I9TwStwQ==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + mpg123-decoder@1.0.3: + resolution: {integrity: sha512-+fjxnWigodWJm3+4pndi+KUg9TBojgn31DPk85zEsim7C6s0X5Ztc/hQYdytXkwuGXH+aB0/aEkG40Emukv6oQ==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + music-metadata@11.12.3: + resolution: {integrity: sha512-n6hSTZkuD59qWgHh6IP5dtDlDZQXoxk/bcA85Jywg8Z1iFrlNgl2+GTFgjZyn52W5UgQpV42V4XqrQZZAMbZTQ==} + engines: {node: '>=18'} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + netmask@2.1.1: + resolution: {integrity: sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==} + engines: {node: '>= 0.4.0'} + + node-addon-api@8.7.0: + resolution: {integrity: sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==} + engines: {node: ^18 || ^20 || >= 21} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-downloader-helper@2.1.11: + resolution: {integrity: sha512-882fH2C9AWdiPCwz/2beq5t8FGMZK9Dx8TJUOIxzMCbvG7XUKM5BuJwN5f0NKo4SCQK6jR4p2TPm54mYGdGchQ==} + engines: {node: '>=14.18'} + hasBin: true + + node-edge-tts@1.2.10: + resolution: {integrity: sha512-bV2i4XU54D45+US0Zm1HcJRkifuB3W438dWyuJEHLQdKxnuqlI1kim2MOvR6Q3XUQZvfF9PoDyR1Rt7aeXhPdQ==} + hasBin: true + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + node-readable-to-web-readable-stream@0.4.2: + resolution: {integrity: sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==} + + nopt@5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} + engines: {node: '>=6'} + hasBin: true + + nostr-tools@2.23.3: + resolution: {integrity: sha512-AALyt9k8xPdF4UV2mlLJ2mgCn4kpTB0DZ8t2r6wjdUh6anfx2cTVBsHUlo9U0EY/cKC5wcNyiMAmRJV5OVEalA==} + peerDependencies: + typescript: '>=5.0.0' + peerDependenciesMeta: + typescript: + optional: true + + nostr-wasm@0.1.0: + resolution: {integrity: sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==} + + npmlog@5.0.1: + resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} + deprecated: This package is no longer supported. + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + oidc-client-ts@3.5.0: + resolution: {integrity: sha512-l2q8l9CTCTOlbX+AnK4p3M+4CEpKpyQhle6blQkdFhm0IsBqsxm15bYaSa11G7pWdsYr6epdsRZxJpCyCRbT8A==} + engines: {node: '>=18'} + + omggif@1.0.10: + resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + oniguruma-parser@0.12.2: + resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} + + oniguruma-to-es@4.3.6: + resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} + + openai@6.26.0: + resolution: {integrity: sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + + openai@6.34.0: + resolution: {integrity: sha512-yEr2jdGf4tVFYG6ohmr3pF6VJuveP0EA/sS8TBx+4Eq5NT10alu5zg2dmxMXMgqpihRDQlFGpRt2XwsGj+Fyxw==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + + openclaw@2026.4.15-beta.1: + resolution: {integrity: sha512-KU9kbBHVRgBCIGYlNRhgkK+GgqUBWdPJpUPeF8qTRI/HlTyMJ+BaK4DGPLjMGcG9q6SB8d5SajCcy5dKhtTMcg==} + engines: {node: '>=22.14.0'} + hasBin: true + peerDependencies: + '@napi-rs/canvas': ^0.1.89 + node-llama-cpp: 3.18.1 + peerDependenciesMeta: + node-llama-cpp: + optional: true + + opusscript@0.1.1: + resolution: {integrity: sha512-mL0fZZOUnXdZ78woRXp18lApwpp0lF5tozJOD1Wut0dgrA9WuQTgSels/CSmFleaAZrJi/nci5KOVtbuxeWoQA==} + + osc-progress@0.3.0: + resolution: {integrity: sha512-4/8JfsetakdeEa4vAYV45FW20aY+B/+K8NEXp5Eiar3wR8726whgHrbSg5Ar/ZY1FLJ/AGtUqV7W2IVF+Gvp9A==} + engines: {node: '>=20'} + + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + + p-queue@6.6.2: + resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} + engines: {node: '>=8'} + + p-queue@9.1.2: + resolution: {integrity: sha512-ktsDOALzTYTWWF1PbkNVg2rOt+HaOaMWJMUnt7T3qf5tvZ1L8dBW3tObzprBcXNMKkwj+yFSLqHso0x+UFcJXw==} + engines: {node: '>=20'} + + p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + + p-retry@7.1.1: + resolution: {integrity: sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==} + engines: {node: '>=20'} + + p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + + p-timeout@7.0.1: + resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==} + engines: {node: '>=20'} + + pac-proxy-agent@7.2.0: + resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} + engines: {node: '>= 14'} + + pac-proxy-agent@9.0.1: + resolution: {integrity: sha512-3ZOSpLboOlpW4yp8Cuv21KlTULRqyJ5Uuad3wXpSKFrxdNgcHEyoa22GRaZ2UlgCVuR6z+5BiavtYVvbajL/Yw==} + engines: {node: '>= 20'} + + pac-resolver@7.0.1: + resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} + engines: {node: '>= 14'} + + pac-resolver@9.0.1: + resolution: {integrity: sha512-lJbS008tmkj08VhoM8Hzuv/VE5tK9MS0OIQ/7+s0lIF+BYhiQWFYzkSpML7lXs9iBu2jfmzBTLzhe9n6BX+dYw==} + engines: {node: '>= 20'} + peerDependencies: + quickjs-wasi: ^2.2.0 + + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + + parse-bmfont-ascii@1.0.6: + resolution: {integrity: sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==} + + parse-bmfont-binary@1.0.6: + resolution: {integrity: sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==} + + parse-bmfont-xml@1.1.6: + resolution: {integrity: sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==} + + parse5-htmlparser2-tree-adapter@6.0.1: + resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} + + parse5@5.1.1: + resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} + + parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + partial-json@0.1.7: + resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==} + + path-expression-matcher@1.5.0: + resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} + engines: {node: '>=14.0.0'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + pdfjs-dist@5.6.205: + resolution: {integrity: sha512-tlUj+2IDa7G1SbvBNN74UHRLJybZDWYom+k6p5KIZl7huBvsA4APi6mKL+zCxd3tLjN5hOOEE9Tv7VdzO88pfg==} + engines: {node: '>=20.19.0 || >=22.13.0 || >=24'} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@9.14.0: + resolution: {integrity: sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==} + hasBin: true + + pixelmatch@5.3.0: + resolution: {integrity: sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==} + hasBin: true + + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + + playwright-core@1.59.1: + resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} + engines: {node: '>=18'} + hasBin: true + + pngjs@6.0.0: + resolution: {integrity: sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==} + engines: {node: '>=12.13.0'} + + pngjs@7.0.0: + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} + engines: {node: '>=14.19.0'} + + postcss@8.5.10: + resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} + engines: {node: ^10 || ^12 || >=14} + + prism-media@1.3.5: + resolution: {integrity: sha512-IQdl0Q01m4LrkN1EGIE9lphov5Hy7WWlH6ulf5QdGePLlPas9p2mhgddTEHrlaXYjjFToM1/rWuwF37VF4taaA==} + peerDependencies: + '@discordjs/opus': '>=0.8.0 <1.0.0' + ffmpeg-static: ^5.0.2 || ^4.2.7 || ^3.0.0 || ^2.4.0 + node-opus: ^0.3.3 + opusscript: ^0.0.8 + peerDependenciesMeta: + '@discordjs/opus': + optional: true + ffmpeg-static: + optional: true + node-opus: + optional: true + opusscript: + optional: true + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + + protobufjs@6.8.8: + resolution: {integrity: sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==} + hasBin: true + + protobufjs@7.5.5: + resolution: {integrity: sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==} + engines: {node: '>=12.0.0'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-agent@6.5.0: + resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} + engines: {node: '>= 14'} + + proxy-agent@8.0.1: + resolution: {integrity: sha512-kccqGBqHZXR8onQhY/ganJjoO8QIKKRiFBhPOzbTZK16attzSZ/0XSmp9H7jrRxPKHjhGyx1q32lMPrJ3uLFgA==} + engines: {node: '>= 20'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + + qified@0.9.1: + resolution: {integrity: sha512-n7mar4T0xQ+39dE2vGTAlbxUEpndwPANH0kDef1/MYsB8Bba9wshkybIRx74qgcvKQPEWErf9AqAdYjhzY2Ilg==} + engines: {node: '>=20'} + + qrcode-terminal@0.12.0: + resolution: {integrity: sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==} + hasBin: true + + qs@6.15.1: + resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} + engines: {node: '>=0.6'} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + quickjs-wasi@2.2.0: + resolution: {integrity: sha512-zQxXmQMrEoD3S+jQdYsloq4qAuaxKFHZj6hHqOYGwB2iQZH+q9e/lf5zQPXCKOk0WJuAjzRFbO4KwHIp2D05Iw==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + react-dom@19.2.5: + resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==} + peerDependencies: + react: ^19.2.5 + + react@19.2.5: + resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} + engines: {node: '>=0.10.0'} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rollup@4.60.2: + resolution: {integrity: sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + sdp-transform@3.0.0: + resolution: {integrity: sha512-gfYVRGxjHkGF2NPeUWHw5u6T/KGFtS5/drPms73gaSuMaVHKCY3lpLnGDfswVQO0kddeePoti09AwhYP4zA8dQ==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shiki@3.23.0: + resolution: {integrity: sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + silk-wasm@3.7.1: + resolution: {integrity: sha512-mXPwLRtZxrYV3TZx41jMAeKc80wvmyrcXIcs8HctFxK15Ahz2OJQENYhNgEPeCEOdI6Mbx1NxQsqxzwc3DKerw==} + engines: {node: '>=16.11.0'} + + simple-xml-to-json@1.2.7: + resolution: {integrity: sha512-mz9VXphOxQWX3eQ/uXCtm6upltoN0DLx8Zb5T4TFC4FHB7S9FDPGre8CfLWqPWQQH/GrQYd2AXhhVM5LDpYx6Q==} + engines: {node: '>=20.12.2'} + + simple-yenc@1.0.4: + resolution: {integrity: sha512-5gvxpSd79e9a3V4QDYUqnqxeD4HGlhCakVpb6gMnDD7lexJggSBJRBO5h52y/iJrdXRilX9UCuDaIJhSWm5OWw==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks-proxy-agent@10.0.0: + resolution: {integrity: sha512-pyp2YR3mNxAMu0mGLtzs4g7O3uT4/9sQOLAKcViAkaS9fJWkud7nmaf6ZREFqQEi24IPkBcjfHjXhPTUWjo3uA==} + engines: {node: '>= 20'} + + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.7: + resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + sqlite-vec-darwin-arm64@0.1.9: + resolution: {integrity: sha512-jSsZpE42OfBkGL/ItyJTVCUwl6o6Ka3U5rc4j+UBDIQzC1ulSSKMEhQLthsOnF/MdAf1MuAkYhkdKmmcjaIZQg==} + cpu: [arm64] + os: [darwin] + + sqlite-vec-darwin-x64@0.1.9: + resolution: {integrity: sha512-KDlVyqQT7pnOhU1ymB9gs7dMbSoVmKHitT+k1/xkjarcX8bBqPxWrGlK/R+C5WmWkfvWwyq5FfXfiBYCBs6PlA==} + cpu: [x64] + os: [darwin] + + sqlite-vec-linux-arm64@0.1.9: + resolution: {integrity: sha512-5wXVJ9c9kR4CHm/wVqXb/R+XUHTdpZ4nWbPHlS+gc9qQFVHs92Km4bPnCKX4rtcPMzvNis+SIzMJR1SCEwpuUw==} + cpu: [arm64] + os: [linux] + + sqlite-vec-linux-x64@0.1.9: + resolution: {integrity: sha512-w3tCH8xK2finW8fQJ/m8uqKodXUZ9KAuAar2UIhz4BHILfpE0WM/MTGCRfa7RjYbrYim5Luk3guvMOGI7T7JQA==} + cpu: [x64] + os: [linux] + + sqlite-vec-windows-x64@0.1.9: + resolution: {integrity: sha512-y3gEIyy/17bq2QFPQOWLE68TYWcRZkBQVA2XLrTPHNTOp55xJi/BBBmOm40tVMDMjtP+Elpk6UBUXdaq+46b0Q==} + cpu: [x64] + os: [win32] + + sqlite-vec@0.1.9: + resolution: {integrity: sha512-L7XJWRIBNvR9O5+vh1FQ+IGkh/3D2AzVksW5gdtk28m78Hy8skFD0pqReKH1Yp0/BUKRGcffgKvyO/EON5JXpA==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strnum@2.2.3: + resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==} + + strtok3@10.3.5: + resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} + engines: {node: '>=18'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + table-layout@4.1.1: + resolution: {integrity: sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==} + engines: {node: '>=12.17'} + + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + tar@7.5.13: + resolution: {integrity: sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==} + engines: {node: '>=18'} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + token-types@6.1.2: + resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} + engines: {node: '>=14.16'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + ts-algebra@2.0.0: + resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tslog@4.10.2: + resolution: {integrity: sha512-XuELoRpMR+sq8fuWwX7P0bcj+PRNiicOKDEb3fGNURhxWVyykCi9BNq7c4uVz7h7P0sj8qgBsr5SWS6yBClq3g==} + engines: {node: '>=16'} + + tsscmp@1.0.6: + resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} + engines: {node: '>=0.6.x'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + typical@4.0.0: + resolution: {integrity: sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==} + engines: {node: '>=8'} + + typical@7.3.0: + resolution: {integrity: sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==} + engines: {node: '>=12.17'} + + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + + uhyphen@0.2.0: + resolution: {integrity: sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==} + + uint8array-extras@1.5.0: + resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} + engines: {node: '>=18'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici-types@7.19.2: + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} + + undici@7.25.0: + resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} + engines: {node: '>=20.18.1'} + + undici@8.0.2: + resolution: {integrity: sha512-B9MeU5wuFhkFAuNeA19K2GDFcQXZxq33fL0nRy2Aq30wdufZbyyvxW3/ChaeipXVfy/wUweZyzovQGk39+9k2w==} + engines: {node: '>=22.19.0'} + + unhomoglyph@1.0.6: + resolution: {integrity: sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + utif2@4.1.0: + resolution: {integrity: sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@13.0.0: + resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} + hasBin: true + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: 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 + + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + + win-guid@0.2.1: + resolution: {integrity: sha512-gEIQU4mkgl2OPeoNrWflcJFJ3Ae2BPd4eCsHHA/XikslkIVms/nHhvnvzIZV7VLmBvtFlDOzLt9rrZT+n6D67A==} + + wordwrapjs@5.1.1: + resolution: {integrity: sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==} + engines: {node: '>=12.17'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.20.0: + resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + 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 + + xml-parse-from-string@1.0.1: + resolution: {integrity: sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==} + + xml2js@0.5.0: + resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@agentclientprotocol/sdk@0.18.2(zod@4.3.6)': + dependencies: + zod: 4.3.6 + + '@anthropic-ai/sdk@0.73.0(zod@4.3.6)': + dependencies: + json-schema-to-ts: 3.1.1 + optionalDependencies: + zod: 4.3.6 + + '@anthropic-ai/sdk@0.90.0(zod@4.3.6)': + dependencies: + json-schema-to-ts: 3.1.1 + optionalDependencies: + zod: 4.3.6 + + '@anthropic-ai/vertex-sdk@0.15.0(zod@4.3.6)': + dependencies: + '@anthropic-ai/sdk': 0.90.0(zod@4.3.6) + google-auth-library: 9.15.1 + transitivePeerDependencies: + - encoding + - supports-color + - zod + + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.8 + tslib: 2.8.1 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-locate-window': 3.965.5 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.8 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-bedrock-runtime@3.1028.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.5 + '@aws-sdk/credential-provider-node': 3.972.36 + '@aws-sdk/eventstream-handler-node': 3.972.14 + '@aws-sdk/middleware-eventstream': 3.972.10 + '@aws-sdk/middleware-host-header': 3.972.10 + '@aws-sdk/middleware-logger': 3.972.10 + '@aws-sdk/middleware-recursion-detection': 3.972.11 + '@aws-sdk/middleware-user-agent': 3.972.35 + '@aws-sdk/middleware-websocket': 3.972.16 + '@aws-sdk/region-config-resolver': 3.972.13 + '@aws-sdk/token-providers': 3.1028.0 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-endpoints': 3.996.8 + '@aws-sdk/util-user-agent-browser': 3.972.10 + '@aws-sdk/util-user-agent-node': 3.973.21 + '@smithy/config-resolver': 4.4.17 + '@smithy/core': 3.23.17 + '@smithy/eventstream-serde-browser': 4.2.14 + '@smithy/eventstream-serde-config-resolver': 4.3.14 + '@smithy/eventstream-serde-node': 4.2.14 + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/hash-node': 4.2.14 + '@smithy/invalid-dependency': 4.2.14 + '@smithy/middleware-content-length': 4.2.14 + '@smithy/middleware-endpoint': 4.4.32 + '@smithy/middleware-retry': 4.5.5 + '@smithy/middleware-serde': 4.2.20 + '@smithy/middleware-stack': 4.2.14 + '@smithy/node-config-provider': 4.3.14 + '@smithy/node-http-handler': 4.6.1 + '@smithy/protocol-http': 5.3.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.49 + '@smithy/util-defaults-mode-node': 4.2.54 + '@smithy/util-endpoints': 3.4.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-retry': 4.3.4 + '@smithy/util-stream': 4.5.25 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-bedrock-runtime@3.1036.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.5 + '@aws-sdk/credential-provider-node': 3.972.36 + '@aws-sdk/eventstream-handler-node': 3.972.14 + '@aws-sdk/middleware-eventstream': 3.972.10 + '@aws-sdk/middleware-host-header': 3.972.10 + '@aws-sdk/middleware-logger': 3.972.10 + '@aws-sdk/middleware-recursion-detection': 3.972.11 + '@aws-sdk/middleware-user-agent': 3.972.35 + '@aws-sdk/middleware-websocket': 3.972.16 + '@aws-sdk/region-config-resolver': 3.972.13 + '@aws-sdk/token-providers': 3.1036.0 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-endpoints': 3.996.8 + '@aws-sdk/util-user-agent-browser': 3.972.10 + '@aws-sdk/util-user-agent-node': 3.973.21 + '@smithy/config-resolver': 4.4.17 + '@smithy/core': 3.23.17 + '@smithy/eventstream-serde-browser': 4.2.14 + '@smithy/eventstream-serde-config-resolver': 4.3.14 + '@smithy/eventstream-serde-node': 4.2.14 + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/hash-node': 4.2.14 + '@smithy/invalid-dependency': 4.2.14 + '@smithy/middleware-content-length': 4.2.14 + '@smithy/middleware-endpoint': 4.4.32 + '@smithy/middleware-retry': 4.5.5 + '@smithy/middleware-serde': 4.2.20 + '@smithy/middleware-stack': 4.2.14 + '@smithy/node-config-provider': 4.3.14 + '@smithy/node-http-handler': 4.6.1 + '@smithy/protocol-http': 5.3.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.49 + '@smithy/util-defaults-mode-node': 4.2.54 + '@smithy/util-endpoints': 3.4.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-retry': 4.3.4 + '@smithy/util-stream': 4.5.25 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-bedrock@3.1028.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.5 + '@aws-sdk/credential-provider-node': 3.972.36 + '@aws-sdk/middleware-host-header': 3.972.10 + '@aws-sdk/middleware-logger': 3.972.10 + '@aws-sdk/middleware-recursion-detection': 3.972.11 + '@aws-sdk/middleware-user-agent': 3.972.35 + '@aws-sdk/region-config-resolver': 3.972.13 + '@aws-sdk/token-providers': 3.1028.0 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-endpoints': 3.996.8 + '@aws-sdk/util-user-agent-browser': 3.972.10 + '@aws-sdk/util-user-agent-node': 3.973.21 + '@smithy/config-resolver': 4.4.17 + '@smithy/core': 3.23.17 + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/hash-node': 4.2.14 + '@smithy/invalid-dependency': 4.2.14 + '@smithy/middleware-content-length': 4.2.14 + '@smithy/middleware-endpoint': 4.4.32 + '@smithy/middleware-retry': 4.5.5 + '@smithy/middleware-serde': 4.2.20 + '@smithy/middleware-stack': 4.2.14 + '@smithy/node-config-provider': 4.3.14 + '@smithy/node-http-handler': 4.6.1 + '@smithy/protocol-http': 5.3.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.49 + '@smithy/util-defaults-mode-node': 4.2.54 + '@smithy/util-endpoints': 3.4.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-retry': 4.3.4 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-cognito-identity@3.1036.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.5 + '@aws-sdk/credential-provider-node': 3.972.36 + '@aws-sdk/middleware-host-header': 3.972.10 + '@aws-sdk/middleware-logger': 3.972.10 + '@aws-sdk/middleware-recursion-detection': 3.972.11 + '@aws-sdk/middleware-user-agent': 3.972.35 + '@aws-sdk/region-config-resolver': 3.972.13 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-endpoints': 3.996.8 + '@aws-sdk/util-user-agent-browser': 3.972.10 + '@aws-sdk/util-user-agent-node': 3.973.21 + '@smithy/config-resolver': 4.4.17 + '@smithy/core': 3.23.17 + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/hash-node': 4.2.14 + '@smithy/invalid-dependency': 4.2.14 + '@smithy/middleware-content-length': 4.2.14 + '@smithy/middleware-endpoint': 4.4.32 + '@smithy/middleware-retry': 4.5.5 + '@smithy/middleware-serde': 4.2.20 + '@smithy/middleware-stack': 4.2.14 + '@smithy/node-config-provider': 4.3.14 + '@smithy/node-http-handler': 4.6.1 + '@smithy/protocol-http': 5.3.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.49 + '@smithy/util-defaults-mode-node': 4.2.54 + '@smithy/util-endpoints': 3.4.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-retry': 4.3.4 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/core@3.974.5': + dependencies: + '@aws-sdk/types': 3.973.8 + '@aws-sdk/xml-builder': 3.972.19 + '@smithy/core': 3.23.17 + '@smithy/node-config-provider': 4.3.14 + '@smithy/property-provider': 4.2.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/signature-v4': 5.3.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + '@smithy/util-base64': 4.3.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-retry': 4.3.4 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-cognito-identity@3.972.28': + dependencies: + '@aws-sdk/nested-clients': 3.997.3 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-env@3.972.31': + dependencies: + '@aws-sdk/core': 3.974.5 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.33': + dependencies: + '@aws-sdk/core': 3.974.5 + '@aws-sdk/types': 3.973.8 + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/node-http-handler': 4.6.1 + '@smithy/property-provider': 4.2.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + '@smithy/util-stream': 4.5.25 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.972.35': + dependencies: + '@aws-sdk/core': 3.974.5 + '@aws-sdk/credential-provider-env': 3.972.31 + '@aws-sdk/credential-provider-http': 3.972.33 + '@aws-sdk/credential-provider-login': 3.972.35 + '@aws-sdk/credential-provider-process': 3.972.31 + '@aws-sdk/credential-provider-sso': 3.972.35 + '@aws-sdk/credential-provider-web-identity': 3.972.35 + '@aws-sdk/nested-clients': 3.997.3 + '@aws-sdk/types': 3.973.8 + '@smithy/credential-provider-imds': 4.2.14 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-login@3.972.35': + dependencies: + '@aws-sdk/core': 3.974.5 + '@aws-sdk/nested-clients': 3.997.3 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-node@3.972.30': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.31 + '@aws-sdk/credential-provider-http': 3.972.33 + '@aws-sdk/credential-provider-ini': 3.972.35 + '@aws-sdk/credential-provider-process': 3.972.31 + '@aws-sdk/credential-provider-sso': 3.972.35 + '@aws-sdk/credential-provider-web-identity': 3.972.35 + '@aws-sdk/types': 3.973.8 + '@smithy/credential-provider-imds': 4.2.14 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-node@3.972.36': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.31 + '@aws-sdk/credential-provider-http': 3.972.33 + '@aws-sdk/credential-provider-ini': 3.972.35 + '@aws-sdk/credential-provider-process': 3.972.31 + '@aws-sdk/credential-provider-sso': 3.972.35 + '@aws-sdk/credential-provider-web-identity': 3.972.35 + '@aws-sdk/types': 3.973.8 + '@smithy/credential-provider-imds': 4.2.14 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-process@3.972.31': + dependencies: + '@aws-sdk/core': 3.974.5 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.972.35': + dependencies: + '@aws-sdk/core': 3.974.5 + '@aws-sdk/nested-clients': 3.997.3 + '@aws-sdk/token-providers': 3.1036.0 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-web-identity@3.972.35': + dependencies: + '@aws-sdk/core': 3.974.5 + '@aws-sdk/nested-clients': 3.997.3 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-providers@3.1036.0': + dependencies: + '@aws-sdk/client-cognito-identity': 3.1036.0 + '@aws-sdk/core': 3.974.5 + '@aws-sdk/credential-provider-cognito-identity': 3.972.28 + '@aws-sdk/credential-provider-env': 3.972.31 + '@aws-sdk/credential-provider-http': 3.972.33 + '@aws-sdk/credential-provider-ini': 3.972.35 + '@aws-sdk/credential-provider-login': 3.972.35 + '@aws-sdk/credential-provider-node': 3.972.36 + '@aws-sdk/credential-provider-process': 3.972.31 + '@aws-sdk/credential-provider-sso': 3.972.35 + '@aws-sdk/credential-provider-web-identity': 3.972.35 + '@aws-sdk/nested-clients': 3.997.3 + '@aws-sdk/types': 3.973.8 + '@smithy/config-resolver': 4.4.17 + '@smithy/core': 3.23.17 + '@smithy/credential-provider-imds': 4.2.14 + '@smithy/node-config-provider': 4.3.14 + '@smithy/property-provider': 4.2.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/eventstream-handler-node@3.972.14': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/eventstream-codec': 4.2.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-eventstream@3.972.10': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-host-header@3.972.10': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-logger@3.972.10': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-recursion-detection@3.972.11': + dependencies: + '@aws-sdk/types': 3.973.8 + '@aws/lambda-invoke-store': 0.2.4 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.972.34': + dependencies: + '@aws-sdk/core': 3.974.5 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-arn-parser': 3.972.3 + '@smithy/core': 3.23.17 + '@smithy/node-config-provider': 4.3.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/signature-v4': 5.3.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + '@smithy/util-config-provider': 4.2.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-stream': 4.5.25 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-user-agent@3.972.35': + dependencies: + '@aws-sdk/core': 3.974.5 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-endpoints': 3.996.8 + '@smithy/core': 3.23.17 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + '@smithy/util-retry': 4.3.4 + tslib: 2.8.1 + + '@aws-sdk/middleware-websocket@3.972.16': + dependencies: + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-format-url': 3.972.10 + '@smithy/eventstream-codec': 4.2.14 + '@smithy/eventstream-serde-browser': 4.2.14 + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/protocol-http': 5.3.14 + '@smithy/signature-v4': 5.3.14 + '@smithy/types': 4.14.1 + '@smithy/util-base64': 4.3.2 + '@smithy/util-hex-encoding': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.3': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.5 + '@aws-sdk/middleware-host-header': 3.972.10 + '@aws-sdk/middleware-logger': 3.972.10 + '@aws-sdk/middleware-recursion-detection': 3.972.11 + '@aws-sdk/middleware-user-agent': 3.972.35 + '@aws-sdk/region-config-resolver': 3.972.13 + '@aws-sdk/signature-v4-multi-region': 3.996.22 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-endpoints': 3.996.8 + '@aws-sdk/util-user-agent-browser': 3.972.10 + '@aws-sdk/util-user-agent-node': 3.973.21 + '@smithy/config-resolver': 4.4.17 + '@smithy/core': 3.23.17 + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/hash-node': 4.2.14 + '@smithy/invalid-dependency': 4.2.14 + '@smithy/middleware-content-length': 4.2.14 + '@smithy/middleware-endpoint': 4.4.32 + '@smithy/middleware-retry': 4.5.5 + '@smithy/middleware-serde': 4.2.20 + '@smithy/middleware-stack': 4.2.14 + '@smithy/node-config-provider': 4.3.14 + '@smithy/node-http-handler': 4.6.1 + '@smithy/protocol-http': 5.3.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.49 + '@smithy/util-defaults-mode-node': 4.2.54 + '@smithy/util-endpoints': 3.4.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-retry': 4.3.4 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/region-config-resolver@3.972.13': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/config-resolver': 4.4.17 + '@smithy/node-config-provider': 4.3.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.22': + dependencies: + '@aws-sdk/middleware-sdk-s3': 3.972.34 + '@aws-sdk/types': 3.973.8 + '@smithy/protocol-http': 5.3.14 + '@smithy/signature-v4': 5.3.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1028.0': + dependencies: + '@aws-sdk/core': 3.974.5 + '@aws-sdk/nested-clients': 3.997.3 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/token-providers@3.1036.0': + dependencies: + '@aws-sdk/core': 3.974.5 + '@aws-sdk/nested-clients': 3.997.3 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/types@3.973.8': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/util-arn-parser@3.972.3': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-endpoints@3.996.8': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + '@smithy/util-endpoints': 3.4.2 + tslib: 2.8.1 + + '@aws-sdk/util-format-url@3.972.10': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/querystring-builder': 4.2.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.5': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-browser@3.972.10': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.1 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-node@3.973.21': + dependencies: + '@aws-sdk/middleware-user-agent': 3.972.35 + '@aws-sdk/types': 3.973.8 + '@smithy/node-config-provider': 4.3.14 + '@smithy/types': 4.14.1 + '@smithy/util-config-provider': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.19': + dependencies: + '@smithy/types': 4.14.1 + fast-xml-parser: 5.7.1 + tslib: 2.8.1 + + '@aws/bedrock-token-generator@1.1.0': + dependencies: + '@aws-sdk/credential-providers': 3.1036.0 + '@aws-sdk/util-format-url': 3.972.10 + '@smithy/config-resolver': 4.4.17 + '@smithy/hash-node': 4.2.14 + '@smithy/invalid-dependency': 4.2.14 + '@smithy/node-config-provider': 4.3.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/signature-v4': 5.3.14 + '@smithy/types': 4.14.1 + transitivePeerDependencies: + - aws-crt + + '@aws/lambda-invoke-store@0.2.4': {} + + '@babel/runtime@7.29.2': {} + + '@borewit/text-codec@0.2.2': {} + + '@buape/carbon@0.15.0(@discordjs/opus@0.10.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(hono@4.12.14)(opusscript@0.1.1)': + dependencies: + '@types/node': 25.6.0 + discord-api-types: 0.38.45 + optionalDependencies: + '@cloudflare/workers-types': 4.20260405.1 + '@discordjs/voice': 0.19.2(@discordjs/opus@0.10.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(opusscript@0.1.1) + '@hono/node-server': 1.19.13(hono@4.12.14) + '@types/bun': 1.3.11 + '@types/ws': 8.18.1 + ws: 8.20.0 + transitivePeerDependencies: + - '@discordjs/opus' + - '@emnapi/core' + - '@emnapi/runtime' + - bufferutil + - ffmpeg-static + - hono + - node-opus + - opusscript + - utf-8-validate + + '@cacheable/memory@2.0.8': + dependencies: + '@cacheable/utils': 2.4.1 + '@keyv/bigmap': 1.3.1(keyv@5.6.0) + hookified: 1.15.1 + keyv: 5.6.0 + + '@cacheable/node-cache@1.7.6': + dependencies: + cacheable: 2.3.4 + hookified: 1.15.1 + keyv: 5.6.0 + + '@cacheable/utils@2.4.1': + dependencies: + hashery: 1.5.1 + keyv: 5.6.0 + + '@clack/core@1.2.0': + dependencies: + fast-wrap-ansi: 0.1.6 + sisteransi: 1.0.5 + + '@clack/prompts@1.2.0': + dependencies: + '@clack/core': 1.2.0 + fast-string-width: 1.1.0 + fast-wrap-ansi: 0.1.6 + sisteransi: 1.0.5 + + '@cloudflare/workers-types@4.20260405.1': + optional: true + + '@discordjs/node-pre-gyp@0.4.5': + dependencies: + detect-libc: 2.1.2 + https-proxy-agent: 5.0.1 + make-dir: 3.1.0 + node-fetch: 2.7.0 + nopt: 5.0.0 + npmlog: 5.0.1 + rimraf: 3.0.2 + semver: 7.7.4 + tar: 6.2.1 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + '@discordjs/opus@0.10.0': + dependencies: + '@discordjs/node-pre-gyp': 0.4.5 + node-addon-api: 8.7.0 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + '@discordjs/voice@0.19.2(@discordjs/opus@0.10.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(opusscript@0.1.1)': + dependencies: + '@snazzah/davey': 0.1.11(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@types/ws': 8.18.1 + discord-api-types: 0.38.47 + prism-media: 1.3.5(@discordjs/opus@0.10.0)(opusscript@0.1.1) + tslib: 2.8.1 + ws: 8.20.0 + transitivePeerDependencies: + - '@discordjs/opus' + - '@emnapi/core' + - '@emnapi/runtime' + - bufferutil + - ffmpeg-static + - node-opus + - opusscript + - utf-8-validate + optional: true + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@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 + + '@eshaz/web-worker@1.2.2': {} + + '@google/genai@1.50.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))': + dependencies: + google-auth-library: 10.6.2 + p-retry: 4.6.2 + protobufjs: 7.5.5 + ws: 8.20.0 + optionalDependencies: + '@modelcontextprotocol/sdk': 1.29.0(zod@4.3.6) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@grammyjs/runner@2.0.3(grammy@1.42.0)': + dependencies: + abort-controller: 3.0.0 + grammy: 1.42.0 + + '@grammyjs/transformer-throttler@1.2.1(grammy@1.42.0)': + dependencies: + bottleneck: 2.19.5 + grammy: 1.42.0 + + '@grammyjs/types@3.26.0': {} + + '@hapi/boom@9.1.4': + dependencies: + '@hapi/hoek': 9.3.0 + + '@hapi/hoek@9.3.0': {} + + '@homebridge/ciao@1.3.6': + dependencies: + debug: 4.4.3 + fast-deep-equal: 3.1.3 + source-map-support: 0.5.21 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@hono/node-server@1.19.13(hono@4.12.14)': + dependencies: + hono: 4.12.14 + optional: true + + '@hono/node-server@1.19.14(hono@4.12.14)': + dependencies: + hono: 4.12.14 + + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.10.0 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.3 + + '@jimp/core@1.6.1': + dependencies: + '@jimp/file-ops': 1.6.1 + '@jimp/types': 1.6.1 + '@jimp/utils': 1.6.1 + await-to-js: 3.0.0 + exif-parser: 0.1.12 + file-type: 21.3.4 + mime: 3.0.0 + transitivePeerDependencies: + - supports-color + + '@jimp/diff@1.6.1': + dependencies: + '@jimp/plugin-resize': 1.6.1 + '@jimp/types': 1.6.1 + '@jimp/utils': 1.6.1 + pixelmatch: 5.3.0 + transitivePeerDependencies: + - supports-color + + '@jimp/file-ops@1.6.1': {} + + '@jimp/js-bmp@1.6.1': + dependencies: + '@jimp/core': 1.6.1 + '@jimp/types': 1.6.1 + '@jimp/utils': 1.6.1 + bmp-ts: 1.0.9 + transitivePeerDependencies: + - supports-color + + '@jimp/js-gif@1.6.1': + dependencies: + '@jimp/core': 1.6.1 + '@jimp/types': 1.6.1 + gifwrap: 0.10.1 + omggif: 1.0.10 + transitivePeerDependencies: + - supports-color + + '@jimp/js-jpeg@1.6.1': + dependencies: + '@jimp/core': 1.6.1 + '@jimp/types': 1.6.1 + jpeg-js: 0.4.4 + transitivePeerDependencies: + - supports-color + + '@jimp/js-png@1.6.1': + dependencies: + '@jimp/core': 1.6.1 + '@jimp/types': 1.6.1 + pngjs: 7.0.0 + transitivePeerDependencies: + - supports-color + + '@jimp/js-tiff@1.6.1': + dependencies: + '@jimp/core': 1.6.1 + '@jimp/types': 1.6.1 + utif2: 4.1.0 + transitivePeerDependencies: + - supports-color + + '@jimp/plugin-blit@1.6.1': + dependencies: + '@jimp/types': 1.6.1 + '@jimp/utils': 1.6.1 + zod: 3.25.76 + + '@jimp/plugin-blur@1.6.1': + dependencies: + '@jimp/core': 1.6.1 + '@jimp/utils': 1.6.1 + transitivePeerDependencies: + - supports-color + + '@jimp/plugin-circle@1.6.1': + dependencies: + '@jimp/types': 1.6.1 + zod: 3.25.76 + + '@jimp/plugin-color@1.6.1': + dependencies: + '@jimp/core': 1.6.1 + '@jimp/types': 1.6.1 + '@jimp/utils': 1.6.1 + tinycolor2: 1.6.0 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + + '@jimp/plugin-contain@1.6.1': + dependencies: + '@jimp/core': 1.6.1 + '@jimp/plugin-blit': 1.6.1 + '@jimp/plugin-resize': 1.6.1 + '@jimp/types': 1.6.1 + '@jimp/utils': 1.6.1 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + + '@jimp/plugin-cover@1.6.1': + dependencies: + '@jimp/core': 1.6.1 + '@jimp/plugin-crop': 1.6.1 + '@jimp/plugin-resize': 1.6.1 + '@jimp/types': 1.6.1 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + + '@jimp/plugin-crop@1.6.1': + dependencies: + '@jimp/core': 1.6.1 + '@jimp/types': 1.6.1 + '@jimp/utils': 1.6.1 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + + '@jimp/plugin-displace@1.6.1': + dependencies: + '@jimp/types': 1.6.1 + '@jimp/utils': 1.6.1 + zod: 3.25.76 + + '@jimp/plugin-dither@1.6.1': + dependencies: + '@jimp/types': 1.6.1 + + '@jimp/plugin-fisheye@1.6.1': + dependencies: + '@jimp/types': 1.6.1 + '@jimp/utils': 1.6.1 + zod: 3.25.76 + + '@jimp/plugin-flip@1.6.1': + dependencies: + '@jimp/types': 1.6.1 + zod: 3.25.76 + + '@jimp/plugin-hash@1.6.1': + dependencies: + '@jimp/core': 1.6.1 + '@jimp/js-bmp': 1.6.1 + '@jimp/js-jpeg': 1.6.1 + '@jimp/js-png': 1.6.1 + '@jimp/js-tiff': 1.6.1 + '@jimp/plugin-color': 1.6.1 + '@jimp/plugin-resize': 1.6.1 + '@jimp/types': 1.6.1 + '@jimp/utils': 1.6.1 + any-base: 1.1.0 + transitivePeerDependencies: + - supports-color + + '@jimp/plugin-mask@1.6.1': + dependencies: + '@jimp/types': 1.6.1 + zod: 3.25.76 + + '@jimp/plugin-print@1.6.1': + dependencies: + '@jimp/core': 1.6.1 + '@jimp/js-jpeg': 1.6.1 + '@jimp/js-png': 1.6.1 + '@jimp/plugin-blit': 1.6.1 + '@jimp/types': 1.6.1 + parse-bmfont-ascii: 1.0.6 + parse-bmfont-binary: 1.0.6 + parse-bmfont-xml: 1.1.6 + simple-xml-to-json: 1.2.7 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + + '@jimp/plugin-quantize@1.6.1': + dependencies: + image-q: 4.0.0 + zod: 3.25.76 + + '@jimp/plugin-resize@1.6.1': + dependencies: + '@jimp/core': 1.6.1 + '@jimp/types': 1.6.1 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + + '@jimp/plugin-rotate@1.6.1': + dependencies: + '@jimp/core': 1.6.1 + '@jimp/plugin-crop': 1.6.1 + '@jimp/plugin-resize': 1.6.1 + '@jimp/types': 1.6.1 + '@jimp/utils': 1.6.1 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + + '@jimp/plugin-threshold@1.6.1': + dependencies: + '@jimp/core': 1.6.1 + '@jimp/plugin-color': 1.6.1 + '@jimp/plugin-hash': 1.6.1 + '@jimp/types': 1.6.1 + '@jimp/utils': 1.6.1 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + + '@jimp/types@1.6.1': + dependencies: + zod: 3.25.76 + + '@jimp/utils@1.6.1': + dependencies: + '@jimp/types': 1.6.1 + tinycolor2: 1.6.0 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@keyv/bigmap@1.3.1(keyv@5.6.0)': + dependencies: + hashery: 1.5.1 + hookified: 1.15.1 + keyv: 5.6.0 + + '@keyv/serialize@1.1.1': {} + + '@lancedb/lancedb-darwin-arm64@0.27.2': + optional: true + + '@lancedb/lancedb-linux-arm64-gnu@0.27.2': + optional: true + + '@lancedb/lancedb-linux-arm64-musl@0.27.2': + optional: true + + '@lancedb/lancedb-linux-x64-gnu@0.27.2': + optional: true + + '@lancedb/lancedb-linux-x64-musl@0.27.2': + optional: true + + '@lancedb/lancedb-win32-arm64-msvc@0.27.2': + optional: true + + '@lancedb/lancedb-win32-x64-msvc@0.27.2': + optional: true + + '@lancedb/lancedb@0.27.2(apache-arrow@18.1.0)': + dependencies: + apache-arrow: 18.1.0 + reflect-metadata: 0.2.2 + optionalDependencies: + '@lancedb/lancedb-darwin-arm64': 0.27.2 + '@lancedb/lancedb-linux-arm64-gnu': 0.27.2 + '@lancedb/lancedb-linux-arm64-musl': 0.27.2 + '@lancedb/lancedb-linux-x64-gnu': 0.27.2 + '@lancedb/lancedb-linux-x64-musl': 0.27.2 + '@lancedb/lancedb-win32-arm64-msvc': 0.27.2 + '@lancedb/lancedb-win32-x64-msvc': 0.27.2 + + '@larksuiteoapi/node-sdk@1.61.1': + dependencies: + axios: 1.13.6 + lodash.identity: 3.0.0 + lodash.merge: 4.6.2 + lodash.pickby: 4.6.0 + protobufjs: 7.5.5 + qs: 6.15.1 + ws: 8.20.0 + transitivePeerDependencies: + - bufferutil + - debug + - utf-8-validate + + '@lydell/node-pty-darwin-arm64@1.2.0-beta.12': + optional: true + + '@lydell/node-pty-darwin-x64@1.2.0-beta.12': + optional: true + + '@lydell/node-pty-linux-arm64@1.2.0-beta.12': + optional: true + + '@lydell/node-pty-linux-x64@1.2.0-beta.12': + optional: true + + '@lydell/node-pty-win32-arm64@1.2.0-beta.12': + optional: true + + '@lydell/node-pty-win32-x64@1.2.0-beta.12': + optional: true + + '@lydell/node-pty@1.2.0-beta.12': + optionalDependencies: + '@lydell/node-pty-darwin-arm64': 1.2.0-beta.12 + '@lydell/node-pty-darwin-x64': 1.2.0-beta.12 + '@lydell/node-pty-linux-arm64': 1.2.0-beta.12 + '@lydell/node-pty-linux-x64': 1.2.0-beta.12 + '@lydell/node-pty-win32-arm64': 1.2.0-beta.12 + '@lydell/node-pty-win32-x64': 1.2.0-beta.12 + + '@mariozechner/clipboard-darwin-arm64@0.3.2': + optional: true + + '@mariozechner/clipboard-darwin-universal@0.3.2': + optional: true + + '@mariozechner/clipboard-darwin-x64@0.3.2': + optional: true + + '@mariozechner/clipboard-linux-arm64-gnu@0.3.2': + optional: true + + '@mariozechner/clipboard-linux-arm64-musl@0.3.2': + optional: true + + '@mariozechner/clipboard-linux-riscv64-gnu@0.3.2': + optional: true + + '@mariozechner/clipboard-linux-x64-gnu@0.3.2': + optional: true + + '@mariozechner/clipboard-linux-x64-musl@0.3.2': + optional: true + + '@mariozechner/clipboard-win32-arm64-msvc@0.3.2': + optional: true + + '@mariozechner/clipboard-win32-x64-msvc@0.3.2': + optional: true + + '@mariozechner/clipboard@0.3.2': + optionalDependencies: + '@mariozechner/clipboard-darwin-arm64': 0.3.2 + '@mariozechner/clipboard-darwin-universal': 0.3.2 + '@mariozechner/clipboard-darwin-x64': 0.3.2 + '@mariozechner/clipboard-linux-arm64-gnu': 0.3.2 + '@mariozechner/clipboard-linux-arm64-musl': 0.3.2 + '@mariozechner/clipboard-linux-riscv64-gnu': 0.3.2 + '@mariozechner/clipboard-linux-x64-gnu': 0.3.2 + '@mariozechner/clipboard-linux-x64-musl': 0.3.2 + '@mariozechner/clipboard-win32-arm64-msvc': 0.3.2 + '@mariozechner/clipboard-win32-x64-msvc': 0.3.2 + optional: true + + '@mariozechner/jiti@2.6.5': + dependencies: + std-env: 3.10.0 + yoctocolors: 2.1.2 + + '@mariozechner/pi-agent-core@0.66.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)': + dependencies: + '@mariozechner/pi-ai': 0.66.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - aws-crt + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@mariozechner/pi-ai@0.66.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)': + dependencies: + '@anthropic-ai/sdk': 0.73.0(zod@4.3.6) + '@aws-sdk/client-bedrock-runtime': 3.1036.0 + '@google/genai': 1.50.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6)) + '@mistralai/mistralai': 1.14.1 + '@sinclair/typebox': 0.34.49 + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + chalk: 5.6.2 + openai: 6.26.0(ws@8.20.0)(zod@4.3.6) + partial-json: 0.1.7 + proxy-agent: 6.5.0 + undici: 7.25.0 + zod-to-json-schema: 3.25.2(zod@4.3.6) + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - aws-crt + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@mariozechner/pi-coding-agent@0.66.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)': + dependencies: + '@mariozechner/jiti': 2.6.5 + '@mariozechner/pi-agent-core': 0.66.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) + '@mariozechner/pi-ai': 0.66.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) + '@mariozechner/pi-tui': 0.66.1 + '@silvia-odwyer/photon-node': 0.3.4 + ajv: 8.18.0 + chalk: 5.6.2 + cli-highlight: 2.1.11 + diff: 8.0.4 + extract-zip: 2.0.1 + file-type: 21.3.4 + glob: 13.0.6 + hosted-git-info: 9.0.2 + ignore: 7.0.5 + marked: 15.0.12 + minimatch: 10.2.5 + proper-lockfile: 4.1.2 + strip-ansi: 7.2.0 + undici: 7.25.0 + yaml: 2.8.3 + optionalDependencies: + '@mariozechner/clipboard': 0.3.2 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - aws-crt + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@mariozechner/pi-tui@0.66.1': + dependencies: + '@types/mime-types': 2.1.4 + chalk: 5.6.2 + get-east-asian-width: 1.5.0 + marked: 15.0.12 + mime-types: 3.0.2 + optionalDependencies: + koffi: 2.16.1 + + '@matrix-org/matrix-sdk-crypto-nodejs@0.4.0': + dependencies: + https-proxy-agent: 7.0.6 + node-downloader-helper: 2.1.11 + transitivePeerDependencies: + - supports-color + optional: true + + '@matrix-org/matrix-sdk-crypto-wasm@18.0.0': {} + + '@mistralai/mistralai@1.14.1': + dependencies: + ws: 8.20.0 + zod: 4.3.6 + zod-to-json-schema: 3.25.2(zod@4.3.6) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@modelcontextprotocol/sdk@1.29.0(zod@4.3.6)': + dependencies: + '@hono/node-server': 1.19.14(hono@4.12.14) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.8 + express: 5.2.1 + express-rate-limit: 8.4.0(express@5.2.1) + hono: 4.12.14 + jose: 6.2.2 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.3.6 + zod-to-json-schema: 3.25.2(zod@4.3.6) + transitivePeerDependencies: + - supports-color + + '@mozilla/readability@0.6.0': {} + + '@napi-rs/canvas-android-arm64@0.1.99': + optional: true + + '@napi-rs/canvas-darwin-arm64@0.1.99': + optional: true + + '@napi-rs/canvas-darwin-x64@0.1.99': + optional: true + + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.99': + optional: true + + '@napi-rs/canvas-linux-arm64-gnu@0.1.99': + optional: true + + '@napi-rs/canvas-linux-arm64-musl@0.1.99': + optional: true + + '@napi-rs/canvas-linux-riscv64-gnu@0.1.99': + optional: true + + '@napi-rs/canvas-linux-x64-gnu@0.1.99': + optional: true + + '@napi-rs/canvas-linux-x64-musl@0.1.99': + optional: true + + '@napi-rs/canvas-win32-arm64-msvc@0.1.99': + optional: true + + '@napi-rs/canvas-win32-x64-msvc@0.1.99': + optional: true + + '@napi-rs/canvas@0.1.99': + optionalDependencies: + '@napi-rs/canvas-android-arm64': 0.1.99 + '@napi-rs/canvas-darwin-arm64': 0.1.99 + '@napi-rs/canvas-darwin-x64': 0.1.99 + '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.99 + '@napi-rs/canvas-linux-arm64-gnu': 0.1.99 + '@napi-rs/canvas-linux-arm64-musl': 0.1.99 + '@napi-rs/canvas-linux-riscv64-gnu': 0.1.99 + '@napi-rs/canvas-linux-x64-gnu': 0.1.99 + '@napi-rs/canvas-linux-x64-musl': 0.1.99 + '@napi-rs/canvas-win32-arm64-msvc': 0.1.99 + '@napi-rs/canvas-win32-x64-msvc': 0.1.99 + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@noble/ciphers@2.1.1': {} + + '@noble/curves@2.0.1': + dependencies: + '@noble/hashes': 2.0.1 + + '@noble/hashes@2.0.1': {} + + '@nodable/entities@2.1.0': {} + + '@pierre/diffs@1.1.13(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@pierre/theme': 0.0.28 + '@shikijs/transformers': 3.23.0 + diff: 8.0.3 + hast-util-to-html: 9.0.5 + lru_map: 0.4.1 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + shiki: 3.23.0 + + '@pierre/theme@0.0.28': {} + + '@pinojs/redact@0.4.0': {} + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + + '@rollup/rollup-android-arm-eabi@4.60.2': + optional: true + + '@rollup/rollup-android-arm64@4.60.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.2': + optional: true + + '@rollup/rollup-darwin-x64@4.60.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.2': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.2': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.2': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.2': + optional: true + + '@scure/base@2.0.0': {} + + '@scure/bip32@2.0.1': + dependencies: + '@noble/curves': 2.0.1 + '@noble/hashes': 2.0.1 + '@scure/base': 2.0.0 + + '@scure/bip39@2.0.1': + dependencies: + '@noble/hashes': 2.0.1 + '@scure/base': 2.0.0 + + '@shikijs/core@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.6 + + '@shikijs/engine-oniguruma@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + + '@shikijs/themes@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + + '@shikijs/transformers@3.23.0': + dependencies: + '@shikijs/core': 3.23.0 + '@shikijs/types': 3.23.0 + + '@shikijs/types@3.23.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + + '@silvia-odwyer/photon-node@0.3.4': {} + + '@sinclair/typebox@0.33.22': {} + + '@sinclair/typebox@0.34.49': {} + + '@slack/bolt@4.7.1(@types/express@5.0.6)': + dependencies: + '@slack/logger': 4.0.1 + '@slack/oauth': 3.0.5 + '@slack/socket-mode': 2.0.6 + '@slack/types': 2.20.1 + '@slack/web-api': 7.15.1 + '@types/express': 5.0.6 + axios: 1.15.2 + express: 5.2.1 + path-to-regexp: 8.4.2 + raw-body: 3.0.2 + tsscmp: 1.0.6 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + + '@slack/logger@4.0.1': + dependencies: + '@types/node': 25.6.0 + + '@slack/oauth@3.0.5': + dependencies: + '@slack/logger': 4.0.1 + '@slack/web-api': 7.15.1 + '@types/jsonwebtoken': 9.0.10 + '@types/node': 25.6.0 + jsonwebtoken: 9.0.3 + transitivePeerDependencies: + - debug + + '@slack/socket-mode@2.0.6': + dependencies: + '@slack/logger': 4.0.1 + '@slack/web-api': 7.15.1 + '@types/node': 25.6.0 + '@types/ws': 8.18.1 + eventemitter3: 5.0.4 + ws: 8.20.0 + transitivePeerDependencies: + - bufferutil + - debug + - utf-8-validate + + '@slack/types@2.20.1': {} + + '@slack/web-api@7.15.1': + dependencies: + '@slack/logger': 4.0.1 + '@slack/types': 2.20.1 + '@types/node': 25.6.0 + '@types/retry': 0.12.0 + axios: 1.15.2 + eventemitter3: 5.0.4 + form-data: 4.0.5 + is-electron: 2.2.2 + is-stream: 2.0.1 + p-queue: 6.6.2 + p-retry: 4.6.2 + retry: 0.13.1 + transitivePeerDependencies: + - debug + + '@smithy/config-resolver@4.4.17': + dependencies: + '@smithy/node-config-provider': 4.3.14 + '@smithy/types': 4.14.1 + '@smithy/util-config-provider': 4.2.2 + '@smithy/util-endpoints': 3.4.2 + '@smithy/util-middleware': 4.2.14 + tslib: 2.8.1 + + '@smithy/core@3.23.17': + dependencies: + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-stream': 4.5.25 + '@smithy/util-utf8': 4.2.2 + '@smithy/uuid': 1.1.2 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.2.14': + dependencies: + '@smithy/node-config-provider': 4.3.14 + '@smithy/property-provider': 4.2.14 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + tslib: 2.8.1 + + '@smithy/eventstream-codec@4.2.14': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.14.1 + '@smithy/util-hex-encoding': 4.2.2 + tslib: 2.8.1 + + '@smithy/eventstream-serde-browser@4.2.14': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/eventstream-serde-config-resolver@4.3.14': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/eventstream-serde-node@4.2.14': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/eventstream-serde-universal@4.2.14': + dependencies: + '@smithy/eventstream-codec': 4.2.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.3.17': + dependencies: + '@smithy/protocol-http': 5.3.14 + '@smithy/querystring-builder': 4.2.14 + '@smithy/types': 4.14.1 + '@smithy/util-base64': 4.3.2 + tslib: 2.8.1 + + '@smithy/hash-node@4.2.14': + dependencies: + '@smithy/types': 4.14.1 + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/invalid-dependency@4.2.14': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/is-array-buffer@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/middleware-content-length@4.2.14': + dependencies: + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/middleware-endpoint@4.4.32': + dependencies: + '@smithy/core': 3.23.17 + '@smithy/middleware-serde': 4.2.20 + '@smithy/node-config-provider': 4.3.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + '@smithy/util-middleware': 4.2.14 + tslib: 2.8.1 + + '@smithy/middleware-retry@4.5.5': + dependencies: + '@smithy/core': 3.23.17 + '@smithy/node-config-provider': 4.3.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/service-error-classification': 4.3.0 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-retry': 4.3.4 + '@smithy/uuid': 1.1.2 + tslib: 2.8.1 + + '@smithy/middleware-serde@4.2.20': + dependencies: + '@smithy/core': 3.23.17 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/middleware-stack@4.2.14': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/node-config-provider@4.3.14': + dependencies: + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.6.1': + dependencies: + '@smithy/protocol-http': 5.3.14 + '@smithy/querystring-builder': 4.2.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/property-provider@4.2.14': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/protocol-http@5.3.14': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/querystring-builder@4.2.14': + dependencies: + '@smithy/types': 4.14.1 + '@smithy/util-uri-escape': 4.2.2 + tslib: 2.8.1 + + '@smithy/querystring-parser@4.2.14': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/service-error-classification@4.3.0': + dependencies: + '@smithy/types': 4.14.1 + + '@smithy/shared-ini-file-loader@4.4.9': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/signature-v4@5.3.14': + dependencies: + '@smithy/is-array-buffer': 4.2.2 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + '@smithy/util-hex-encoding': 4.2.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-uri-escape': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/smithy-client@4.12.13': + dependencies: + '@smithy/core': 3.23.17 + '@smithy/middleware-endpoint': 4.4.32 + '@smithy/middleware-stack': 4.2.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + '@smithy/util-stream': 4.5.25 + tslib: 2.8.1 + + '@smithy/types@4.14.1': + dependencies: + tslib: 2.8.1 + + '@smithy/url-parser@4.2.14': + dependencies: + '@smithy/querystring-parser': 4.2.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/util-base64@4.3.2': + dependencies: + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-body-length-browser@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-body-length-node@4.2.3': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-buffer-from@4.2.2': + dependencies: + '@smithy/is-array-buffer': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-config-provider@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-defaults-mode-browser@4.3.49': + dependencies: + '@smithy/property-provider': 4.2.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-node@4.2.54': + dependencies: + '@smithy/config-resolver': 4.4.17 + '@smithy/credential-provider-imds': 4.2.14 + '@smithy/node-config-provider': 4.3.14 + '@smithy/property-provider': 4.2.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/util-endpoints@3.4.2': + dependencies: + '@smithy/node-config-provider': 4.3.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/util-hex-encoding@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-middleware@4.2.14': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/util-retry@4.3.4': + dependencies: + '@smithy/service-error-classification': 4.3.0 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/util-stream@4.5.25': + dependencies: + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/node-http-handler': 4.6.1 + '@smithy/types': 4.14.1 + '@smithy/util-base64': 4.3.2 + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-hex-encoding': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-uri-escape@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@4.2.2': + dependencies: + '@smithy/util-buffer-from': 4.2.2 + tslib: 2.8.1 + + '@smithy/uuid@1.1.2': + dependencies: + tslib: 2.8.1 + + '@snazzah/davey-android-arm-eabi@0.1.11': + optional: true + + '@snazzah/davey-android-arm64@0.1.11': + optional: true + + '@snazzah/davey-darwin-arm64@0.1.11': + optional: true + + '@snazzah/davey-darwin-x64@0.1.11': + optional: true + + '@snazzah/davey-freebsd-x64@0.1.11': + optional: true + + '@snazzah/davey-linux-arm-gnueabihf@0.1.11': + optional: true + + '@snazzah/davey-linux-arm64-gnu@0.1.11': + optional: true + + '@snazzah/davey-linux-arm64-musl@0.1.11': + optional: true + + '@snazzah/davey-linux-x64-gnu@0.1.11': + optional: true + + '@snazzah/davey-linux-x64-musl@0.1.11': + optional: true + + '@snazzah/davey-wasm32-wasi@0.1.11(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@snazzah/davey-win32-arm64-msvc@0.1.11': + optional: true + + '@snazzah/davey-win32-ia32-msvc@0.1.11': + optional: true + + '@snazzah/davey-win32-x64-msvc@0.1.11': + optional: true + + '@snazzah/davey@0.1.11(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + optionalDependencies: + '@snazzah/davey-android-arm-eabi': 0.1.11 + '@snazzah/davey-android-arm64': 0.1.11 + '@snazzah/davey-darwin-arm64': 0.1.11 + '@snazzah/davey-darwin-x64': 0.1.11 + '@snazzah/davey-freebsd-x64': 0.1.11 + '@snazzah/davey-linux-arm-gnueabihf': 0.1.11 + '@snazzah/davey-linux-arm64-gnu': 0.1.11 + '@snazzah/davey-linux-arm64-musl': 0.1.11 + '@snazzah/davey-linux-x64-gnu': 0.1.11 + '@snazzah/davey-linux-x64-musl': 0.1.11 + '@snazzah/davey-wasm32-wasi': 0.1.11(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@snazzah/davey-win32-arm64-msvc': 0.1.11 + '@snazzah/davey-win32-ia32-msvc': 0.1.11 + '@snazzah/davey-win32-x64-msvc': 0.1.11 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@swc/helpers@0.5.21': + dependencies: + tslib: 2.8.1 + + '@tokenizer/inflate@0.4.1': + dependencies: + debug: 4.4.3 + token-types: 6.1.2 + transitivePeerDependencies: + - supports-color + + '@tokenizer/token@0.3.0': {} + + '@tootallnate/quickjs-emscripten@0.23.0': {} + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 25.6.0 + + '@types/bun@1.3.11': + dependencies: + bun-types: 1.3.11 + optional: true + + '@types/command-line-args@5.2.3': {} + + '@types/command-line-usage@5.0.4': {} + + '@types/connect@3.4.38': + dependencies: + '@types/node': 25.6.0 + + '@types/estree@1.0.8': {} + + '@types/events@3.0.3': {} + + '@types/express-serve-static-core@5.1.1': + dependencies: + '@types/node': 25.6.0 + '@types/qs': 6.15.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@5.0.6': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.1 + '@types/serve-static': 2.2.0 + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/http-errors@2.0.5': {} + + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 25.6.0 + + '@types/long@4.0.2': {} + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mime-types@2.1.4': {} + + '@types/ms@2.1.0': {} + + '@types/node@10.17.60': {} + + '@types/node@16.9.1': {} + + '@types/node@20.19.39': + dependencies: + undici-types: 6.21.0 + + '@types/node@25.6.0': + dependencies: + undici-types: 7.19.2 + + '@types/qs@6.15.0': {} + + '@types/range-parser@1.2.7': {} + + '@types/retry@0.12.0': {} + + '@types/send@1.2.1': + dependencies: + '@types/node': 25.6.0 + + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 25.6.0 + + '@types/unist@3.0.3': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 25.6.0 + + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 25.6.0 + optional: true + + '@ungap/structured-clone@1.3.0': {} + + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@25.6.0))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@25.6.0) + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.21 + pathe: 1.1.2 + + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + + '@wasm-audio-decoders/common@9.0.7': + dependencies: + '@eshaz/web-worker': 1.2.2 + simple-yenc: 1.0.4 + + '@whiskeysockets/baileys@7.0.0-rc.9(jimp@1.6.1)(sharp@0.34.5)': + dependencies: + '@cacheable/node-cache': 1.7.6 + '@hapi/boom': 9.1.4 + async-mutex: 0.5.0 + libsignal: '@whiskeysockets/libsignal-node@https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67' + lru-cache: 11.3.5 + music-metadata: 11.12.3 + p-queue: 9.1.2 + pino: 9.14.0 + protobufjs: 7.5.5 + sharp: 0.34.5 + ws: 8.20.0 + optionalDependencies: + jimp: 1.6.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@whiskeysockets/libsignal-node@https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67': + dependencies: + curve25519-js: 0.0.4 + protobufjs: 6.8.8 + + abbrev@1.1.1: + optional: true + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + optional: true + + agent-base@7.1.4: {} + + agent-base@9.0.0: {} + + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + another-json@0.2.0: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + any-base@1.1.0: {} + + any-promise@1.3.0: {} + + apache-arrow@18.1.0: + dependencies: + '@swc/helpers': 0.5.21 + '@types/command-line-args': 5.2.3 + '@types/command-line-usage': 5.0.4 + '@types/node': 20.19.39 + command-line-args: 5.2.1 + command-line-usage: 7.0.4 + flatbuffers: 24.12.23 + json-bignum: 0.0.3 + tslib: 2.8.1 + + aproba@2.1.0: + optional: true + + are-we-there-yet@2.0.0: + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + optional: true + + argparse@2.0.1: {} + + array-back@3.1.0: {} + + array-back@6.2.3: {} + + assertion-error@2.0.1: {} + + ast-types@0.13.4: + dependencies: + tslib: 2.8.1 + + async-mutex@0.5.0: + dependencies: + tslib: 2.8.1 + + asynckit@0.4.0: {} + + atomic-sleep@1.0.0: {} + + await-to-js@3.0.0: {} + + axios@1.13.6: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + axios@1.15.2: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.5 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + + balanced-match@1.0.2: + optional: true + + balanced-match@4.0.4: {} + + base-x@5.0.1: {} + + base64-js@1.5.1: {} + + basic-ftp@5.3.0: {} + + bignumber.js@9.3.1: {} + + bmp-ts@1.0.9: {} + + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.1 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + boolbase@1.0.0: {} + + bottleneck@2.19.5: {} + + bowser@2.14.1: {} + + brace-expansion@1.1.14: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + optional: true + + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + + bs58@6.0.0: + dependencies: + base-x: 5.0.1 + + buffer-crc32@0.2.13: {} + + buffer-equal-constant-time@1.0.1: {} + + buffer-from@1.1.2: {} + + bun-types@1.3.11: + dependencies: + '@types/node': 25.6.0 + optional: true + + bytes@3.1.2: {} + + cac@6.7.14: {} + + cacheable@2.3.4: + dependencies: + '@cacheable/memory': 2.0.8 + '@cacheable/utils': 2.4.1 + hookified: 1.15.1 + keyv: 5.6.0 + qified: 0.9.1 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + ccount@2.0.1: {} + + 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 + + chalk-template@0.4.0: + dependencies: + chalk: 4.1.2 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + check-error@2.1.3: {} + + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + chownr@2.0.0: + optional: true + + chownr@3.0.0: {} + + cli-highlight@2.1.11: + dependencies: + chalk: 4.1.2 + highlight.js: 10.7.3 + mz: 2.7.0 + parse5: 5.1.1 + parse5-htmlparser2-tree-adapter: 6.0.1 + yargs: 16.2.0 + + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-support@1.1.3: + optional: true + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + comma-separated-tokens@2.0.3: {} + + command-line-args@5.2.1: + dependencies: + array-back: 3.1.0 + find-replace: 3.0.0 + lodash.camelcase: 4.3.0 + typical: 4.0.0 + + command-line-usage@7.0.4: + dependencies: + array-back: 6.2.3 + chalk-template: 0.4.0 + table-layout: 4.1.1 + typical: 7.3.0 + + commander@14.0.3: {} + + concat-map@0.0.1: + optional: true + + console-control-strings@1.1.0: + optional: true + + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + core-util-is@1.0.3: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + croner@10.0.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-select@5.2.2: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-what@6.2.2: {} + + cssom@0.5.0: {} + + curve25519-js@0.0.4: {} + + data-uri-to-buffer@4.0.1: {} + + data-uri-to-buffer@6.0.2: {} + + data-uri-to-buffer@8.0.0: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + degenerator@5.0.1: + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + + degenerator@7.0.1(quickjs-wasi@2.2.0): + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + quickjs-wasi: 2.2.0 + + delayed-stream@1.0.0: {} + + delegates@1.0.0: + optional: true + + depd@2.0.0: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + diff@8.0.3: {} + + diff@8.0.4: {} + + discord-api-types@0.38.45: {} + + discord-api-types@0.38.47: {} + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dotenv@17.4.2: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + + emoji-regex@8.0.0: {} + + encodeurl@2.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + entities@4.5.0: {} + + entities@7.0.1: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.3 + + 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: {} + + escape-html@1.0.3: {} + + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + + esprima@4.0.1: {} + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + etag@1.8.1: {} + + event-target-shim@5.0.1: {} + + eventemitter3@4.0.7: {} + + eventemitter3@5.0.4: {} + + events@3.3.0: {} + + eventsource-parser@3.0.8: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.8 + + exif-parser@0.1.12: {} + + expect-type@1.3.0: {} + + express-rate-limit@8.4.0(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.1.0 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.1 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + extend@3.0.2: {} + + extract-zip@2.0.1: + dependencies: + debug: 4.4.3 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + + fake-indexeddb@6.2.5: + optional: true + + fast-deep-equal@3.1.3: {} + + fast-string-truncated-width@1.2.1: {} + + fast-string-width@1.1.0: + dependencies: + fast-string-truncated-width: 1.2.1 + + fast-uri@3.1.0: {} + + fast-wrap-ansi@0.1.6: + dependencies: + fast-string-width: 1.1.0 + + fast-xml-builder@1.1.5: + dependencies: + path-expression-matcher: 1.5.0 + + fast-xml-parser@5.7.1: + dependencies: + '@nodable/entities': 2.1.0 + fast-xml-builder: 1.1.5 + path-expression-matcher: 1.5.0 + strnum: 2.2.3 + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + file-type@21.3.4: + dependencies: + '@tokenizer/inflate': 0.4.1 + strtok3: 10.3.5 + token-types: 6.1.2 + uint8array-extras: 1.5.0 + transitivePeerDependencies: + - supports-color + + file-type@22.0.1: + dependencies: + '@tokenizer/inflate': 0.4.1 + strtok3: 10.3.5 + token-types: 6.1.2 + uint8array-extras: 1.5.0 + transitivePeerDependencies: + - supports-color + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + find-replace@3.0.0: + dependencies: + array-back: 3.1.0 + + flatbuffers@24.12.23: {} + + follow-redirects@1.16.0: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.3 + mime-types: 2.1.35 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + optional: true + + fs.realpath@1.0.0: + optional: true + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gauge@3.0.2: + dependencies: + aproba: 2.1.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + object-assign: 4.1.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + optional: true + + gaxios@6.7.1: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + is-stream: 2.0.1 + node-fetch: 2.7.0 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + gaxios@7.1.4: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + transitivePeerDependencies: + - supports-color + + gcp-metadata@6.1.1: + dependencies: + gaxios: 6.7.1 + google-logging-utils: 0.0.2 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + + gcp-metadata@8.1.2: + dependencies: + gaxios: 7.1.4 + google-logging-utils: 1.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.5.0: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.3 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@5.2.0: + dependencies: + pump: 3.0.4 + + get-uri@6.0.5: + dependencies: + basic-ftp: 5.3.0 + data-uri-to-buffer: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + get-uri@8.0.0: + dependencies: + basic-ftp: 5.3.0 + data-uri-to-buffer: 8.0.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + gifwrap@0.10.1: + dependencies: + image-q: 4.0.0 + omggif: 1.0.10 + + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + optional: true + + google-auth-library@10.6.2: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.1.4 + gcp-metadata: 8.1.2 + google-logging-utils: 1.1.3 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + + google-auth-library@9.15.1: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 6.7.1 + gcp-metadata: 6.1.1 + gtoken: 7.1.0 + jws: 4.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + google-logging-utils@0.0.2: {} + + google-logging-utils@1.1.3: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + grammy@1.42.0: + dependencies: + '@grammyjs/types': 3.26.0 + abort-controller: 3.0.0 + debug: 4.4.3 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + - supports-color + + gtoken@7.1.0: + dependencies: + gaxios: 6.7.1 + jws: 4.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + has-unicode@2.0.1: + optional: true + + hashery@1.5.1: + dependencies: + hookified: 1.15.1 + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + highlight.js@10.7.3: {} + + hono@4.12.14: {} + + hookified@1.15.1: {} + + hookified@2.1.1: {} + + hosted-git-info@9.0.2: + dependencies: + lru-cache: 11.3.5 + + html-escaper@3.0.3: {} + + html-void-elements@3.0.0: {} + + htmlparser2@10.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 7.0.1 + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + http-proxy-agent@9.0.0: + dependencies: + agent-base: 9.0.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + optional: true + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@9.0.0: + dependencies: + agent-base: 9.0.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore@7.0.5: {} + + image-q@4.0.0: + dependencies: + '@types/node': 16.9.1 + + immediate@3.0.6: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + optional: true + + inherits@2.0.4: {} + + ip-address@10.1.0: {} + + ipaddr.js@1.9.1: {} + + ipaddr.js@2.3.0: {} + + is-electron@2.2.2: {} + + is-fullwidth-code-point@3.0.0: {} + + is-network-error@1.3.1: {} + + is-promise@4.0.0: {} + + is-stream@2.0.1: {} + + isarray@1.0.0: {} + + isexe@2.0.0: {} + + jimp@1.6.1: + dependencies: + '@jimp/core': 1.6.1 + '@jimp/diff': 1.6.1 + '@jimp/js-bmp': 1.6.1 + '@jimp/js-gif': 1.6.1 + '@jimp/js-jpeg': 1.6.1 + '@jimp/js-png': 1.6.1 + '@jimp/js-tiff': 1.6.1 + '@jimp/plugin-blit': 1.6.1 + '@jimp/plugin-blur': 1.6.1 + '@jimp/plugin-circle': 1.6.1 + '@jimp/plugin-color': 1.6.1 + '@jimp/plugin-contain': 1.6.1 + '@jimp/plugin-cover': 1.6.1 + '@jimp/plugin-crop': 1.6.1 + '@jimp/plugin-displace': 1.6.1 + '@jimp/plugin-dither': 1.6.1 + '@jimp/plugin-fisheye': 1.6.1 + '@jimp/plugin-flip': 1.6.1 + '@jimp/plugin-hash': 1.6.1 + '@jimp/plugin-mask': 1.6.1 + '@jimp/plugin-print': 1.6.1 + '@jimp/plugin-quantize': 1.6.1 + '@jimp/plugin-resize': 1.6.1 + '@jimp/plugin-rotate': 1.6.1 + '@jimp/plugin-threshold': 1.6.1 + '@jimp/types': 1.6.1 + '@jimp/utils': 1.6.1 + transitivePeerDependencies: + - supports-color + + jiti@2.6.1: {} + + jose@6.2.2: {} + + jpeg-js@0.4.4: {} + + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + + json-bignum@0.0.3: {} + + json-schema-to-ts@3.1.1: + dependencies: + '@babel/runtime': 7.29.2 + ts-algebra: 2.0.0 + + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + + json5@2.2.3: {} + + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.4 + + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + jwt-decode@4.0.0: {} + + keyv@5.6.0: + dependencies: + '@keyv/serialize': 1.1.1 + + koffi@2.16.1: + optional: true + + lie@3.3.0: + dependencies: + immediate: 3.0.6 + + linkedom@0.18.12: + dependencies: + css-select: 5.2.2 + cssom: 0.5.0 + html-escaper: 3.0.3 + htmlparser2: 10.1.0 + uhyphen: 0.2.0 + + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + + lodash.camelcase@4.3.0: {} + + lodash.identity@3.0.0: {} + + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.merge@4.6.2: {} + + lodash.once@4.1.1: {} + + lodash.pickby@4.6.0: {} + + loglevel@1.9.2: {} + + long@4.0.0: {} + + long@5.3.2: {} + + loupe@3.2.1: {} + + lru-cache@11.3.5: {} + + lru-cache@7.18.3: {} + + lru_map@0.4.1: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + optional: true + + markdown-it@14.1.1: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + + marked@15.0.12: {} + + math-intrinsics@1.1.0: {} + + matrix-events-sdk@0.0.1: {} + + matrix-js-sdk@41.3.0: + dependencies: + '@babel/runtime': 7.29.2 + '@matrix-org/matrix-sdk-crypto-wasm': 18.0.0 + another-json: 0.2.0 + bs58: 6.0.0 + content-type: 1.0.5 + jwt-decode: 4.0.0 + loglevel: 1.9.2 + matrix-events-sdk: 0.0.1 + matrix-widget-api: 1.17.0 + oidc-client-ts: 3.5.0 + p-retry: 7.1.1 + sdp-transform: 3.0.0 + unhomoglyph: 1.0.6 + uuid: 13.0.0 + + matrix-widget-api@1.17.0: + dependencies: + '@types/events': 3.0.3 + events: 3.3.0 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdurl@2.0.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-encode@2.0.1: {} + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mime@3.0.0: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.5 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.14 + optional: true + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + optional: true + + minipass@5.0.0: + optional: true + + minipass@7.1.3: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + optional: true + + minizlib@3.1.0: + dependencies: + minipass: 7.1.3 + + mkdirp@1.0.4: + optional: true + + mpg123-decoder@1.0.3: + dependencies: + '@wasm-audio-decoders/common': 9.0.7 + + ms@2.1.3: {} + + music-metadata@11.12.3: + dependencies: + '@borewit/text-codec': 0.2.2 + '@tokenizer/token': 0.3.0 + content-type: 1.0.5 + debug: 4.4.3 + file-type: 21.3.4 + media-typer: 1.1.0 + strtok3: 10.3.5 + token-types: 6.1.2 + uint8array-extras: 1.5.0 + win-guid: 0.2.1 + transitivePeerDependencies: + - supports-color + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + negotiator@1.0.0: {} + + netmask@2.1.1: {} + + node-addon-api@8.7.0: + optional: true + + node-domexception@1.0.0: {} + + node-downloader-helper@2.1.11: + optional: true + + node-edge-tts@1.2.10: + dependencies: + https-proxy-agent: 7.0.6 + ws: 8.20.0 + yargs: 17.7.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + node-readable-to-web-readable-stream@0.4.2: + optional: true + + nopt@5.0.0: + dependencies: + abbrev: 1.1.1 + optional: true + + nostr-tools@2.23.3(typescript@5.9.3): + dependencies: + '@noble/ciphers': 2.1.1 + '@noble/curves': 2.0.1 + '@noble/hashes': 2.0.1 + '@scure/base': 2.0.0 + '@scure/bip32': 2.0.1 + '@scure/bip39': 2.0.1 + nostr-wasm: 0.1.0 + optionalDependencies: + typescript: 5.9.3 + + nostr-wasm@0.1.0: {} + + npmlog@5.0.1: + dependencies: + are-we-there-yet: 2.0.0 + console-control-strings: 1.1.0 + gauge: 3.0.2 + set-blocking: 2.0.0 + optional: true + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + oidc-client-ts@3.5.0: + dependencies: + jwt-decode: 4.0.0 + + omggif@1.0.10: {} + + on-exit-leak-free@2.1.2: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + oniguruma-parser@0.12.2: {} + + oniguruma-to-es@4.3.6: + dependencies: + oniguruma-parser: 0.12.2 + regex: 6.1.0 + regex-recursion: 6.0.2 + + openai@6.26.0(ws@8.20.0)(zod@4.3.6): + optionalDependencies: + ws: 8.20.0 + zod: 4.3.6 + + openai@6.34.0(ws@8.20.0)(zod@4.3.6): + optionalDependencies: + ws: 8.20.0 + zod: 4.3.6 + + openclaw@2026.4.15-beta.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@napi-rs/canvas@0.1.99)(@types/express@5.0.6)(apache-arrow@18.1.0)(hono@4.12.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3): + dependencies: + '@agentclientprotocol/sdk': 0.18.2(zod@4.3.6) + '@anthropic-ai/vertex-sdk': 0.15.0(zod@4.3.6) + '@aws-sdk/client-bedrock': 3.1028.0 + '@aws-sdk/client-bedrock-runtime': 3.1028.0 + '@aws-sdk/credential-provider-node': 3.972.30 + '@aws/bedrock-token-generator': 1.1.0 + '@buape/carbon': 0.15.0(@discordjs/opus@0.10.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(hono@4.12.14)(opusscript@0.1.1) + '@clack/prompts': 1.2.0 + '@google/genai': 1.50.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6)) + '@grammyjs/runner': 2.0.3(grammy@1.42.0) + '@grammyjs/transformer-throttler': 1.2.1(grammy@1.42.0) + '@homebridge/ciao': 1.3.6 + '@lancedb/lancedb': 0.27.2(apache-arrow@18.1.0) + '@larksuiteoapi/node-sdk': 1.61.1 + '@lydell/node-pty': 1.2.0-beta.12 + '@mariozechner/pi-agent-core': 0.66.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) + '@mariozechner/pi-ai': 0.66.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) + '@mariozechner/pi-coding-agent': 0.66.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) + '@mariozechner/pi-tui': 0.66.1 + '@matrix-org/matrix-sdk-crypto-wasm': 18.0.0 + '@modelcontextprotocol/sdk': 1.29.0(zod@4.3.6) + '@mozilla/readability': 0.6.0 + '@napi-rs/canvas': 0.1.99 + '@pierre/diffs': 1.1.13(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@sinclair/typebox': 0.34.49 + '@slack/bolt': 4.7.1(@types/express@5.0.6) + '@slack/web-api': 7.15.1 + '@whiskeysockets/baileys': 7.0.0-rc.9(jimp@1.6.1)(sharp@0.34.5) + ajv: 8.18.0 + chalk: 5.6.2 + chokidar: 5.0.0 + cli-highlight: 2.1.11 + commander: 14.0.3 + croner: 10.0.1 + discord-api-types: 0.38.47 + dotenv: 17.4.2 + express: 5.2.1 + file-type: 22.0.1 + gaxios: 7.1.4 + google-auth-library: 10.6.2 + grammy: 1.42.0 + https-proxy-agent: 9.0.0 + ipaddr.js: 2.3.0 + jimp: 1.6.1 + jiti: 2.6.1 + json5: 2.2.3 + jszip: 3.10.1 + linkedom: 0.18.12 + long: 5.3.2 + markdown-it: 14.1.1 + matrix-js-sdk: 41.3.0 + mpg123-decoder: 1.0.3 + node-edge-tts: 1.2.10 + nostr-tools: 2.23.3(typescript@5.9.3) + openai: 6.34.0(ws@8.20.0)(zod@4.3.6) + opusscript: 0.1.1 + osc-progress: 0.3.0 + pdfjs-dist: 5.6.205 + playwright-core: 1.59.1 + proxy-agent: 8.0.1 + qrcode-terminal: 0.12.0 + sharp: 0.34.5 + silk-wasm: 3.7.1 + sqlite-vec: 0.1.9 + tar: 7.5.13 + tslog: 4.10.2 + undici: 8.0.2 + uuid: 13.0.0 + ws: 8.20.0 + yaml: 2.8.3 + zod: 4.3.6 + optionalDependencies: + '@discordjs/opus': 0.10.0 + '@matrix-org/matrix-sdk-crypto-nodejs': 0.4.0 + fake-indexeddb: 6.2.5 + music-metadata: 11.12.3 + transitivePeerDependencies: + - '@cfworker/json-schema' + - '@emnapi/core' + - '@emnapi/runtime' + - '@types/express' + - apache-arrow + - audio-decode + - aws-crt + - bufferutil + - canvas + - debug + - encoding + - ffmpeg-static + - hono + - link-preview-js + - node-opus + - react + - react-dom + - supports-color + - typescript + - utf-8-validate + + opusscript@0.1.1: {} + + osc-progress@0.3.0: {} + + p-finally@1.0.0: {} + + p-queue@6.6.2: + dependencies: + eventemitter3: 4.0.7 + p-timeout: 3.2.0 + + p-queue@9.1.2: + dependencies: + eventemitter3: 5.0.4 + p-timeout: 7.0.1 + + p-retry@4.6.2: + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + + p-retry@7.1.1: + dependencies: + is-network-error: 1.3.1 + + p-timeout@3.2.0: + dependencies: + p-finally: 1.0.0 + + p-timeout@7.0.1: {} + + pac-proxy-agent@7.2.0: + dependencies: + '@tootallnate/quickjs-emscripten': 0.23.0 + agent-base: 7.1.4 + debug: 4.4.3 + get-uri: 6.0.5 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + pac-resolver: 7.0.1 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + pac-proxy-agent@9.0.1: + dependencies: + agent-base: 9.0.0 + debug: 4.4.3 + get-uri: 8.0.0 + http-proxy-agent: 9.0.0 + https-proxy-agent: 9.0.0 + pac-resolver: 9.0.1(quickjs-wasi@2.2.0) + quickjs-wasi: 2.2.0 + socks-proxy-agent: 10.0.0 + transitivePeerDependencies: + - supports-color + + pac-resolver@7.0.1: + dependencies: + degenerator: 5.0.1 + netmask: 2.1.1 + + pac-resolver@9.0.1(quickjs-wasi@2.2.0): + dependencies: + degenerator: 7.0.1(quickjs-wasi@2.2.0) + netmask: 2.1.1 + quickjs-wasi: 2.2.0 + + pako@1.0.11: {} + + parse-bmfont-ascii@1.0.6: {} + + parse-bmfont-binary@1.0.6: {} + + parse-bmfont-xml@1.1.6: + dependencies: + xml-parse-from-string: 1.0.1 + xml2js: 0.5.0 + + parse5-htmlparser2-tree-adapter@6.0.1: + dependencies: + parse5: 6.0.1 + + parse5@5.1.1: {} + + parse5@6.0.1: {} + + parseurl@1.3.3: {} + + partial-json@0.1.7: {} + + path-expression-matcher@1.5.0: {} + + path-is-absolute@1.0.1: + optional: true + + path-key@3.1.1: {} + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.3.5 + minipass: 7.1.3 + + path-to-regexp@8.4.2: {} + + pathe@1.1.2: {} + + pathval@2.0.1: {} + + pdfjs-dist@5.6.205: + optionalDependencies: + '@napi-rs/canvas': 0.1.99 + node-readable-to-web-readable-stream: 0.4.2 + + pend@1.2.0: {} + + picocolors@1.1.1: {} + + pino-abstract-transport@2.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.1.0: {} + + pino@9.14.0: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 3.1.0 + + pixelmatch@5.3.0: + dependencies: + pngjs: 6.0.0 + + pkce-challenge@5.0.1: {} + + playwright-core@1.59.1: {} + + pngjs@6.0.0: {} + + pngjs@7.0.0: {} + + postcss@8.5.10: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prism-media@1.3.5(@discordjs/opus@0.10.0)(opusscript@0.1.1): + optionalDependencies: + '@discordjs/opus': 0.10.0 + opusscript: 0.1.1 + optional: true + + process-nextick-args@2.0.1: {} + + process-warning@5.0.0: {} + + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + + property-information@7.1.0: {} + + protobufjs@6.8.8: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/long': 4.0.2 + '@types/node': 10.17.60 + long: 4.0.0 + + protobufjs@7.5.5: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 25.6.0 + long: 5.3.2 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-agent@6.5.0: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 7.18.3 + pac-proxy-agent: 7.2.0 + proxy-from-env: 1.1.0 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + proxy-agent@8.0.1: + dependencies: + agent-base: 9.0.0 + debug: 4.4.3 + http-proxy-agent: 9.0.0 + https-proxy-agent: 9.0.0 + lru-cache: 7.18.3 + pac-proxy-agent: 9.0.1 + proxy-from-env: 2.1.0 + socks-proxy-agent: 10.0.0 + transitivePeerDependencies: + - supports-color + + proxy-from-env@1.1.0: {} + + proxy-from-env@2.1.0: {} + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + punycode.js@2.3.1: {} + + qified@0.9.1: + dependencies: + hookified: 2.1.1 + + qrcode-terminal@0.12.0: {} + + qs@6.15.1: + dependencies: + side-channel: 1.1.0 + + quick-format-unescaped@4.0.4: {} + + quickjs-wasi@2.2.0: {} + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + react-dom@19.2.5(react@19.2.5): + dependencies: + react: 19.2.5 + scheduler: 0.27.0 + + react@19.2.5: {} + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + optional: true + + readdirp@5.0.0: {} + + real-require@0.2.0: {} + + reflect-metadata@0.2.2: {} + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + retry@0.12.0: {} + + retry@0.13.1: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + optional: true + + rollup@4.60.2: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.2 + '@rollup/rollup-android-arm64': 4.60.2 + '@rollup/rollup-darwin-arm64': 4.60.2 + '@rollup/rollup-darwin-x64': 4.60.2 + '@rollup/rollup-freebsd-arm64': 4.60.2 + '@rollup/rollup-freebsd-x64': 4.60.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.2 + '@rollup/rollup-linux-arm-musleabihf': 4.60.2 + '@rollup/rollup-linux-arm64-gnu': 4.60.2 + '@rollup/rollup-linux-arm64-musl': 4.60.2 + '@rollup/rollup-linux-loong64-gnu': 4.60.2 + '@rollup/rollup-linux-loong64-musl': 4.60.2 + '@rollup/rollup-linux-ppc64-gnu': 4.60.2 + '@rollup/rollup-linux-ppc64-musl': 4.60.2 + '@rollup/rollup-linux-riscv64-gnu': 4.60.2 + '@rollup/rollup-linux-riscv64-musl': 4.60.2 + '@rollup/rollup-linux-s390x-gnu': 4.60.2 + '@rollup/rollup-linux-x64-gnu': 4.60.2 + '@rollup/rollup-linux-x64-musl': 4.60.2 + '@rollup/rollup-openbsd-x64': 4.60.2 + '@rollup/rollup-openharmony-arm64': 4.60.2 + '@rollup/rollup-win32-arm64-msvc': 4.60.2 + '@rollup/rollup-win32-ia32-msvc': 4.60.2 + '@rollup/rollup-win32-x64-gnu': 4.60.2 + '@rollup/rollup-win32-x64-msvc': 4.60.2 + fsevents: 2.3.3 + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + sax@1.6.0: {} + + scheduler@0.27.0: {} + + sdp-transform@3.0.0: {} + + semver@6.3.1: + optional: true + + semver@7.7.4: {} + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + set-blocking@2.0.0: + optional: true + + setimmediate@1.0.5: {} + + setprototypeof@1.2.0: {} + + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.7.4 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shiki@3.23.0: + dependencies: + '@shikijs/core': 3.23.0 + '@shikijs/engine-javascript': 3.23.0 + '@shikijs/engine-oniguruma': 3.23.0 + '@shikijs/langs': 3.23.0 + '@shikijs/themes': 3.23.0 + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + silk-wasm@3.7.1: {} + + simple-xml-to-json@1.2.7: {} + + simple-yenc@1.0.4: {} + + sisteransi@1.0.5: {} + + smart-buffer@4.2.0: {} + + socks-proxy-agent@10.0.0: + dependencies: + agent-base: 9.0.0 + debug: 4.4.3 + socks: 2.8.7 + transitivePeerDependencies: + - supports-color + + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + socks: 2.8.7 + transitivePeerDependencies: + - supports-color + + socks@2.8.7: + dependencies: + ip-address: 10.1.0 + smart-buffer: 4.2.0 + + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + space-separated-tokens@2.0.2: {} + + split2@4.2.0: {} + + sqlite-vec-darwin-arm64@0.1.9: + optional: true + + sqlite-vec-darwin-x64@0.1.9: + optional: true + + sqlite-vec-linux-arm64@0.1.9: + optional: true + + sqlite-vec-linux-x64@0.1.9: + optional: true + + sqlite-vec-windows-x64@0.1.9: + optional: true + + sqlite-vec@0.1.9: + optionalDependencies: + sqlite-vec-darwin-arm64: 0.1.9 + sqlite-vec-darwin-x64: 0.1.9 + sqlite-vec-linux-arm64: 0.1.9 + sqlite-vec-linux-x64: 0.1.9 + sqlite-vec-windows-x64: 0.1.9 + + stackback@0.0.2: {} + + statuses@2.0.2: {} + + std-env@3.10.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strnum@2.2.3: {} + + strtok3@10.3.5: + dependencies: + '@tokenizer/token': 0.3.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + table-layout@4.1.1: + dependencies: + array-back: 6.2.3 + wordwrapjs: 5.1.1 + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + optional: true + + tar@7.5.13: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.3 + minizlib: 3.1.0 + yallist: 5.0.0 + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + thread-stream@3.1.0: + dependencies: + real-require: 0.2.0 + + tinybench@2.9.0: {} + + tinycolor2@1.6.0: {} + + tinyexec@0.3.2: {} + + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + + toidentifier@1.0.1: {} + + token-types@6.1.2: + dependencies: + '@borewit/text-codec': 0.2.2 + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + + tr46@0.0.3: {} + + trim-lines@3.0.1: {} + + ts-algebra@2.0.0: {} + + tslib@2.8.1: {} + + tslog@4.10.2: {} + + tsscmp@1.0.6: {} + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typescript@5.9.3: {} + + typical@4.0.0: {} + + typical@7.3.0: {} + + uc.micro@2.1.0: {} + + uhyphen@0.2.0: {} + + uint8array-extras@1.5.0: {} + + undici-types@6.21.0: {} + + undici-types@7.19.2: {} + + undici@7.25.0: {} + + undici@8.0.2: {} + + unhomoglyph@1.0.6: {} + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + unpipe@1.0.0: {} + + utif2@4.1.0: + dependencies: + pako: 1.0.11 + + util-deprecate@1.0.2: {} + + uuid@13.0.0: {} + + uuid@9.0.1: {} + + vary@1.1.2: {} + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + vite-node@2.1.9(@types/node@25.6.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.21(@types/node@25.6.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.21(@types/node@25.6.0): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.10 + rollup: 4.60.2 + optionalDependencies: + '@types/node': 25.6.0 + fsevents: 2.3.3 + + vitest@2.1.9(@types/node@25.6.0): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@25.6.0)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.21(@types/node@25.6.0) + vite-node: 2.1.9(@types/node@25.6.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.6.0 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + web-streams-polyfill@3.3.3: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wide-align@1.1.5: + dependencies: + string-width: 4.2.3 + optional: true + + win-guid@0.2.1: {} + + wordwrapjs@5.1.1: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + ws@8.20.0: {} + + xml-parse-from-string@1.0.1: {} + + xml2js@0.5.0: + dependencies: + sax: 1.6.0 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} + + y18n@5.0.8: {} + + yallist@4.0.0: + optional: true + + yallist@5.0.0: {} + + yaml@2.8.3: {} + + yargs-parser@20.2.9: {} + + yargs-parser@21.1.1: {} + + yargs@16.2.0: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + + yoctocolors@2.1.2: {} + + zod-to-json-schema@3.25.2(zod@4.3.6): + dependencies: + zod: 4.3.6 + + zod@3.25.76: {} + + zod@4.3.6: {} + + zwitch@2.0.4: {} diff --git a/schema/src/display.ts b/schema/src/display.ts index fa0b063..9302ebd 100644 --- a/schema/src/display.ts +++ b/schema/src/display.ts @@ -24,7 +24,11 @@ export const DISPLAY_CAPS = [ export const DISPLAY_MODE_HEADSHOT = "headshot" as const; export const DISPLAY_MODE_FULLBODY = "fullbody" as const; -const LoopModeSchema = Type.String({ enum: ["infinite", "once", "ping-pong"] }); +const LoopModeSchema = Type.Union([ + Type.Literal("infinite"), + Type.Literal("once"), + Type.Literal("ping-pong"), +]); // A single source rectangle inside an atlas image. For non-atlas frame sources // only `ref` is set (points at a whole-image asset) and rect coords are omitted. @@ -69,7 +73,7 @@ const TransitionRefSchema = Type.Union([ NonEmptyString, Type.Object( { - blend: Type.String({ enum: ["crossfade"] }), + blend: Type.Literal("crossfade"), ms: Type.Integer({ minimum: 1, maximum: 10_000 }), }, { additionalProperties: false }, @@ -208,7 +212,7 @@ export type ModeContent = Static; export type AssetBundle = Static; export type EmotionEntry = Static; export type EmotionDirective = Static; -export type LoopMode = "infinite" | "once" | "ping-pong"; +export type LoopMode = Static; // Wire version literal for the CharacterManifest envelope. Bump + fanout to // every client when making a breaking change to the shape. diff --git a/scripts/check-versions.mjs b/scripts/check-versions.mjs new file mode 100755 index 0000000..cfe581f --- /dev/null +++ b/scripts/check-versions.mjs @@ -0,0 +1,78 @@ +#!/usr/bin/env node +// Asserts every publishable artifact in this repo carries the same version +// string. Used by the release workflow as a pre-publish gate: if the git tag +// is `v1.2.3`, the plugin's package.json, the client-js package.json, and +// the Kotlin Gradle version must all read `1.2.3` before publishing starts. +// +// Usage: +// node scripts/check-versions.mjs +// +// When is omitted, it's read from process.env.TAG_VERSION +// (release workflow sets this). When neither is provided, the script still +// succeeds only if all packages already agree with the plugin's version as +// the reference — useful for a pre-commit sanity check. +// +// Exits 0 on agreement, 1 on mismatch with a specific diagnostic. + +import { readFileSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); + +function readJson(relPath) { + const p = join(repoRoot, relPath); + try { + return JSON.parse(readFileSync(p, "utf8")); + } catch (err) { + fail(`failed to read ${relPath}: ${err.message}`); + } +} + +function readGradleVersion(relPath) { + const text = readFileSync(join(repoRoot, relPath), "utf8"); + // Match both `version = "1.2.3"` and `version = findProperty("version")?.toString() ?: "1.2.3"`. + const match = text.match(/version\s*=.*?["']([^"']+)["']\s*$/m); + if (!match) { + fail(`could not locate version literal in ${relPath}`); + } + return match[1]; +} + +function fail(msg) { + console.error(`version-check: ${msg}`); + process.exit(1); +} + +const plugin = readJson("packages/plugin/package.json"); +const clientJs = readJson("packages/client-js/package.json"); +const schema = readJson("schema/package.json"); +const kotlinCore = readGradleVersion("packages/client-kotlin/core/build.gradle.kts"); +const kotlinAndroid = readGradleVersion("packages/client-kotlin/android/build.gradle.kts"); +// SwiftPM has no declared version — the git tag IS the Swift version. Skip. + +const expected = process.argv[2] ?? process.env.TAG_VERSION ?? plugin.version; + +const entries = [ + ["plugin", plugin.version], + ["client-js", clientJs.version], + ["schema", schema.version], + ["client-kotlin:core (Gradle fallback)", kotlinCore], + ["client-kotlin:android (Gradle fallback)", kotlinAndroid], +]; + +let mismatch = false; +console.log(`version-check: expected=${expected}`); +for (const [name, actual] of entries) { + const ok = actual === expected; + console.log(` ${ok ? "✓" : "✗"} ${name}: ${actual}`); + if (!ok) mismatch = true; +} + +if (mismatch) { + console.error( + "version-check: FAIL — bump every package to the same version before tagging.", + ); + process.exit(1); +} +console.log("version-check: OK"); From b7d7f3c4a07a2c5421ea22d1edd901047c50119b Mon Sep 17 00:00:00 2001 From: Tyler-RNG Date: Thu, 23 Apr 2026 21:13:09 -0400 Subject: [PATCH 06/10] Refine pixellab avatar pipeline: --apply, canonical state names, voice-in-default-flow - pixellab-animate.mjs: motion-oriented default emotion prompts so pixellab's v3 generator lands on-target ("big open-mouth smile, eyes bright and crinkled in joy, slight excited bounce" instead of just "happy warm smile"). - pixellab-export.mjs: DEFAULT_CANONICAL_RENAMES collapses pixellab's verbose folder slugs back to canonical emotion keys (idle, happy, sad, ...) when /characters//animations 404s; DEFAULT_CANONICAL_DESCRIPTIONS replaces pixellab's 50-char-truncated slug descriptions with full prompt text. - pixellab-export.mjs: new --apply flag patches openclaw.json directly under plugins.entries["sprite-core"].config.agents., with a timestamped backup and restart-reminder. Closes the long-standing "operator copy-pastes the snippet" ergonomic gap. - SKILL.md: voice selection promoted to Step 1 (ask the user) + new Step 5a (discover via --list-voices) so agents running the skill always pair a voice with the atlas. - scripts/sync-to-openclaw.sh: new helper; mirrors plugin scripts, template, and skills into openclaw-src/extensions/sprite-core with SKILL.md path rewrites (plugin-rooted to monorepo-rooted) so gateway-side execution resolves correctly. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../skills/openclaw-pixellab-avatar/SKILL.md | 87 ++++++++--- packages/plugin/scripts/pixellab-animate.mjs | 30 ++-- packages/plugin/scripts/pixellab-export.mjs | 144 +++++++++++++++++- scripts/sync-to-openclaw.sh | 54 +++++++ 4 files changed, 276 insertions(+), 39 deletions(-) create mode 100755 scripts/sync-to-openclaw.sh diff --git a/.agents/skills/openclaw-pixellab-avatar/SKILL.md b/.agents/skills/openclaw-pixellab-avatar/SKILL.md index 132a71e..9e8baa0 100644 --- a/.agents/skills/openclaw-pixellab-avatar/SKILL.md +++ b/.agents/skills/openclaw-pixellab-avatar/SKILL.md @@ -35,6 +35,11 @@ Ask the user: At minimum include `idle` and `thinking` (the watch auto-plays `thinking` while waiting for replies). - **Agent id** this will be wired into (default: `agent`). +- **Voice** — which ElevenLabs voice the agent should speak with. Offer + to run `--list-voices` (see Step 5b) so the user can pick; if they have + no preference, suggest `--voice auto` for a smoke-test voice they can + change later. A voice id drops an `elevenlabs` voice block into the + wired config so TTS just works the moment the gateway restarts. ### 2. Create the character (step 1 of 4) @@ -94,16 +99,44 @@ Expect ~5–10 min per emotion. The script prints `✓ complete` as each job finishes. If any emotion fails, the script exits non-zero with a summary — rerun with just the failed emotions to fill in the gaps. -### 5. Export into SpriteCore (step 4 of 4) +### 5a. (Optional) Discover ElevenLabs voices + +Before exporting, if the user wants to pin a specific voice, list what's +in their ElevenLabs library: + +```bash +node scripts/pixellab-export.mjs --list-voices +``` + +No `--uid` required — this is a read-only lookup. Each line prints +` [category] `. Copy the voice id the user +picks and pass it to the export step via `--voice-id`. + +Auth: `ELEVENLABS_API_KEY` env, `--elevenlabs-api-key-command `, +or `pass show elevenlabs/api-key`. + +### 5b. Export into SpriteCore (step 4 of 4) Once animations exist on the character, the exporter pulls the ZIP bundle, calls `/characters//animations` for canonical emotion names (`happy`, `sad`, etc. — not the verbose pixellab slugs), packs frames into a WebP -atlas, writes the manifest, and prints the config snippet ready to paste -into `openclaw.json`. +atlas, writes the manifest, generates the config block (including the +voice if a voice id was supplied), and with `--apply` patches +`openclaw.json` directly. ```bash -# Writes atlas + manifest to ~/.openclaw/assets/avatars// +# Normal path: write atlas + manifest AND patch openclaw.json in one shot. +# The exporter backs up the config before writing; restart the gateway +# afterward so it picks up the new agent entry. +node scripts/pixellab-export.mjs \ + --uid \ + --agent-id \ + --overwrite \ + --apply + +# Dry-style path: write atlas + manifest only; print the openclaw.json +# snippet for manual review/paste. Use this when you want to eyeball the +# block before wiring it in. node scripts/pixellab-export.mjs \ --uid \ --agent-id \ @@ -180,11 +213,16 @@ exporter, so any existing call site keeps working. ### 6. Wire into `openclaw.json` -Copy the config snippet the exporter prints into `openclaw.json` under -`plugins.entries["sprite-core"].config.agents.`, then restart the -gateway. Default state from the snippet is `idle` when present; otherwise -the first animation. Review before saving — sometimes you want a more -specific default. +If you ran the exporter with `--apply`, this step is already done — the +exporter wrote the agent block under +`plugins.entries["sprite-core"].config.agents.` and printed the +backup path. Skip to the restart in Step 7. + +If you ran without `--apply`, copy the config snippet the exporter printed +into `openclaw.json` under +`plugins.entries["sprite-core"].config.agents.`. Default state +from the snippet is `idle` when present; otherwise the first animation. +Review before saving — sometimes you want a more specific default. ### 7. Verify @@ -208,15 +246,22 @@ specific default. ## Open TODOs -- `--apply` flag for the exporter: patch the snippet directly into - `openclaw.json` under `plugins.entries["sprite-core"].config.agents.` - and optionally restart the gateway, so the whole flow is one command. -- Animate-then-tag pipeline: today the operator passes `--rename` so the - exporter can collapse pixellab's verbose slugs back to canonical emotion - names. Better would be for `pixellab-animate.mjs` to tag each animation - upstream with the emotion key it was invoked with, making `--rename` - unnecessary. -- `/characters//animations` currently 404s in our environment so the - exporter falls back to slug names. Confirm the pixellab API contract - (endpoint path / required auth / response shape) and update the exporter, - or remove the fetch entirely if it's permanently gone. +- Animate-then-tag pipeline: today the exporter's `DEFAULT_CANONICAL_RENAMES` + map collapses pixellab's verbose slugs back to canonical emotion names + (`idle`, `happy`, `sad`, …) when `/characters//animations` 404s. + Better would be for `pixellab-animate.mjs` to tag each animation upstream + with the emotion key it was invoked with (via a name field in the + animate-character request or a follow-up PATCH), so the mapping is + upstream and the exporter never has to guess. +- Auto-restart the gateway after `--apply`: currently the exporter prints a + restart command but intentionally does not run it (visible side effect + the operator should own). Add `--restart-gateway` as opt-in sugar for + unattended runs. +- `/characters//animations` currently 404s in our environment; the + exporter falls back to slug names plus the canonical rename map. + Confirm the pixellab API contract (endpoint path / required auth / + response shape) and update the exporter, or remove the fetch entirely + if it's permanently gone. +- Pixellab 3-concurrent-job cap: account-wide limit seen during batch + runs. Future: add a simple semaphore to the animate or batch scripts so + parallel pipelines block-and-retry instead of 429'ing out on create. diff --git a/packages/plugin/scripts/pixellab-animate.mjs b/packages/plugin/scripts/pixellab-animate.mjs index 57a421b..bb9bc2b 100755 --- a/packages/plugin/scripts/pixellab-animate.mjs +++ b/packages/plugin/scripts/pixellab-animate.mjs @@ -43,21 +43,23 @@ const DEFAULT_FRAME_COUNT = 8; // Pixellab reads `action_description` as a natural-language prompt. The // clean `animation_type` we want back comes from pixellab's own classifier -// on the prompt, so the wording matters. These default prompts were tuned -// against test characters to produce the corresponding animation_type. +// on the prompt, so the wording matters. Motion-oriented verbs + explicit +// body language land on-target far more often than bare emotion tokens — +// "happy" alone often produces a neutral stance; "big smile, eyes bright, +// slight excited bounce" reliably produces a recognizable happy loop. const DEFAULT_PROMPTS = { - idle: "standing still breathing gently", - thinking: "hand on chin looking up hmm expression pondering", - happy: "warm smile bright eyes joyful expression", - sad: "downturned mouth droopy eyes sorrowful expression", - angry: "furrowed brow scowl frustrated expression", - surprised: "wide eyes open mouth shocked expression", - smile: "subtle smirk to genuine smile", - frown: "neutral to disappointed frown", - love: "blushing heart eyes shy smile adoring expression", - wink: "playful wink with smirk flirty expression", - sleepy: "alert to drowsy half-closed eyes", - annoyed: "neutral to frustrated eye roll", + idle: "standing still, gentle breathing, subtle weight shift side to side", + thinking: "hand on chin, eyes glancing upward, head tilting thoughtfully, pondering", + happy: "big open-mouth smile, eyes bright and crinkled in joy, slight excited bounce", + sad: "head lowered, eyes downcast, shoulders slumped, downturned mouth, sorrowful", + angry: "furrowed brow, clenched teeth, fists tightened, body leaning forward, frustrated", + surprised: "eyes wide open, mouth agape, slight step back, arms raised, shocked", + smile: "gentle mouth curving from neutral to a warm genuine smile, eyes softening", + frown: "neutral expression turning to a disappointed frown, brow slightly furrowed", + love: "eyes becoming hearts, cheeks blushing pink, hands clasped near chest, shy adoring smile", + wink: "one eye closed in a playful wink, confident smirk, slight head tilt, finger-gun gesture", + sleepy: "eyes transitioning from alert to drowsy half-closed, head gently nodding, yawning", + annoyed: "neutral turning to frustrated eye roll, arms crossing, exasperated sigh", }; function parseArgs(argv) { diff --git a/packages/plugin/scripts/pixellab-export.mjs b/packages/plugin/scripts/pixellab-export.mjs index 280d6bd..a8ff0ce 100755 --- a/packages/plugin/scripts/pixellab-export.mjs +++ b/packages/plugin/scripts/pixellab-export.mjs @@ -47,6 +47,52 @@ const PIXELLAB_API_BASE = "https://api.pixellab.ai/v2"; const ATLAS_COLS = 7; // matches the 1024×1024 / 136-px-frame layout used upstream const DEFAULT_ATLAS_SIDE = 1024; +// Fallback slug → canonical-emotion rename map applied when the pixellab +// `/characters//animations` metadata endpoint returns 404 (often) and the +// operator did not pass `--rename`. These substrings mirror the default +// prompts in `pixellab-animate.mjs`. Without this, the manifest ends up with +// verbose state keys like `big_open-mouth_smile_eyes_bright_and_crinkled_in_j` +// that break the SpriteCore state contract. User-supplied `--rename` overrides +// these (so a stoic character with a custom "warm smile" prompt can still map +// cleanly). Match is case-insensitive substring after normalizing `_`/`-` to +// spaces. +const DEFAULT_CANONICAL_RENAMES = { + idle: "gentle breathing", + thinking: "hand on chin", + happy: "big open mouth smile", + sad: "shoulders slumped", + angry: "clenched teeth", + surprised: "mouth agape", + love: "eyes becoming hearts", + wink: "playful wink", + smile: "warm genuine smile", + frown: "disappointed frown", + sleepy: "drowsy half closed", + annoyed: "frustrated eye roll", +}; + +// Full human-readable descriptions for each canonical emotion. Used to +// override pixellab's 50-char-truncated slug description when the rename +// step matched a canonical emotion. Keys match `DEFAULT_CANONICAL_RENAMES` +// and `pixellab-animate.mjs#DEFAULT_PROMPTS` (the prompt text we fed +// pixellab to generate the animation in the first place). Without this, +// snippets look like `"description": "big open-mouth smile eyes bright and +// crinkled in j"` (chopped mid-word by the pixellab slug limit). +const DEFAULT_CANONICAL_DESCRIPTIONS = { + idle: "standing still, gentle breathing, subtle weight shift side to side", + thinking: "hand on chin, eyes glancing upward, head tilting thoughtfully, pondering", + happy: "big open-mouth smile, eyes bright and crinkled in joy, slight excited bounce", + sad: "head lowered, eyes downcast, shoulders slumped, downturned mouth, sorrowful", + angry: "furrowed brow, clenched teeth, fists tightened, body leaning forward, frustrated", + surprised: "eyes wide open, mouth agape, slight step back, arms raised, shocked", + love: "eyes becoming hearts, cheeks blushing pink, hands clasped near chest, shy adoring smile", + wink: "one eye closed in a playful wink, confident smirk, slight head tilt, finger-gun gesture", + smile: "gentle mouth curving from neutral to a warm genuine smile, eyes softening", + frown: "neutral expression turning to a disappointed frown, brow slightly furrowed", + sleepy: "eyes transitioning from alert to drowsy half-closed, head gently nodding, yawning", + annoyed: "neutral turning to frustrated eye roll, arms crossing, exasperated sigh", +}; + function parseArgs(argv) { const opts = { uid: null, @@ -64,6 +110,8 @@ function parseArgs(argv) { voiceAuto: false, listVoices: false, elevenApiKeyCommand: null, + apply: false, + configPath: null, }; for (let i = 0; i < argv.length; i++) { const arg = argv[i]; @@ -132,6 +180,15 @@ function parseArgs(argv) { case "--elevenlabs-api-key-command": opts.elevenApiKeyCommand = argv[++i]; break; + case "--apply": + // Patch the generated snippet directly into openclaw.json under + // `plugins.entries["sprite-core"].config.agents.`. Backs + // up the existing config with a timestamp suffix before writing. + opts.apply = true; + break; + case "--config-path": + opts.configPath = argv[++i]; + break; case "-h": case "--help": printUsage(); @@ -181,6 +238,68 @@ function printUsage() { console.error(" --list-voices List available ElevenLabs voices and exit"); console.error(" --elevenlabs-api-key-command Stdout is the ElevenLabs API key;"); console.error(" fallback when ELEVENLABS_API_KEY env is unset"); + console.error(""); + console.error(" Config apply:"); + console.error(" --apply Patch openclaw.json directly (backed up first)"); + console.error(" --config-path

Override openclaw.json location (default: ~/.openclaw/openclaw.json)"); +} + +/** + * Patch `plugins.entries["sprite-core"].config.agents.` in + * openclaw.json with the freshly-generated block. Writes a timestamped + * backup next to the config before overwriting. Creates any missing + * intermediate objects so fresh configs still work. + * + * The gateway reads config at startup, so the caller still needs to + * restart the gateway for the new block to become visible — we print a + * reminder but deliberately do NOT auto-restart (visible side effect + * the operator should own). + */ +async function applyToConfig({ agentId, agentBlock, configPath }) { + const resolved = path.resolve( + configPath ?? path.join(os.homedir(), ".openclaw", "openclaw.json"), + ); + let raw; + try { + raw = await readFile(resolved, "utf8"); + } catch (err) { + throw new Error(`--apply: cannot read ${resolved}: ${err.message}`); + } + let cfg; + try { + cfg = JSON.parse(raw); + } catch (err) { + throw new Error(`--apply: ${resolved} is not valid JSON: ${err.message}`); + } + + const timestamp = new Date() + .toISOString() + .replace(/[-:]/g, "") + .replace(/\..+/, "") + .replace("T", "-"); + const backup = `${resolved}.pre-apply-${timestamp}`; + await writeFile(backup, raw); + + cfg.plugins ??= {}; + cfg.plugins.entries ??= {}; + cfg.plugins.entries["sprite-core"] ??= { enabled: true, config: {} }; + cfg.plugins.entries["sprite-core"].config ??= {}; + cfg.plugins.entries["sprite-core"].config.agents ??= {}; + const existed = + agentId in cfg.plugins.entries["sprite-core"].config.agents; + cfg.plugins.entries["sprite-core"].config.agents[agentId] = agentBlock; + + // Preserve trailing newline behavior from the source so diffs stay clean. + const trailingNewline = raw.endsWith("\n") ? "\n" : ""; + await writeFile(resolved, `${JSON.stringify(cfg, null, 2)}${trailingNewline}`); + + console.log(""); + console.log(`✓ applied to ${resolved}`); + console.log(` ${existed ? "replaced" : "added"} plugins.entries["sprite-core"].config.agents.${agentId}`); + console.log(` backup: ${backup}`); + console.log(""); + console.log("↻ restart the gateway to pick up the new entry:"); + console.log(" pkill -9 -f openclaw-gateway; nohup openclaw gateway run --bind loopback --port 18789 --force > /tmp/openclaw-gateway.log 2>&1 &"); } function resolveApiKey(apiKeyCommand) { @@ -470,7 +589,10 @@ async function detectAnimationDirs(extractDir, apiMetadata, renameMap) { const { readdir } = await import("node:fs/promises"); const dirents = await readdir(animationsRoot, { withFileTypes: true }); - const renameEntries = renameMap ? Object.entries(renameMap) : []; + // User-supplied renames override defaults so custom per-character prompts + // still land on canonical emotion keys. + const mergedRenames = { ...DEFAULT_CANONICAL_RENAMES, ...(renameMap ?? {}) }; + const renameEntries = Object.entries(mergedRenames); // Normalize `_` and `-` to spaces so natural-language needles // (e.g. "standing still") match underscored pixellab slugs // (e.g. "standing_still_breathing_gently"). @@ -509,7 +631,15 @@ async function detectAnimationDirs(extractDir, apiMetadata, renameMap) { const apiType = apiEntry?.animationType?.trim(); const renamed = tryRename(slug, apiType); const cleanName = renamed ?? apiType ?? slug; - const description = apiEntry?.displayName || prettifySlug(slug); + // Prefer the canonical full-prompt description over pixellab's truncated + // slug when we renamed into a known canonical emotion. User-supplied + // display_name (from the API metadata endpoint) still wins when present. + const canonicalDescription = + renamed && DEFAULT_CANONICAL_DESCRIPTIONS[renamed] + ? DEFAULT_CANONICAL_DESCRIPTIONS[renamed] + : null; + const description = + apiEntry?.displayName || canonicalDescription || prettifySlug(slug); raw.push({ dir: facingDir, slug, @@ -874,8 +1004,14 @@ async function main() { ), }; - console.log('paste into openclaw.json under plugins.entries["sprite-core"].config.agents:'); - console.log(JSON.stringify({ [agentId]: agentBlock }, null, 2)); + if (opts.apply) { + await applyToConfig({ agentId, agentBlock, configPath: opts.configPath }); + } else { + console.log('paste into openclaw.json under plugins.entries["sprite-core"].config.agents:'); + console.log(JSON.stringify({ [agentId]: agentBlock }, null, 2)); + console.log(""); + console.log("tip: pass --apply to patch openclaw.json directly (backs up first)."); + } if (!voiceBlock) { console.log(""); console.log( diff --git a/scripts/sync-to-openclaw.sh b/scripts/sync-to-openclaw.sh new file mode 100755 index 0000000..38f8fd5 --- /dev/null +++ b/scripts/sync-to-openclaw.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Sync the standalone sprite-core plugin into an openclaw checkout. +# +# The running gateway loads the bundled plugin from openclaw-src/extensions/ +# sprite-core (and its dist-runtime mirror), not from this standalone repo. +# While the standalone repo is being developed, mirror skills + scripts + +# template over so live agents pick up changes. +# +# Usage: scripts/sync-to-openclaw.sh [openclaw-src-root] +# openclaw-src-root defaults to ~/openclaw-src. + +set -euo pipefail + +SPRITE_CORE_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +OPENCLAW_ROOT="${1:-$HOME/openclaw-src}" +DEST="$OPENCLAW_ROOT/extensions/sprite-core" + +if [[ ! -d "$DEST" ]]; then + echo "error: destination not found: $DEST" >&2 + echo " pass the openclaw-src root as the first arg, or symlink it to ~/openclaw-src" >&2 + exit 1 +fi + +echo "sprite-core sync" +echo " source: $SPRITE_CORE_ROOT" +echo " dest: $DEST" + +# Scripts and template are 1:1 mirrors — no path rewrites needed. +mkdir -p "$DEST/scripts" "$DEST/template" +rsync -a --delete \ + "$SPRITE_CORE_ROOT/packages/plugin/scripts/" "$DEST/scripts/" +rsync -a --delete \ + "$SPRITE_CORE_ROOT/packages/plugin/template/" "$DEST/template/" + +# Skills live under .agents/skills/. Mirror the directory, then rewrite the +# SKILL.md paths from plugin-rooted ("scripts/foo.mjs", "README.md") to +# monorepo-rooted ("extensions/sprite-core/scripts/foo.mjs", +# "extensions/sprite-core/README.md") so the deployed skill resolves files +# correctly when executed from the openclaw-src repo root. +mkdir -p "$DEST/.agents/skills" +rsync -a --delete \ + "$SPRITE_CORE_ROOT/.agents/skills/" "$DEST/.agents/skills/" + +SKILL_FILE="$DEST/.agents/skills/openclaw-pixellab-avatar/SKILL.md" +if [[ -f "$SKILL_FILE" ]]; then + sed -i \ + -e 's|node scripts/pixellab-|node extensions/sprite-core/scripts/pixellab-|g' \ + -e 's|^- `README.md`|- `extensions/sprite-core/README.md`|g' \ + -e 's|^- `template/agent/README.md`|- `extensions/sprite-core/template/agent/README.md`|g' \ + "$SKILL_FILE" + echo " rewrote paths in $(basename "$SKILL_FILE")" +fi + +echo "done." From f6c0c8557e22a29d2d4676e92171cdc9966b23e6 Mon Sep 17 00:00:00 2001 From: Tyler-RNG Date: Fri, 24 Apr 2026 16:46:47 -0400 Subject: [PATCH 07/10] Add plugin-served dashboard UI + write endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ships a browser dashboard for editing per-agent avatar, voice, and emotion config entirely within the plugin package — no changes to openclaw's Control UI. The SPA is served at /sprite-core/ui/ and uses the same client-js SDK the phone and watch use, so previews render through the real playback engine. Server side (packages/plugin/src/): - ui-route.ts: static serve for the built UI bundle with SPA fallback and hashed-asset cache headers. Registered with auth:"plugin" so the HTML shell can bootstrap in a fresh browser; API routes stay gateway-gated. - character-manifest-route.ts: HTTP sibling of node.getCharacterManifest so the dashboard preview can drive the client SDK over plain HTTP. - agents-write-route.ts: PUT /sprite-core/agents/:id and PUT /sprite-core/agents/:id/emotions/:state. - config-writes.ts: serialized writes via the openclaw SDK's readConfigFileSnapshotForWrite + writeConfigFile, scoped to plugins.entries["sprite-core"].config. Env-var-safe. - validation.ts: schema validation against the plugin's configSchema before any write is persisted. UI (packages/plugin/ui/): Vite + React + TS SPA, depends on workspace @tyler-rng/sprite-core-client and -schema. Builds into ui-dist/ which ships inside the npm tarball via package.json "files". Dev tooling: - scripts/install-into-openclaw.sh: build + pack + atomic-swap the plugin into any OpenClaw install's node_modules and restart the daemon. Self- contained; no openclaw-src checkout needed. - scripts/sync-to-openclaw.sh extended to carry the built UI bundle. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 2 + README.md | 33 ++ package.json | 1 + packages/plugin/README.md | 50 +- packages/plugin/index.ts | 48 ++ packages/plugin/package.json | 5 +- packages/plugin/scripts/pixellab-export.mjs | 27 +- packages/plugin/src/agents-write-route.ts | 220 ++++++++ .../plugin/src/character-manifest-route.ts | 74 +++ packages/plugin/src/config-writes.ts | 68 +++ packages/plugin/src/ui-route.ts | 186 +++++++ packages/plugin/src/validation.ts | 272 ++++++++++ packages/plugin/ui/index.html | 13 + packages/plugin/ui/package.json | 27 + packages/plugin/ui/src/App.tsx | 105 ++++ packages/plugin/ui/src/api/client.ts | 57 +++ packages/plugin/ui/src/api/types.ts | 98 ++++ .../ui/src/components/EmotionEditor.tsx | 182 +++++++ packages/plugin/ui/src/main.tsx | 28 + .../plugin/ui/src/preview/AvatarPreview.tsx | 207 ++++++++ .../plugin/ui/src/preview/use-observable.ts | 22 + packages/plugin/ui/src/styles.css | 272 ++++++++++ packages/plugin/ui/tsconfig.app.json | 20 + packages/plugin/ui/tsconfig.json | 7 + packages/plugin/ui/tsconfig.node.json | 15 + packages/plugin/ui/vite.config.ts | 28 + pnpm-lock.yaml | 483 +++++++++++++++++- pnpm-workspace.yaml | 1 + scripts/install-into-openclaw.sh | 121 +++++ scripts/sync-to-openclaw.sh | 35 +- 30 files changed, 2686 insertions(+), 21 deletions(-) create mode 100644 packages/plugin/src/agents-write-route.ts create mode 100644 packages/plugin/src/character-manifest-route.ts create mode 100644 packages/plugin/src/config-writes.ts create mode 100644 packages/plugin/src/ui-route.ts create mode 100644 packages/plugin/src/validation.ts create mode 100644 packages/plugin/ui/index.html create mode 100644 packages/plugin/ui/package.json create mode 100644 packages/plugin/ui/src/App.tsx create mode 100644 packages/plugin/ui/src/api/client.ts create mode 100644 packages/plugin/ui/src/api/types.ts create mode 100644 packages/plugin/ui/src/components/EmotionEditor.tsx create mode 100644 packages/plugin/ui/src/main.tsx create mode 100644 packages/plugin/ui/src/preview/AvatarPreview.tsx create mode 100644 packages/plugin/ui/src/preview/use-observable.ts create mode 100644 packages/plugin/ui/src/styles.css create mode 100644 packages/plugin/ui/tsconfig.app.json create mode 100644 packages/plugin/ui/tsconfig.json create mode 100644 packages/plugin/ui/tsconfig.node.json create mode 100644 packages/plugin/ui/vite.config.ts create mode 100755 scripts/install-into-openclaw.sh diff --git a/.gitignore b/.gitignore index db8ebce..c9ecb51 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,13 @@ node_modules/ dist/ +packages/plugin/ui-dist/ .DS_Store *.log .env .env.local coverage/ .turbo/ +*.tsbuildinfo # Kotlin / Gradle packages/client-kotlin/**/build/ diff --git a/README.md b/README.md index 831925f..9b0c466 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,9 @@ OpenClaw installs the plugin like any other npm-served extension: } ``` +Full config schema and the dashboard UI are documented in +[`packages/plugin/README.md`](./packages/plugin/README.md). + And the apps pull in the language-appropriate client SDK: - Web / Electron / React Native → `@tyler-rng/sprite-core-client` (npm) @@ -81,6 +84,36 @@ And the apps pull in the language-appropriate client SDK: For live-development against OpenClaw without publishing, both Gradle (`includeBuild`) and SwiftPM (`path:`) support local path links. +## Installing a dev build into your local OpenClaw + +If you're testing plugin changes against a real gateway without publishing to +npm, use the helper script — it builds the UI, packs the plugin, drops it into +your OpenClaw install's `node_modules`, and restarts the daemon: + +```bash +# Defaults to ~/.openclaw/app (the global `npm i -g openclaw` location) +scripts/install-into-openclaw.sh + +# Or point at a different install: +scripts/install-into-openclaw.sh --install-dir /path/to/openclaw + +# Faster iteration: reuse an existing ui-dist/ build +scripts/install-into-openclaw.sh --skip-build +``` + +The script is idempotent — every run does an atomic swap and keeps the +previous copy at `node_modules/@tyler-rng/sprite-core.prev` for rollback. + +After it finishes, enable the plugin in your `openclaw.json` (see +[packages/plugin/README.md → Enable](./packages/plugin/README.md#enable)) +and browse to `http://localhost:18789/sprite-core/ui/` — the dashboard's +HTML shell is served publicly; its API calls are same-origin and ride the +session you already have for the OpenClaw Control UI. + +`scripts/sync-to-openclaw.sh` serves a narrower use case: it mirrors this +repo's plugin sources into a sibling `openclaw-src/extensions/sprite-core/` +checkout. Only relevant if you're developing OpenClaw core at the same time. + ## License MIT. diff --git a/package.json b/package.json index 031f7fc..7762884 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "license": "MIT", "workspaces": [ "packages/plugin", + "packages/plugin/ui", "packages/client-js", "schema" ], diff --git a/packages/plugin/README.md b/packages/plugin/README.md index a95c17c..1ead5e3 100644 --- a/packages/plugin/README.md +++ b/packages/plugin/README.md @@ -250,12 +250,50 @@ name in the manifest just works. ## Routes -| Path | Auth | Purpose | -| ----------------------------- | --------- | ---------------------------------------------------------------------- | -| `GET /openclaw-assets/` | gateway\* | Static asset serving. \*`auth: "plugin"` when `publicAssets: true`. | -| `GET /stream/tts` | gateway | Streaming TTS proxy (ElevenLabs). | -| `POST /stream/stt` | gateway | Streaming STT proxy (ElevenLabs). | -| `GET /sprite-core/agents` | gateway | `{ agents: { : { avatar, voice } }, publicBaseUrl? }` for clients. | +| Path | Auth | Purpose | +| ------------------------------------------------------------ | --------- | ----------------------------------------------------------------------------------------------- | +| `GET /openclaw-assets/` | gateway\* | Static asset serving. \*`auth: "plugin"` when `publicAssets: true`. | +| `GET /stream/tts` | gateway | Streaming TTS proxy (ElevenLabs). | +| `POST /stream/stt` | gateway | Streaming STT proxy (ElevenLabs). | +| `GET /sprite-core/agents` | gateway | `{ agents: { : { avatar, voice } }, publicBaseUrl? }` for clients. | +| `PUT /sprite-core/agents/:id` | gateway | Replace a single agent entry. Body: `AgentEntry`. Dashboard UI writes here. | +| `PUT /sprite-core/agents/:id/emotions/:state` | gateway | Replace a single emotion entry. Body: `{ description, directive? }`. Dashboard UI writes here. | +| `GET /sprite-core/character-manifest?agentId=[&mode=...]` | gateway | HTTP sibling of `node.getCharacterManifest` — used by the dashboard UI preview. | +| `GET /sprite-core/ui[/path]` | plugin | Dashboard UI bundle (static HTML + JS, no secrets). See [Dashboard UI](#dashboard-ui). | + +## Dashboard UI + +SpriteCore ships with a browser dashboard for editing per-agent avatar, +voice, and emotion config. It's served by the plugin itself — no changes to +the OpenClaw Control UI are required. + +**URL:** `https:///sprite-core/ui` + +The dashboard uses the same TypeScript client SDK (`@tyler-rng/sprite-core-client`) +that the phone and watch use to render avatars. Previews in the editor drive +through the real playback engine, so what you see in the dashboard is exactly +what users see on-device. + +Writes go through the OpenClaw SDK's config-file write path +(`readConfigFileSnapshotForWrite` + `writeConfigFile`). Saving in the +dashboard is equivalent to hand-editing `openclaw.json`'s +`plugins.entries["sprite-core"].config` branch — and no other branches are +ever touched. + +### Building the UI bundle + +The dashboard is prebuilt into `packages/plugin/ui-dist/` before publish, so +npm-installed copies of the plugin serve the UI out of the box. For +in-workspace development: + +```sh +# from repo root +pnpm --filter @tyler-rng/sprite-core-ui build # one-shot build +pnpm --filter @tyler-rng/sprite-core-ui dev # Vite dev server (HMR) +``` + +In dev mode, Vite proxies `/sprite-core/*` to the gateway URL in +`SPRITE_CORE_GATEWAY_URL` (default `http://localhost:8080`). ## Gateway RPC diff --git a/packages/plugin/index.ts b/packages/plugin/index.ts index d7812ec..c35b2ee 100644 --- a/packages/plugin/index.ts +++ b/packages/plugin/index.ts @@ -1,8 +1,16 @@ import { loadConfig } from "openclaw/plugin-sdk/config-runtime"; import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; import { AGENTS_ROUTE_PATH, handleAgentsRequest } from "./src/agents-route.js"; +import { + AGENTS_WRITE_ROUTE_PREFIX, + handleAgentsWriteRequest, +} from "./src/agents-write-route.js"; import { ASSETS_ROUTE_PATH, handleAssetsRequest } from "./src/assets-route.js"; import { buildCharacterManifest } from "./src/character-manifest.js"; +import { + CHARACTER_MANIFEST_ROUTE_PATH, + handleCharacterManifestRequest, +} from "./src/character-manifest-route.js"; import { buildPromptingInstruction, hasSpriteDisplayCapability, @@ -10,6 +18,7 @@ import { } from "./src/prompting.js"; import { handleSttRequest, STT_ROUTE_PATH } from "./src/stt-route.js"; import { handleTtsRequest, TTS_ROUTE_PATH } from "./src/tts-route.js"; +import { handleUiRequest, UI_ROUTE_PATH } from "./src/ui-route.js"; import type { SpriteCoreConfig } from "./src/types.js"; const SPRITE_CORE_PLUGIN_ID = "sprite-core"; @@ -91,6 +100,45 @@ export default definePluginEntry({ }, }); + // Dashboard UI (browser SPA). Serves the built bundle from + // packages/plugin/ui-dist/ shipped inside the plugin package. Prefix + // match so nested asset paths (/sprite-core/ui/assets/app.abc123.js) + // resolve through the same handler. + // + // auth: "plugin" — the static HTML + JS bundle has no secrets and must + // be servable to a fresh browser so the SPA can bootstrap. The SPA then + // uses `credentials: "same-origin"` for its API calls to `/sprite-core/*` + // which remain gateway-gated. This mirrors how openclaw's Control UI + // serves its HTML shell at `/` unauthenticated and authenticates its + // API calls after the bundle has loaded. + api.registerHttpRoute({ + path: UI_ROUTE_PATH, + match: "prefix", + auth: "plugin", + handler: (req, res) => handleUiRequest(req, res), + }); + + // HTTP sibling of node.getCharacterManifest — the dashboard UI consumes + // this to drive the client SDK's AssetSource without speaking the + // gateway WebSocket. Keeps everything the UI needs on the HTTP plane. + api.registerHttpRoute({ + path: CHARACTER_MANIFEST_ROUTE_PATH, + match: "exact", + auth: "gateway", + handler: (req, res) => + handleCharacterManifestRequest(req, res, { readPluginConfig }), + }); + + // Write endpoints: PUT /sprite-core/agents/:id and + // PUT /sprite-core/agents/:id/emotions/:state. The handler performs its + // own path parsing, so register as a prefix match. + api.registerHttpRoute({ + path: AGENTS_WRITE_ROUTE_PREFIX, + match: "prefix", + auth: "gateway", + handler: (req, res) => handleAgentsWriteRequest(req, res), + }); + // System-prompt contribution: teach the model the `<<>>` marker // vocabulary, but only for sessions whose connected client can render a // sprite. Dashboard / Telegram / headless chat never see this block even diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 8243ecd..3adac02 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -13,6 +13,7 @@ "index.ts", "src", "template", + "ui-dist", "openclaw.plugin.json", "tsconfig.json", "LICENSE", @@ -20,7 +21,9 @@ ], "scripts": { "typecheck": "tsc --noEmit", - "test": "vitest run" + "test": "vitest run", + "build:ui": "pnpm --filter @tyler-rng/sprite-core-ui build", + "dev:ui": "pnpm --filter @tyler-rng/sprite-core-ui dev" }, "devDependencies": { "openclaw": "2026.4.15-beta.1", diff --git a/packages/plugin/scripts/pixellab-export.mjs b/packages/plugin/scripts/pixellab-export.mjs index a8ff0ce..850e063 100755 --- a/packages/plugin/scripts/pixellab-export.mjs +++ b/packages/plugin/scripts/pixellab-export.mjs @@ -170,8 +170,11 @@ function parseArgs(argv) { // ElevenLabs library. Any other value is treated as a voice id. { const v = argv[++i]; - if (v === "auto") opts.voiceAuto = true; - else opts.voiceId = v; + if (v === "auto") { + opts.voiceAuto = true; + } else { + opts.voiceId = v; + } } break; case "--list-voices": @@ -263,13 +266,13 @@ async function applyToConfig({ agentId, agentBlock, configPath }) { try { raw = await readFile(resolved, "utf8"); } catch (err) { - throw new Error(`--apply: cannot read ${resolved}: ${err.message}`); + throw new Error(`--apply: cannot read ${resolved}: ${err.message}`, { cause: err }); } let cfg; try { cfg = JSON.parse(raw); } catch (err) { - throw new Error(`--apply: ${resolved} is not valid JSON: ${err.message}`); + throw new Error(`--apply: ${resolved} is not valid JSON: ${err.message}`, { cause: err }); } const timestamp = new Date() @@ -344,14 +347,18 @@ function resolveApiKey(apiKeyCommand) { function resolveElevenLabsKey(apiKeyCommand) { const fromEnv = process.env.ELEVENLABS_API_KEY?.trim(); - if (fromEnv) return fromEnv; + if (fromEnv) { + return fromEnv; + } if (apiKeyCommand) { try { const out = execFileSync("sh", ["-c", apiKeyCommand], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"], }).trim(); - if (out) return out; + if (out) { + return out; + } } catch (err) { console.error(`--elevenlabs-api-key-command failed: ${err.message}`); return null; @@ -362,7 +369,9 @@ function resolveElevenLabsKey(apiKeyCommand) { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], }).trim(); - if (out) return out; + if (out) { + return out; + } } catch { // pass not available or entry missing — silent; caller decides fallback. } @@ -591,7 +600,7 @@ async function detectAnimationDirs(extractDir, apiMetadata, renameMap) { // User-supplied renames override defaults so custom per-character prompts // still land on canonical emotion keys. - const mergedRenames = { ...DEFAULT_CANONICAL_RENAMES, ...(renameMap ?? {}) }; + const mergedRenames = { ...DEFAULT_CANONICAL_RENAMES, ...renameMap }; const renameEntries = Object.entries(mergedRenames); // Normalize `_` and `-` to spaces so natural-language needles // (e.g. "standing still") match underscored pixellab slugs @@ -839,7 +848,7 @@ async function main() { } for (const v of voices) { const labelBits = Object.entries(v.labels || {}) - .map(([k, val]) => `${k}=${val}`) + .map(([k, val]) => `${k}=${String(val)}`) .join(" "); const cat = v.category ? ` [${v.category}]` : ""; console.log(`${v.voiceId} ${v.name}${cat}${labelBits ? ` ${labelBits}` : ""}`); diff --git a/packages/plugin/src/agents-write-route.ts b/packages/plugin/src/agents-write-route.ts new file mode 100644 index 0000000..0bc0bb6 --- /dev/null +++ b/packages/plugin/src/agents-write-route.ts @@ -0,0 +1,220 @@ +import type { IncomingMessage, ServerResponse } from "node:http"; +import { sendJson, sendMethodNotAllowed } from "./http-helpers.js"; +import { updateSpriteCoreConfig } from "./config-writes.js"; +import { validateAgentEntry, validateEmotionEntry } from "./validation.js"; + +export const AGENTS_WRITE_ROUTE_PREFIX = "/sprite-core/agents/"; + +const MAX_BODY_BYTES = 64 * 1024; + +// Parse `/sprite-core/agents/:id` and `/sprite-core/agents/:id/emotions/:state`. +type ParsedTarget = + | { kind: "agent"; agentId: string } + | { kind: "emotion"; agentId: string; state: string } + | null; + +function parseTarget(pathname: string): ParsedTarget { + if (!pathname.startsWith(AGENTS_WRITE_ROUTE_PREFIX)) { + return null; + } + const tail = pathname.slice(AGENTS_WRITE_ROUTE_PREFIX.length); + if (!tail) { + return null; + } + + const parts = tail.split("/"); + if (parts.length === 1) { + const agentId = safeDecode(parts[0]); + if (!agentId) { + return null; + } + return { kind: "agent", agentId }; + } + if (parts.length === 3 && parts[1] === "emotions") { + const agentId = safeDecode(parts[0]); + const state = safeDecode(parts[2]); + if (!agentId || !state) { + return null; + } + return { kind: "emotion", agentId, state }; + } + return null; +} + +function safeDecode(seg: string | undefined): string | null { + if (!seg) { + return null; + } + try { + const d = decodeURIComponent(seg); + if (!d || d.includes("/") || d.includes("\0")) { + return null; + } + return d; + } catch { + return null; + } +} + +async function readJsonBody(req: IncomingMessage): Promise { + return await new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + let total = 0; + req.on("data", (chunk: Buffer) => { + total += chunk.length; + if (total > MAX_BODY_BYTES) { + reject(new BodyTooLargeError()); + req.destroy(); + return; + } + chunks.push(chunk); + }); + req.on("end", () => { + const raw = Buffer.concat(chunks).toString("utf8"); + if (!raw) { + resolve(undefined); + return; + } + try { + resolve(JSON.parse(raw)); + } catch { + reject(new JsonParseError()); + } + }); + req.on("error", (err) => reject(err)); + }); +} + +class BodyTooLargeError extends Error { + constructor() { + super("body too large"); + this.name = "BodyTooLargeError"; + } +} +class JsonParseError extends Error { + constructor() { + super("invalid JSON"); + this.name = "JsonParseError"; + } +} + +/** + * Handles `PUT /sprite-core/agents/:id` and + * `PUT /sprite-core/agents/:id/emotions/:state`. Returns `true` if the + * request was handled (including error responses), `false` if the path + * didn't match so the dispatcher can try the next handler. + * + * Registered with `auth: "gateway"` so the plugin HTTP dispatcher enforces + * gateway auth before this handler runs. + */ +export async function handleAgentsWriteRequest( + req: IncomingMessage, + res: ServerResponse, +): Promise { + const urlRaw = req.url; + if (!urlRaw) { + return false; + } + let url: URL; + try { + url = new URL(urlRaw, "http://localhost"); + } catch { + return false; + } + const target = parseTarget(url.pathname); + if (!target) { + return false; + } + + if (req.method !== "PUT") { + sendMethodNotAllowed(res, "PUT"); + return true; + } + + let body: unknown; + try { + body = await readJsonBody(req); + } catch (err) { + if (err instanceof BodyTooLargeError) { + sendJson(res, 413, { + error: { message: `body exceeds ${MAX_BODY_BYTES} bytes`, type: "invalid_request_error" }, + }); + return true; + } + sendJson(res, 400, { + error: { message: "invalid JSON body", type: "invalid_request_error" }, + }); + return true; + } + + if (target.kind === "emotion") { + const parsed = validateEmotionEntry(body); + if (!parsed.ok) { + sendJson(res, 400, { + error: { message: parsed.errors.join("; "), type: "invalid_request_error" }, + }); + return true; + } + try { + await updateSpriteCoreConfig((cfg) => { + const agents = { ...cfg.agents }; + const agent = agents[target.agentId]; + if (!agent) { + throw new AgentNotFoundError(target.agentId); + } + agents[target.agentId] = { + ...agent, + emotions: { + ...agent.emotions, + [target.state]: parsed.value, + }, + }; + return { ...cfg, agents }; + }); + sendJson(res, 200, { ok: true }); + } catch (err) { + respondMutationError(res, err); + } + return true; + } + + // target.kind === "agent" + const parsed = validateAgentEntry(body); + if (!parsed.ok) { + sendJson(res, 400, { + error: { message: parsed.errors.join("; "), type: "invalid_request_error" }, + }); + return true; + } + try { + await updateSpriteCoreConfig((cfg) => { + const agents = { ...cfg.agents }; + agents[target.agentId] = parsed.value; + return { ...cfg, agents }; + }); + sendJson(res, 200, { ok: true }); + } catch (err) { + respondMutationError(res, err); + } + return true; +} + +class AgentNotFoundError extends Error { + constructor(agentId: string) { + super(`unknown agent: ${agentId}`); + this.name = "AgentNotFoundError"; + } +} + +function respondMutationError(res: ServerResponse, err: unknown): void { + if (err instanceof AgentNotFoundError) { + sendJson(res, 404, { + error: { message: err.message, type: "invalid_request_error" }, + }); + return; + } + const message = err instanceof Error ? err.message : String(err); + sendJson(res, 500, { + error: { message: `config write failed: ${message}`, type: "invalid_request_error" }, + }); +} diff --git a/packages/plugin/src/character-manifest-route.ts b/packages/plugin/src/character-manifest-route.ts new file mode 100644 index 0000000..ffacd34 --- /dev/null +++ b/packages/plugin/src/character-manifest-route.ts @@ -0,0 +1,74 @@ +import type { IncomingMessage, ServerResponse } from "node:http"; +import { buildCharacterManifest } from "./character-manifest.js"; +import { sendJson, sendMethodNotAllowed } from "./http-helpers.js"; +import type { SpriteCoreConfig } from "./types.js"; + +export const CHARACTER_MANIFEST_ROUTE_PATH = "/sprite-core/character-manifest"; + +export type CharacterManifestRouteOptions = { + readPluginConfig: () => SpriteCoreConfig | undefined; +}; + +/** + * `GET /sprite-core/character-manifest?agentId=[&mode=]` — HTTP + * sibling of the `node.getCharacterManifest` gateway RPC. Exposes the same + * manifest the phone/watch clients already fetch over WebSocket, but on the + * HTTP plane so the plugin's browser UI can drive the SDK's `AssetSource` + * without speaking the gateway WebSocket protocol. + * + * Response matches `NodeGetCharacterManifestResult`: `{ manifest, revision }`. + * Registered with `auth: "gateway"` by the caller. + */ +export async function handleCharacterManifestRequest( + req: IncomingMessage, + res: ServerResponse, + opts: CharacterManifestRouteOptions, +): Promise { + const urlRaw = req.url; + if (!urlRaw) { + return false; + } + let url: URL; + try { + url = new URL(urlRaw, "http://localhost"); + } catch { + return false; + } + if (url.pathname !== CHARACTER_MANIFEST_ROUTE_PATH) { + return false; + } + + if (req.method !== "GET") { + sendMethodNotAllowed(res, "GET"); + return true; + } + + const agentId = url.searchParams.get("agentId")?.trim(); + if (!agentId) { + sendJson(res, 400, { + error: { message: "agentId required", type: "invalid_request_error" }, + }); + return true; + } + + const modeParam = url.searchParams.get("mode"); + const modes = modeParam ? [modeParam] : undefined; + + const result = await buildCharacterManifest({ + pluginConfig: opts.readPluginConfig(), + agentId, + modes, + }); + if (!result.ok) { + const status = result.code === "unknown-agent" ? 404 : 503; + sendJson(res, status, { + error: { message: result.message, type: "invalid_request_error" }, + }); + return true; + } + sendJson(res, 200, { + manifest: result.manifest, + revision: result.revision, + }); + return true; +} diff --git a/packages/plugin/src/config-writes.ts b/packages/plugin/src/config-writes.ts new file mode 100644 index 0000000..1e851c2 --- /dev/null +++ b/packages/plugin/src/config-writes.ts @@ -0,0 +1,68 @@ +import { + readConfigFileSnapshotForWrite, + writeConfigFile, +} from "openclaw/plugin-sdk/config-runtime"; +import type { SpriteCoreConfig } from "./types.js"; + +const SPRITE_CORE_PLUGIN_ID = "sprite-core"; + +// Serial write lock. Two concurrent PUTs to different agent fields can still +// clobber each other because each reads the full snapshot, mutates its slice, +// then writes. Serializing through this queue makes the read-modify-write +// atomic at the plugin layer. (Openclaw itself doesn't guarantee cross-write +// ordering.) +let writeChain: Promise = Promise.resolve(); + +export type SpriteCoreConfigMutator = (current: SpriteCoreConfig) => SpriteCoreConfig; + +/** + * Read the live config, project our plugin slice through [mutate], and write + * back using the SDK's read-for-write snapshot. Pairing the read snapshot's + * `writeOptions` with the write is essential — it carries the env-snapshot + * needed to re-preserve `${VAR}` interpolations. Reading with `loadConfig()` + * and writing with `writeConfigFile(...)` loses that and re-persists secrets + * as plaintext. + * + * Stays strictly inside `plugins.entries["sprite-core"].config`. Other plugin + * slices, channel config, provider config, etc. are never touched. + */ +export async function updateSpriteCoreConfig(mutate: SpriteCoreConfigMutator): Promise { + const run = async () => { + const { snapshot, writeOptions } = await readConfigFileSnapshotForWrite(); + // Mutate `resolved`, not `config`. `resolved` is the post-include, + // post-${VAR} form without runtime defaults baked in — that's what the + // write path expects so it can re-wrap env substitutions. Using the + // runtime form would freeze defaults into the persisted file. + const cfg = snapshot.resolved; + const plugins = cfg.plugins ?? {}; + const entries = plugins.entries ?? {}; + const ourEntry = entries[SPRITE_CORE_PLUGIN_ID] ?? {}; + const currentPluginCfg = (ourEntry.config ?? {}) as SpriteCoreConfig; + + const nextPluginCfg = mutate(currentPluginCfg); + + const nextCfg = { + ...cfg, + plugins: { + ...plugins, + entries: { + ...entries, + [SPRITE_CORE_PLUGIN_ID]: { + ...ourEntry, + // SpriteCoreConfig has named optional fields; the slot type wants + // Record. They're structurally compatible but + // TS rejects the direct assignment; cast through unknown. + config: nextPluginCfg as unknown as Record, + }, + }, + }, + }; + + await writeConfigFile(nextCfg, writeOptions); + }; + const next = writeChain.then(run, run); + writeChain = next.catch(() => { + /* swallow so the chain doesn't get wedged by a single failure */ + }); + await next; +} diff --git a/packages/plugin/src/ui-route.ts b/packages/plugin/src/ui-route.ts new file mode 100644 index 0000000..f0a8bcf --- /dev/null +++ b/packages/plugin/src/ui-route.ts @@ -0,0 +1,186 @@ +import { createReadStream } from "node:fs"; +import fs from "node:fs/promises"; +import type { IncomingMessage, ServerResponse } from "node:http"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { sendJson, sendMethodNotAllowed } from "./http-helpers.js"; + +export const UI_ROUTE_PATH = "/sprite-core/ui"; + +const MIME_BY_EXT: Record = { + ".html": "text/html; charset=utf-8", + ".js": "application/javascript; charset=utf-8", + ".mjs": "application/javascript; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".map": "application/json; charset=utf-8", + ".svg": "image/svg+xml", + ".png": "image/png", + ".webp": "image/webp", + ".ico": "image/x-icon", + ".woff": "font/woff", + ".woff2": "font/woff2", +}; + +// UI bundle ships at `/ui-dist/`. The plugin is consumed in two +// layouts: +// 1. Source / workspace: src/ui-route.ts sits next to ../ui-dist/ +// 2. Bundled (tsdown): index.js is the whole plugin, with ui-dist/ +// as a sibling at the same level +// We accept either — probe both candidates and pick the one that exists. +import fsSync from "node:fs"; + +function resolveUiDistDir(): string { + const here = path.dirname(fileURLToPath(import.meta.url)); + const candidates = [path.resolve(here, "..", "ui-dist"), path.resolve(here, "ui-dist")]; + for (const c of candidates) { + if (fsSync.existsSync(path.join(c, "index.html"))) { + return c; + } + } + // Fall back to the source-layout path; the route handler will emit a 503 + // pointing at the build step if the bundle really isn't there. + return candidates[0] ?? path.resolve(here, "..", "ui-dist"); +} + +const UI_DIST_DIR = resolveUiDistDir(); + +type ValidatedPath = + | { ok: true; absPath: string; stat: { size: number; mtimeMs: number } } + | { ok: false; status: number; message: string }; + +async function resolveRequestedFile(relPath: string): Promise { + const safe = relPath.replace(/^\/+/, ""); + if (safe.includes("\0")) { + return { ok: false, status: 400, message: "invalid path" }; + } + const absPath = path.resolve(UI_DIST_DIR, safe); + const rel = path.relative(UI_DIST_DIR, absPath); + if (rel.startsWith("..") || path.isAbsolute(rel)) { + return { ok: false, status: 403, message: "path traversal rejected" }; + } + try { + const stat = await fs.stat(absPath); + if (!stat.isFile()) { + return { ok: false, status: 404, message: "not a file" }; + } + return { ok: true, absPath, stat: { size: stat.size, mtimeMs: stat.mtimeMs } }; + } catch { + return { ok: false, status: 404, message: "not found" }; + } +} + +function buildEtag(stat: { size: number; mtimeMs: number }): string { + return `"${stat.mtimeMs.toString(36)}-${stat.size.toString(36)}"`; +} + +function isHashedAsset(relPath: string): boolean { + // Vite emits hashed filenames under /assets/ — cache those for a year. + // index.html and anything unhashed gets a no-cache policy. + return relPath.startsWith("assets/") || /[-.][0-9a-f]{8,}\.[a-z0-9]+$/i.test(relPath); +} + +/** + * `GET /sprite-core/ui[/path]` — serves the built UI bundle from + * `packages/plugin/ui-dist/`. Unknown paths fall back to `index.html` so + * client-side routing (if we add it later) works. Returns 404 when the + * bundle hasn't been built yet — the error message points at the build step. + * + * Registered as a prefix route with `auth: "plugin"` — the static HTML + + * JS bundle must be servable to a fresh browser so the SPA can bootstrap. + * The SPA then makes same-origin API calls to `/sprite-core/*` endpoints + * that stay gateway-gated. Registering this route with `auth: "gateway"` + * would 401 the HTML shell itself and block the bundle from loading. + */ +export async function handleUiRequest( + req: IncomingMessage, + res: ServerResponse, +): Promise { + const urlRaw = req.url; + if (!urlRaw) { + return false; + } + let url: URL; + try { + url = new URL(urlRaw, "http://localhost"); + } catch { + return false; + } + if (url.pathname !== UI_ROUTE_PATH && !url.pathname.startsWith(`${UI_ROUTE_PATH}/`)) { + return false; + } + + if (req.method !== "GET" && req.method !== "HEAD") { + sendMethodNotAllowed(res, "GET, HEAD"); + return true; + } + + // Strip the route prefix; treat the rest as a path into ui-dist. + const afterPrefix = url.pathname.slice(UI_ROUTE_PATH.length).replace(/^\//, ""); + const relPath = afterPrefix || "index.html"; + + let validated = await resolveRequestedFile(relPath); + // SPA fallback: any unknown path that doesn't have a file extension gets + // index.html so client-side routing can handle it. Paths with extensions + // get a real 404 — a missing `app.123.js` is not an SPA route. + if (!validated.ok && validated.status === 404 && !path.extname(relPath)) { + validated = await resolveRequestedFile("index.html"); + } + + if (!validated.ok) { + // If index.html itself is missing, the bundle hasn't been built. + if (relPath === "index.html" || (!path.extname(relPath) && validated.status === 404)) { + sendJson(res, 503, { + error: { + message: + "SpriteCore UI bundle not built. Run `pnpm --filter @tyler-rng/sprite-core-ui build`.", + type: "invalid_request_error", + }, + }); + return true; + } + sendJson(res, validated.status, { + error: { message: validated.message, type: "invalid_request_error" }, + }); + return true; + } + + const ext = path.extname(validated.absPath).toLowerCase(); + const contentType = MIME_BY_EXT[ext] ?? "application/octet-stream"; + const etag = buildEtag(validated.stat); + + if (isHashedAsset(relPath)) { + res.setHeader("Cache-Control", "public, max-age=31536000, immutable"); + } else { + res.setHeader("Cache-Control", "no-cache"); + } + res.setHeader("ETag", etag); + + if (req.headers["if-none-match"] === etag) { + res.statusCode = 304; + res.end(); + return true; + } + + res.statusCode = 200; + res.setHeader("Content-Type", contentType); + res.setHeader("Content-Length", String(validated.stat.size)); + + if (req.method === "HEAD") { + res.end(); + return true; + } + + const stream = createReadStream(validated.absPath); + stream.once("error", () => { + if (!res.headersSent) { + sendJson(res, 500, { + error: { message: "read error", type: "invalid_request_error" }, + }); + return; + } + res.destroy(); + }); + stream.pipe(res); + return true; +} diff --git a/packages/plugin/src/validation.ts b/packages/plugin/src/validation.ts new file mode 100644 index 0000000..db2200f --- /dev/null +++ b/packages/plugin/src/validation.ts @@ -0,0 +1,272 @@ +import type { + SpriteCoreAgentEntry, + SpriteCoreAvatarConfig, + SpriteCoreEmotionDirective, + SpriteCoreEmotionEntry, + SpriteCoreVoiceConfig, + SpriteCorePromptingConfig, +} from "./types.js"; + +// Narrow, hand-rolled validators for the write-side payloads. Mirrors the +// plugin's openclaw.plugin.json configSchema shapes but only the subset we +// accept over HTTP: full AgentEntry and single EmotionEntry. Kept dep-free +// so the plugin bundle doesn't grow an ajv/zod runtime. +// +// Returns the canonicalized value on success (unknown keys stripped) and a +// list of path-qualified error messages on failure. + +export type ValidationResult = + | { ok: true; value: T } + | { ok: false; errors: string[] }; + +export function validateEmotionEntry(input: unknown): ValidationResult { + const errors: string[] = []; + const obj = asObject(input, "emotion", errors); + if (!obj) { + return { ok: false, errors }; + } + + const description = asString(obj["description"], "emotion.description", errors, { required: true }); + let directive: SpriteCoreEmotionDirective | undefined; + if (obj["directive"] !== undefined) { + directive = validateDirective(obj["directive"], "emotion.directive", errors); + } + + if (errors.length > 0) { + return { ok: false, errors }; + } + return { + ok: true, + value: { + description: description ?? "", + ...(directive ? { directive } : {}), + }, + }; +} + +export function validateAgentEntry(input: unknown): ValidationResult { + const errors: string[] = []; + const obj = asObject(input, "agent", errors); + if (!obj) { + return { ok: false, errors }; + } + + const out: SpriteCoreAgentEntry = {}; + if (obj["avatar"] !== undefined) { + const avatar = validateAvatar(obj["avatar"], "agent.avatar", errors); + if (avatar) { + out.avatar = avatar; + } + } + if (obj["voice"] !== undefined) { + const voice = validateVoice(obj["voice"], "agent.voice", errors); + if (voice) { + out.voice = voice; + } + } + if (obj["prompting"] !== undefined) { + const prompting = validatePrompting(obj["prompting"], "agent.prompting", errors); + if (prompting) { + out.prompting = prompting; + } + } + if (obj["emotions"] !== undefined) { + const emotions = validateEmotionsMap(obj["emotions"], "agent.emotions", errors); + if (emotions) { + out.emotions = emotions; + } + } + + if (errors.length > 0) { + return { ok: false, errors }; + } + return { ok: true, value: out }; +} + +// --- helpers --- + +function asObject(v: unknown, path: string, errors: string[]): Record | null { + if (v === null || typeof v !== "object" || Array.isArray(v)) { + errors.push(`${path}: expected object`); + return null; + } + return v as Record; +} + +function asString( + v: unknown, + path: string, + errors: string[], + opts: { required?: boolean } = {}, +): string | undefined { + if (v === undefined) { + if (opts.required) { + errors.push(`${path}: required`); + } + return undefined; + } + if (typeof v !== "string") { + errors.push(`${path}: expected string`); + return undefined; + } + return v; +} + +function asNumberInRange( + v: unknown, + path: string, + errors: string[], + min: number, + max: number, +): number | undefined { + if (v === undefined) { + return undefined; + } + if (typeof v !== "number" || !Number.isFinite(v)) { + errors.push(`${path}: expected finite number`); + return undefined; + } + if (v < min || v > max) { + errors.push(`${path}: expected ${min}..${max}, got ${v}`); + return undefined; + } + return v; +} + +function asBool(v: unknown, path: string, errors: string[]): boolean | undefined { + if (v === undefined) { + return undefined; + } + if (typeof v !== "boolean") { + errors.push(`${path}: expected boolean`); + return undefined; + } + return v; +} + +function validateDirective( + input: unknown, + path: string, + errors: string[], +): SpriteCoreEmotionDirective | undefined { + const obj = asObject(input, path, errors); + if (!obj) { + return undefined; + } + const out: SpriteCoreEmotionDirective = {}; + const voiceId = asString(obj["voiceId"], `${path}.voiceId`, errors); + if (voiceId !== undefined) { + out.voiceId = voiceId; + } + const stability = asNumberInRange(obj["stability"], `${path}.stability`, errors, 0, 1); + if (stability !== undefined) { + out.stability = stability; + } + const similarity = asNumberInRange(obj["similarity"], `${path}.similarity`, errors, 0, 1); + if (similarity !== undefined) { + out.similarity = similarity; + } + const style = asNumberInRange(obj["style"], `${path}.style`, errors, 0, 1); + if (style !== undefined) { + out.style = style; + } + const speakerBoost = asBool(obj["speakerBoost"], `${path}.speakerBoost`, errors); + if (speakerBoost !== undefined) { + out.speakerBoost = speakerBoost; + } + const speed = asNumberInRange(obj["speed"], `${path}.speed`, errors, 0.25, 4); + if (speed !== undefined) { + out.speed = speed; + } + const audioTag = asString(obj["audioTag"], `${path}.audioTag`, errors); + if (audioTag !== undefined) { + out.audioTag = audioTag; + } + return out; +} + +function validateAvatar( + input: unknown, + path: string, + errors: string[], +): SpriteCoreAvatarConfig | undefined { + // Avatar shapes are operator territory today — accept the raw object if it + // has a known `kind`, otherwise reject. We don't deep-validate the nested + // sprite/atlas shapes here; the next `loadConfig()` call runs openclaw's + // full JSON-schema validator and rejects malformed branches at load. + const obj = asObject(input, path, errors); + if (!obj) { + return undefined; + } + const kind = obj["kind"]; + if (kind !== "states" && kind !== "sprites" && kind !== "atlas") { + errors.push(`${path}.kind: expected "states" | "sprites" | "atlas"`); + return undefined; + } + return obj as unknown as SpriteCoreAvatarConfig; +} + +function validateVoice( + input: unknown, + path: string, + errors: string[], +): SpriteCoreVoiceConfig | undefined { + const obj = asObject(input, path, errors); + if (!obj) { + return undefined; + } + return obj as SpriteCoreVoiceConfig; +} + +function validatePrompting( + input: unknown, + path: string, + errors: string[], +): SpriteCorePromptingConfig | undefined { + const obj = asObject(input, path, errors); + if (!obj) { + return undefined; + } + const out: SpriteCorePromptingConfig = {}; + if (obj["descriptions"] !== undefined) { + const descObj = asObject(obj["descriptions"], `${path}.descriptions`, errors); + if (descObj) { + const descriptions: Record = {}; + for (const [k, v] of Object.entries(descObj)) { + const s = asString(v, `${path}.descriptions.${k}`, errors); + if (s !== undefined) { + descriptions[k] = s; + } + } + out.descriptions = descriptions; + } + } + const instruction = asString(obj["instruction"], `${path}.instruction`, errors); + if (instruction !== undefined) { + out.instruction = instruction; + } + return out; +} + +function validateEmotionsMap( + input: unknown, + path: string, + errors: string[], +): Record | undefined { + const obj = asObject(input, path, errors); + if (!obj) { + return undefined; + } + const out: Record = {}; + for (const [k, v] of Object.entries(obj)) { + const r = validateEmotionEntry(v); + if (!r.ok) { + for (const e of r.errors) { + errors.push(e.replace(/^emotion/, `${path}.${k}`)); + } + continue; + } + out[k] = r.value; + } + return out; +} diff --git a/packages/plugin/ui/index.html b/packages/plugin/ui/index.html new file mode 100644 index 0000000..16c1161 --- /dev/null +++ b/packages/plugin/ui/index.html @@ -0,0 +1,13 @@ + + + + + + + SpriteCore + + +

+ + + diff --git a/packages/plugin/ui/package.json b/packages/plugin/ui/package.json new file mode 100644 index 0000000..e0033db --- /dev/null +++ b/packages/plugin/ui/package.json @@ -0,0 +1,27 @@ +{ + "name": "@tyler-rng/sprite-core-ui", + "version": "1.0.0", + "description": "Dashboard UI for the SpriteCore plugin — browser SPA served by the plugin at /sprite-core/ui", + "type": "module", + "private": true, + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "typecheck": "tsc -b --noEmit", + "preview": "vite preview" + }, + "dependencies": { + "@tanstack/react-query": "^5.51.0", + "@tyler-rng/sprite-core-client": "workspace:*", + "@tyler-rng/sprite-core-schema": "workspace:*", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "typescript": "^5.6.0", + "vite": "^5.4.0" + } +} diff --git a/packages/plugin/ui/src/App.tsx b/packages/plugin/ui/src/App.tsx new file mode 100644 index 0000000..e212a99 --- /dev/null +++ b/packages/plugin/ui/src/App.tsx @@ -0,0 +1,105 @@ +import { useMemo, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { getAgents } from "./api/client.js"; +import { EmotionEditor } from "./components/EmotionEditor.js"; +import { AvatarPreview } from "./preview/AvatarPreview.js"; +import type { AgentEntry } from "./api/types.js"; + +export function App(): JSX.Element { + const agentsQuery = useQuery({ + queryKey: ["agents"], + queryFn: ({ signal }) => getAgents(signal), + }); + const [selectedAgentId, setSelectedAgentId] = useState(null); + + const agents = agentsQuery.data?.agents ?? {}; + const agentIds = useMemo(() => Object.keys(agents).sort(), [agents]); + const activeId = selectedAgentId && agents[selectedAgentId] ? selectedAgentId : agentIds[0] ?? null; + const active: AgentEntry | null = activeId ? agents[activeId] ?? null : null; + + return ( +
+
+

SpriteCore

+ dashboard · plugin UI +
+ +
+ {active && activeId ? ( + + ) : ( +
Select an agent to begin.
+ )} +
+
+ ); +} + +function AgentPane({ agentId, agent }: { agentId: string; agent: AgentEntry }): JSX.Element { + const emotionKeys = useMemo(() => { + const fromEmotions = Object.keys(agent.emotions ?? {}); + const fromAvatar = avatarStates(agent); + // Union: everything that has a description OR is an avatar state, sorted. + return Array.from(new Set([...fromEmotions, ...fromAvatar])).sort(); + }, [agent]); + + return ( + <> +
+

{agentId}

+ {emotionKeys.length === 0 && ( +
+
+ No avatar states or emotions configured for this agent. +
+
+ )} + {emotionKeys.map((state) => ( + + ))} +
+ + + ); +} + +function avatarStates(agent: AgentEntry): string[] { + const avatar = agent.avatar; + if (!avatar) return []; + if (avatar.kind === "states") return Object.keys(avatar.states); + if (avatar.kind === "sprites") return Object.keys(avatar.states); + if (avatar.kind === "atlas") return Object.keys(avatar.descriptions ?? {}); + return []; +} diff --git a/packages/plugin/ui/src/api/client.ts b/packages/plugin/ui/src/api/client.ts new file mode 100644 index 0000000..9fb81a2 --- /dev/null +++ b/packages/plugin/ui/src/api/client.ts @@ -0,0 +1,57 @@ +import type { AgentEntry, AgentsResponse, EmotionEntry } from "./types.js"; + +// The UI is served from the same origin as the plugin routes, so relative +// paths "just work" under `/sprite-core/ui/`. In Vite dev the config proxies +// `/sprite-core/*` to a gateway URL, also matching this base. +const BASE = "/sprite-core"; + +async function parseJson(res: Response): Promise { + const text = await res.text(); + if (!res.ok) { + let message = `${res.status} ${res.statusText}`; + try { + const body = JSON.parse(text); + if (body?.error?.message) { + message = body.error.message; + } + } catch { + if (text) { + message = text; + } + } + throw new Error(message); + } + return text ? (JSON.parse(text) as T) : (undefined as T); +} + +export async function getAgents(signal?: AbortSignal): Promise { + const res = await fetch(`${BASE}/agents`, { signal, credentials: "same-origin" }); + return parseJson(res); +} + +export async function putAgent(agentId: string, entry: AgentEntry): Promise { + const res = await fetch(`${BASE}/agents/${encodeURIComponent(agentId)}`, { + method: "PUT", + credentials: "same-origin", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(entry), + }); + await parseJson<{ ok: true }>(res); +} + +export async function putEmotion( + agentId: string, + state: string, + entry: EmotionEntry, +): Promise { + const res = await fetch( + `${BASE}/agents/${encodeURIComponent(agentId)}/emotions/${encodeURIComponent(state)}`, + { + method: "PUT", + credentials: "same-origin", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(entry), + }, + ); + await parseJson<{ ok: true }>(res); +} diff --git a/packages/plugin/ui/src/api/types.ts b/packages/plugin/ui/src/api/types.ts new file mode 100644 index 0000000..c84b9c1 --- /dev/null +++ b/packages/plugin/ui/src/api/types.ts @@ -0,0 +1,98 @@ +// Mirrors packages/plugin/src/types.ts. Duplicated on purpose: the plugin is +// a server-side package with openclaw imports that don't belong in a browser +// bundle, so we copy only the wire types the UI needs. Keep in sync with the +// source; a lint or test-level sync check is a reasonable future addition. +// +// See: packages/plugin/src/types.ts + +export type AvatarStateEntry = { file: string; description?: string }; +export type AvatarStatesConfig = { + kind: "states"; + default: string; + states: Record; + instruction?: string; +}; + +export type LoopMode = "infinite" | "once" | "ping-pong"; +export type SpriteSequence = { + count: number; + fps?: number; + loop?: LoopMode; + holdLastFrame?: boolean; + iterations?: number; +}; + +export type SpriteStatePhased = { + intro?: SpriteSequence; + loop: SpriteSequence; + outro?: SpriteSequence; + description?: string; +}; + +export type SpriteState = + | (SpriteSequence & { description?: string }) + | SpriteStatePhased; + +export type AvatarTransition = string | { blend: "crossfade"; ms: number }; + +export type AvatarSpritesConfig = { + kind: "sprites"; + default: string; + basePath: string; + format?: "webp" | "png" | "jpg"; + states: Record; + transitions?: Record; + instruction?: string; +}; + +export type AvatarAtlasConfig = { + kind: "atlas"; + default: string; + manifest: string; + descriptions?: Record; + instruction?: string; +}; + +export type AvatarConfig = + | AvatarStatesConfig + | AvatarSpritesConfig + | AvatarAtlasConfig; + +export type VoiceConfig = { + provider?: string; + voiceId?: string; + label?: string; + [key: string]: unknown; +}; + +export type PromptingConfig = { + descriptions?: Record; + instruction?: string; +}; + +export type EmotionDirective = { + voiceId?: string; + stability?: number; + similarity?: number; + style?: number; + speakerBoost?: boolean; + speed?: number; + audioTag?: string; +}; + +export type EmotionEntry = { + description: string; + directive?: EmotionDirective; +}; + +export type AgentEntry = { + avatar?: AvatarConfig; + voice?: VoiceConfig; + prompting?: PromptingConfig; + emotions?: Record; +}; + +export type AgentsResponse = { + agents: Record; + publicBaseUrl?: string; +}; diff --git a/packages/plugin/ui/src/components/EmotionEditor.tsx b/packages/plugin/ui/src/components/EmotionEditor.tsx new file mode 100644 index 0000000..160d40a --- /dev/null +++ b/packages/plugin/ui/src/components/EmotionEditor.tsx @@ -0,0 +1,182 @@ +import { useEffect, useState } from "react"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { putEmotion } from "../api/client.js"; +import type { EmotionDirective, EmotionEntry } from "../api/types.js"; + +type Props = { + agentId: string; + stateName: string; + initial: EmotionEntry; +}; + +/** + * Edit one per-state emotion entry. Writes back to: + * PUT /sprite-core/agents/:agentId/emotions/:state + * + * Kept intentionally narrow — one state at a time — so the PUT is tiny and + * the plugin's read-modify-write stays scoped to a single config branch. + */ +export function EmotionEditor({ agentId, stateName, initial }: Props): JSX.Element { + const qc = useQueryClient(); + const [description, setDescription] = useState(initial.description); + const [directive, setDirective] = useState(initial.directive ?? {}); + + useEffect(() => { + setDescription(initial.description); + setDirective(initial.directive ?? {}); + }, [agentId, stateName, initial]); + + const save = useMutation({ + mutationFn: async () => { + const entry: EmotionEntry = { + description, + ...(hasAny(directive) ? { directive } : {}), + }; + await putEmotion(agentId, stateName, entry); + }, + onSuccess: () => { + void qc.invalidateQueries({ queryKey: ["agents"] }); + }, + }); + + const dirty = + description !== initial.description || + JSON.stringify(directive) !== JSON.stringify(initial.directive ?? {}); + + return ( +
+

+ emotions.{stateName} +

+
+ +