From 39484837d988ef7a09055db7ed9b341029b61a57 Mon Sep 17 00:00:00 2001 From: Shadow Date: Fri, 30 Jan 2026 00:40:00 -0600 Subject: [PATCH 1/9] feat: rewrite resources moderation automod branding --- AGENTS.md | 10 + CHANGELOG.md | 4 +- README.md | 2 +- bun.lock | 507 ++-- convex/_generated/api.d.ts | 6 + convex/automod.ts | 257 ++ convex/comments.ts | 29 +- convex/crons.ts | 7 + convex/devSeed.ts | 43 +- convex/devSeedExtra.ts | 35 +- convex/extensions.ts | 110 + convex/githubBackupsNode.ts | 18 + convex/http.ts | 2 +- convex/httpApi.ts | 2 +- convex/httpApiV1.handlers.test.ts | 2 +- convex/httpApiV1.ts | 5 +- convex/lib/access.ts | 27 +- convex/lib/badges.ts | 40 +- convex/lib/githubBackup.ts | 76 + convex/lib/githubImport.ts | 2 +- convex/lib/moderation.ts | 4 + convex/lib/public.ts | 21 +- convex/lib/resource.ts | 96 + convex/lib/skillPublish.ts | 14 +- convex/lib/skills.ts | 53 +- convex/lib/webhooks.ts | 5 +- convex/maintenance.ts | 992 ++++++-- convex/schema.ts | 141 +- convex/search.ts | 66 +- convex/skillStatEvents.ts | 17 + convex/skills.ts | 807 +++--- convex/soulComments.ts | 35 +- convex/soulDownloads.ts | 18 +- convex/soulStars.ts | 35 +- convex/souls.ts | 178 +- convex/stars.ts | 2 + convex/statsMaintenance.ts | 8 + convex/users.ts | 38 +- docs/architecture.md | 2 +- e2e/clawdhub.e2e.test.ts | 2 +- package.json | 20 +- packages/schema/README.md | 2 +- packages/schema/package.json | 2 +- packages/schema/src/schemas.test.ts | 2 +- packages/schema/src/textFiles.test.ts | 2 +- plan.md | 66 + server/og/fetchSoulOgMeta.ts | 4 +- server/routes/og/skill.png.ts | 6 +- server/routes/og/soul.png.ts | 7 +- src/__tests__/skill-detail-page.test.tsx | 2 +- src/components/Footer.tsx | 26 +- src/components/Header.tsx | 357 ++- src/components/InstallSwitcher.tsx | 24 +- src/components/PageShell.tsx | 22 + src/components/ResourceCard.tsx | 59 + src/components/ResourceDetailShell.tsx | 52 + src/components/ResourceListRow.tsx | 53 + src/components/SectionHeader.tsx | 21 + src/components/SkillCard.tsx | 43 +- src/components/SkillDetailPage.tsx | 822 +++--- src/components/SkillDiffCard.tsx | 382 ++- src/components/SoulCard.tsx | 17 +- src/components/SoulDetailPage.tsx | 245 +- src/components/ui/avatar.tsx | 44 + src/components/ui/badge.tsx | 30 + src/components/ui/button.tsx | 50 + src/components/ui/card.tsx | 54 + src/components/ui/dropdown-menu.tsx | 10 +- src/components/ui/input.tsx | 19 + src/components/ui/label.tsx | 17 + src/components/ui/navigation-menu.tsx | 48 + src/components/ui/scroll-area.tsx | 23 + src/components/ui/select.tsx | 101 + src/components/ui/separator.tsx | 23 + src/components/ui/sheet.tsx | 56 + src/components/ui/skeleton.tsx | 13 + src/components/ui/switch.tsx | 26 + src/components/ui/tabs.tsx | 49 + src/components/ui/textarea.tsx | 18 + src/components/ui/toggle-group.tsx | 9 +- src/components/ui/tooltip-provider.tsx | 3 + src/components/ui/tooltip.tsx | 25 + src/lib/badges.ts | 32 +- src/lib/og.test.ts | 17 +- src/lib/og.ts | 10 +- src/lib/publicUser.ts | 28 +- src/lib/resources.ts | 68 + src/lib/uploadFiles.ts | 2 +- src/lib/uploadUtils.ts | 2 +- src/lib/useAuthStatus.ts | 5 +- src/routeTree.gen.ts | 118 +- src/routes/$owner/$slug.tsx | 57 +- src/routes/admin.tsx | 4 +- src/routes/cli/auth.tsx | 103 +- src/routes/dashboard.tsx | 189 +- src/routes/extensions/$owner/$slug.tsx | 61 + src/routes/extensions/index.tsx | 105 + src/routes/import.tsx | 354 ++- src/routes/index.tsx | 401 +-- src/routes/management.tsx | 529 ---- src/routes/moderation.tsx | 686 +++++ src/routes/settings.tsx | 224 +- src/routes/skills/$owner/$slug.tsx | 57 + src/routes/skills/index.tsx | 305 +-- src/routes/souls/$owner/$slug.tsx | 57 + src/routes/souls/$slug.tsx | 57 +- src/routes/souls/index.tsx | 206 +- src/routes/stars.tsx | 58 +- src/routes/u/$handle.tsx | 370 +-- src/routes/upload.tsx | 360 +-- src/routes/upload/{utils.ts => -utils.ts} | 2 +- src/styles.css | 2786 +-------------------- 112 files changed, 7352 insertions(+), 6473 deletions(-) create mode 100644 convex/automod.ts create mode 100644 convex/extensions.ts create mode 100644 convex/lib/resource.ts create mode 100644 plan.md create mode 100644 src/components/PageShell.tsx create mode 100644 src/components/ResourceCard.tsx create mode 100644 src/components/ResourceDetailShell.tsx create mode 100644 src/components/ResourceListRow.tsx create mode 100644 src/components/SectionHeader.tsx create mode 100644 src/components/ui/avatar.tsx create mode 100644 src/components/ui/badge.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/components/ui/label.tsx create mode 100644 src/components/ui/navigation-menu.tsx create mode 100644 src/components/ui/scroll-area.tsx create mode 100644 src/components/ui/select.tsx create mode 100644 src/components/ui/separator.tsx create mode 100644 src/components/ui/sheet.tsx create mode 100644 src/components/ui/skeleton.tsx create mode 100644 src/components/ui/switch.tsx create mode 100644 src/components/ui/tabs.tsx create mode 100644 src/components/ui/textarea.tsx create mode 100644 src/components/ui/tooltip-provider.tsx create mode 100644 src/components/ui/tooltip.tsx create mode 100644 src/lib/resources.ts create mode 100644 src/routes/extensions/$owner/$slug.tsx create mode 100644 src/routes/extensions/index.tsx delete mode 100644 src/routes/management.tsx create mode 100644 src/routes/moderation.tsx create mode 100644 src/routes/skills/$owner/$slug.tsx create mode 100644 src/routes/souls/$owner/$slug.tsx rename src/routes/upload/{utils.ts => -utils.ts} (97%) diff --git a/AGENTS.md b/AGENTS.md index cff2239..16873e7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,6 +22,7 @@ - Indentation: 2 spaces, single quotes (Biome). - Lint/format: Biome + oxlint (type-aware). - Convex function names: verb-first (`getBySlug`, `publishVersion`). +- Prefer shared resource components/utilities for skills/souls/extensions to avoid duplicated logic. ## Testing Guidelines - Framework: Vitest 4 + jsdom. @@ -43,3 +44,12 @@ - New Convex functions must be pushed before `convex run`: use `bunx convex dev --once` (dev) or `bunx convex deploy` (prod). - For non-interactive prod deploys, use `bunx convex deploy -y` to skip confirmation. - If `bunx convex run --env-file .env.local ...` returns `401 MissingAccessToken` despite `bunx convex login`, workaround: omit `--env-file` and use `--deployment-name ` / `--prod`. + +## Rewrite Notes (Resources, Moderation, Auth) +- Resources are unified in the `resources` table with `type` (`skill`, `soul`, `extension`); type tables keep version-specific fields. +- Shared UI pieces live in `src/components/ResourceCard.tsx`, `ResourceListRow.tsx`, and `ResourceDetailShell.tsx`. +- Moderation uses a reported-only queue + lookup tools; duplicate system removed. Similar-skill lookup uses `skills.findSimilarSkills` (vector search action). +- GitHub backups delete skill folders on hide/delete via `githubBackupsNode.deleteSkillBackupInternal`. +- Local auth bypass: set `AUTH_BYPASS=true` in Convex env + `VITE_AUTH_BYPASS=true` in `.env.local`. +- Local seed script: `bun run seed:local` (calls `devSeed:seedNixSkillsPublic`). +- Card stats labels: stars, downloads, installs (skills only), versions. diff --git a/CHANGELOG.md b/CHANGELOG.md index 73e121e..0136f51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -123,8 +123,8 @@ ### Fixed - CLI sync: wrap note output to avoid terminal overflow; cap list lengths. - CLI sync: label fallback scans as fallback locations. -- CLI package: bundle schema internally (no external `clawhub-schema` publish). -- Repo: mark `clawhub-schema` as private to prevent publishing. +- CLI package: bundle schema internally (no external `molthub-schema` publish). +- Repo: mark `molthub-schema` as private to prevent publishing. ## 0.0.2 - 2026-01-04 diff --git a/README.md b/README.md index 42ae041..6cb6efb 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ onlycrabs.ai: `https://onlycrabs.ai` - Web app: TanStack Start (React, Vite/Nitro). - Backend: Convex (DB + file storage + HTTP actions) + Convex Auth (GitHub OAuth). - Search: OpenAI embeddings (`text-embedding-3-small`) + Convex vector search. -- API schema + routes: `packages/schema` (`clawhub-schema`). +- API schema + routes: `packages/schema` (`molthub-schema`). ## Telemetry diff --git a/bun.lock b/bun.lock index 0e27ff4..91bb24e 100644 --- a/bun.lock +++ b/bun.lock @@ -1,9 +1,9 @@ { "lockfileVersion": 1, - "configVersion": 1, + "configVersion": 0, "workspaces": { "": { - "name": "clawhub", + "name": "molthub", "dependencies": { "@auth/core": "^0.37.4", "@convex-dev/auth": "^0.0.90", @@ -11,8 +11,18 @@ "@fontsource/ibm-plex-mono": "^5.2.7", "@fontsource/manrope": "^5.2.8", "@monaco-editor/react": "^4.7.0", + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.3", + "@radix-ui/react-navigation-menu": "^1.2.8", + "@radix-ui/react-scroll-area": "^1.2.4", + "@radix-ui/react-select": "^2.1.12", + "@radix-ui/react-separator": "^1.1.3", + "@radix-ui/react-switch": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.6", "@radix-ui/react-toggle-group": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.4", "@resvg/resvg-wasm": "^2.6.2", "@tailwindcss/vite": "^4.1.18", "@tanstack/react-devtools": "^0.9.2", @@ -21,13 +31,14 @@ "@tanstack/react-start": "^1.152.0", "@tanstack/router-plugin": "^1.151.6", "@vercel/analytics": "^1.6.1", - "clawhub-schema": "workspace:*", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "convex": "^1.31.6", "convex-helpers": "^0.1.111", "fflate": "^0.8.2", "h3": "2.0.1-rc.8", "lucide-react": "^0.562.0", + "molthub-schema": "workspace:*", "monaco-editor": "^0.55.1", "nitro": "^3.0.1-alpha.1", "react": "^19.2.3", @@ -87,7 +98,7 @@ }, }, "packages/schema": { - "name": "clawhub-schema", + "name": "molthub-schema", "version": "0.0.2", "dependencies": { "arktype": "^2.1.29", @@ -98,7 +109,7 @@ }, }, "packages": { - "@acemir/cssom": ["@acemir/cssom@0.9.31", "", {}, "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA=="], + "@acemir/cssom": ["@acemir/cssom@0.9.30", "", {}, "sha512-9CnlMCI0LmCIq0olalQqdWrJHPzm0/tw3gzOA9zJSgvFX7Xau3D24mAGa4BtwxwY69nsuJW6kQqqCzf/mEcQgg=="], "@ark/schema": ["@ark/schema@0.56.0", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA=="], @@ -112,23 +123,23 @@ "@auth/core": ["@auth/core@0.37.4", "", { "dependencies": { "@panva/hkdf": "^1.2.1", "jose": "^5.9.6", "oauth4webapi": "^3.1.1", "preact": "10.24.3", "preact-render-to-string": "6.5.11" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "nodemailer": "^6.8.0" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-HOXJwXWXQRhbBDHlMU0K/6FT1v+wjtzdKhsNg0ZN7/gne6XPsIrjZ4daMcFnbq0Z/vsAbYBinQhhua0d77v7qw=="], - "@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], - "@babel/compat-data": ["@babel/compat-data@7.28.6", "", {}, "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg=="], + "@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="], - "@babel/core": ["@babel/core@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw=="], + "@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="], - "@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + "@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], - "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], - "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="], "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], @@ -136,45 +147,45 @@ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], - "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], + "@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="], - "@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + "@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], - "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w=="], - "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A=="], + "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ=="], "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], - "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], - "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], - "@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], + "@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], - "@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], - "@biomejs/biome": ["@biomejs/biome@2.3.13", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.13", "@biomejs/cli-darwin-x64": "2.3.13", "@biomejs/cli-linux-arm64": "2.3.13", "@biomejs/cli-linux-arm64-musl": "2.3.13", "@biomejs/cli-linux-x64": "2.3.13", "@biomejs/cli-linux-x64-musl": "2.3.13", "@biomejs/cli-win32-arm64": "2.3.13", "@biomejs/cli-win32-x64": "2.3.13" }, "bin": { "biome": "bin/biome" } }, "sha512-Fw7UsV0UAtWIBIm0M7g5CRerpu1eKyKAXIazzxhbXYUyMkwNrkX/KLkGI7b+uVDQ5cLUMfOC9vR60q9IDYDstA=="], + "@biomejs/biome": ["@biomejs/biome@2.3.11", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.11", "@biomejs/cli-darwin-x64": "2.3.11", "@biomejs/cli-linux-arm64": "2.3.11", "@biomejs/cli-linux-arm64-musl": "2.3.11", "@biomejs/cli-linux-x64": "2.3.11", "@biomejs/cli-linux-x64-musl": "2.3.11", "@biomejs/cli-win32-arm64": "2.3.11", "@biomejs/cli-win32-x64": "2.3.11" }, "bin": { "biome": "bin/biome" } }, "sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0OCwP0/BoKzyJHnFdaTk/i7hIP9JHH9oJJq6hrSCPmJPo8JWcJhprK4gQlhFzrwdTBAW4Bjt/RmCf3ZZe59gwQ=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-AGr8OoemT/ejynbIu56qeil2+F2WLkIjn2d8jGK1JkchxnMUhYOfnqc9sVzcRxpG9Ycvw4weQ5sprRvtb7Yhcw=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-xvOiFkrDNu607MPMBUQ6huHmBG1PZLOrqhtK6pXJW3GjfVqJg0Z/qpTdhXfcqWdSZHcT+Nct2fOgewZvytESkw=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-TUdDCSY+Eo/EHjhJz7P2GnWwfqet+lFxBZzGHldrvULr59AgahamLs/N85SC4+bdF86EhqDuuw9rYLvLFWWlXA=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.13", "", { "os": "linux", "cpu": "x64" }, "sha512-s+YsZlgiXNq8XkgHs6xdvKDFOj/bwTEevqEY6rC2I3cBHbxXYU1LOZstH3Ffw9hE5tE1sqT7U23C00MzkXztMw=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.13", "", { "os": "linux", "cpu": "x64" }, "sha512-0bdwFVSbbM//Sds6OjtnmQGp4eUjOTt6kHvR/1P0ieR9GcTUAlPNvPC3DiavTqq302W34Ae2T6u5VVNGuQtGlQ=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-QweDxY89fq0VvrxME+wS/BXKmqMrOTZlN9SqQ79kQSIc3FrEwvW/PvUegQF6XIVaekncDykB5dzPqjbwSKs9DA=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.13", "", { "os": "win32", "cpu": "x64" }, "sha512-trDw2ogdM2lyav9WFQsdsfdVy1dvZALymRpgmWsvSez0BJzBjulhOT/t+wyKeh3pZWvwP3VMs1SoOKwO3wecMQ=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.11", "", { "os": "win32", "cpu": "x64" }, "sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg=="], "@clack/core": ["@clack/core@0.5.0", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow=="], @@ -190,13 +201,13 @@ "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.5", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ=="], - "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.0.26", "", {}, "sha512-6boXK0KkzT5u5xOgF6TKB+CLq9SOpEGmkZw0g5n9/7yg85wab3UzSxB8TxhLJ31L4SGJ6BCFRw/iftTha1CJXA=="], + "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.0.22", "", {}, "sha512-qBcx6zYlhleiFfdtzkRgwNC7VVoAwfK76Vmsw5t+PbvtdknO9StgRk7ROvq9so1iqbdW4uLIDAsXRsTfUrIoOw=="], "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="], - "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], + "@emnapi/core": ["@emnapi/core@1.8.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-ryJnSmj4UhrGLZZPJ6PKVb4wNPAIkW6iyLy+0TRwazd3L1u0wzMe8RfqevAh2HbcSkoeLiSYnOVDOys4JSGYyg=="], - "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], + "@emnapi/runtime": ["@emnapi/runtime@1.8.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Z82FDl1ByxqPEPrAYYeTQVlx2FSHPe1qwX465c+96IRS3fTdSYRoJcRxg3g2fEG5I69z1dSEWQlNRRr0/677mg=="], "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], @@ -252,13 +263,13 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], - "@exodus/bytes": ["@exodus/bytes@1.10.0", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-tf8YdcbirXdPnJ+Nd4UN1EXnz+IP2DI45YVEr3vvzcVTOyrApkmIB4zvOQVd3XPr7RXnfBtAx+PXImXOIU0Ajg=="], + "@exodus/bytes": ["@exodus/bytes@1.8.0", "", { "peerDependencies": { "@exodus/crypto": "^1.0.0-rc.4" }, "optionalPeers": ["@exodus/crypto"] }, "sha512-8JPn18Bcp8Uo1T82gR8lh2guEOa5KKU/IEKvvdp0sgmi7coPBWf1Doi1EXsGZb2ehc8ym/StJCjffYV+ne7sXQ=="], - "@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="], + "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="], - "@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="], + "@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="], - "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.7", "", { "dependencies": { "@floating-ui/dom": "^1.7.5" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg=="], + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.6", "", { "dependencies": { "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw=="], "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], @@ -300,127 +311,113 @@ "@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="], - "@oxc-minify/binding-android-arm-eabi": ["@oxc-minify/binding-android-arm-eabi@0.110.0", "", { "os": "android", "cpu": "arm" }, "sha512-43fMTO8/5bMlqfOiNSZNKUzIqeLIYuB9Hr1Ohyf58B1wU11S2dPGibTXOGNaWsfgHy99eeZ1bSgeIHy/fEYqbw=="], - - "@oxc-minify/binding-android-arm64": ["@oxc-minify/binding-android-arm64@0.110.0", "", { "os": "android", "cpu": "arm64" }, "sha512-5oQrnn9eK/ccOp80PTrNj0Vq893NPNNRryjGpOIVsYNgWFuoGCfpnKg68oEFcN8bArizYAqw4nvgHljEnar69w=="], - - "@oxc-minify/binding-darwin-arm64": ["@oxc-minify/binding-darwin-arm64@0.110.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dqBDgTG9tF2z2lrZp9E8wU+Godz1i8gCGSei2eFKS2hRploBOD5dmOLp1j4IMornkPvSQmbwB3uSjPq7fjx4EA=="], - - "@oxc-minify/binding-darwin-x64": ["@oxc-minify/binding-darwin-x64@0.110.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U0AqabqaooDOpYmeeOye8wClv8PSScELXgOfYqyqgrwH9J9KrpCE1jL8Rlqgz68QbL4mPw3V6sKiiHssI4CLeQ=="], - - "@oxc-minify/binding-freebsd-x64": ["@oxc-minify/binding-freebsd-x64@0.110.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-H0w8o/Wo1072WSdLfhwwrpFpwZnPpjQODlHuRYkTfsSSSJbTxQtjJd4uxk7YJsRv5RQp69y0I7zvdH6f8Xueyw=="], - - "@oxc-minify/binding-linux-arm-gnueabihf": ["@oxc-minify/binding-linux-arm-gnueabihf@0.110.0", "", { "os": "linux", "cpu": "arm" }, "sha512-qd6sW0AvEVYZhbVVMGtmKZw3b1zDYGIW+54Uh42moWRAj6i4Jhk/LGr6r9YNZpOINeuvZfkFuEeDD/jbu7xPUA=="], - - "@oxc-minify/binding-linux-arm-musleabihf": ["@oxc-minify/binding-linux-arm-musleabihf@0.110.0", "", { "os": "linux", "cpu": "arm" }, "sha512-7WXP0aXMrWSn0ScppUBi3jf68ebfBG0eri8kxLmBOVSBj6jw1repzkHMITJMBeLr5d0tT/51qFEptiAk2EP2iA=="], - - "@oxc-minify/binding-linux-arm64-gnu": ["@oxc-minify/binding-linux-arm64-gnu@0.110.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-LYfADrq5x1W5gs+u9OIbMbDQNYkAECTXX0ufnAuf3oGmO51rF98kGFR5qJqC/6/csokDyT3wwTpxhE0TkcF/Og=="], - - "@oxc-minify/binding-linux-arm64-musl": ["@oxc-minify/binding-linux-arm64-musl@0.110.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-53GjCVY8kvymk9P6qNDh6zyblcehF5QHstq9QgCjv13ONGRnSHjeds0PxIwiihD7h295bxsWs84DN39syLPH4Q=="], + "@oxc-minify/binding-android-arm64": ["@oxc-minify/binding-android-arm64@0.96.0", "", { "os": "android", "cpu": "arm64" }, "sha512-lzeIEMu/v6Y+La5JSesq4hvyKtKBq84cgQpKYTYM/yGuNk2tfd5Ha31hnC+mTh48lp/5vZH+WBfjVUjjINCfug=="], - "@oxc-minify/binding-linux-ppc64-gnu": ["@oxc-minify/binding-linux-ppc64-gnu@0.110.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-li8XcN81dxbJDMBESnTgGhoiAQ+CNIdM0QGscZ4duVPjCry1RpX+5FJySFbGqG3pk4s9ZzlL/vtQtbRzZIZOzg=="], + "@oxc-minify/binding-darwin-arm64": ["@oxc-minify/binding-darwin-arm64@0.96.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-i0LkJAUXb4BeBFrJQbMKQPoxf8+cFEffDyLSb7NEzzKuPcH8qrVsnEItoOzeAdYam8Sr6qCHVwmBNEQzl7PWpw=="], - "@oxc-minify/binding-linux-riscv64-gnu": ["@oxc-minify/binding-linux-riscv64-gnu@0.110.0", "", { "os": "linux", "cpu": "none" }, "sha512-SweKfsnLKShu6UFV8mwuj1d1wmlNoL/FlAxPUzwjEBgwiT2HQkY24KnjBH+TIA+//1O83kzmWKvvs4OuEhdIEQ=="], + "@oxc-minify/binding-darwin-x64": ["@oxc-minify/binding-darwin-x64@0.96.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-C5vI0WPR+KPIFAD5LMOJk2J8iiT+Nv65vDXmemzXEXouzfEOLYNqnW+u6NSsccpuZHHWAiLyPFkYvKFduveAUQ=="], - "@oxc-minify/binding-linux-riscv64-musl": ["@oxc-minify/binding-linux-riscv64-musl@0.110.0", "", { "os": "linux", "cpu": "none" }, "sha512-oH8G4aFMP8XyTsEpdANC5PQyHgSeGlopHZuW1rpyYcaErg5YaK0vXjQ4EM5HVvPm+feBV24JjxgakTnZoF3aOQ=="], + "@oxc-minify/binding-freebsd-x64": ["@oxc-minify/binding-freebsd-x64@0.96.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-3//5DNx+xUjVBMLLk2sl6hfe4fwfENJtjVQUBXjxzwPuv8xgZUqASG4cRG3WqG5Qe8dV6SbCI4EgKQFjO4KCZA=="], - "@oxc-minify/binding-linux-s390x-gnu": ["@oxc-minify/binding-linux-s390x-gnu@0.110.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-W9na+Vza7XVUlpf8wMt4QBfH35KeTENEmnpPUq3NSlbQHz8lSlSvhAafvo43NcKvHAXV3ckD/mUf2VkqSdbklg=="], + "@oxc-minify/binding-linux-arm-gnueabihf": ["@oxc-minify/binding-linux-arm-gnueabihf@0.96.0", "", { "os": "linux", "cpu": "arm" }, "sha512-WXChFKV7VdDk1NePDK1J31cpSvxACAVztJ7f7lJVYBTkH+iz5D0lCqPcE7a9eb7nC3xvz4yk7DM6dA9wlUQkQg=="], - "@oxc-minify/binding-linux-x64-gnu": ["@oxc-minify/binding-linux-x64-gnu@0.110.0", "", { "os": "linux", "cpu": "x64" }, "sha512-XJdA4mmmXOjJxSRgNJXsDP7Xe8h3gQhmb56hUcCrvq5d+h5UcEi2pR8rxsdIrS8QmkLuBA3eHkGK8E27D7DTgQ=="], + "@oxc-minify/binding-linux-arm-musleabihf": ["@oxc-minify/binding-linux-arm-musleabihf@0.96.0", "", { "os": "linux", "cpu": "arm" }, "sha512-7B18glYMX4Z/YoqgE3VRLs/2YhVLxlxNKSgrtsRpuR8xv58xca+hEhiFwZN1Rn+NSMZ29Z33LWD7iYWnqYFvRA=="], - "@oxc-minify/binding-linux-x64-musl": ["@oxc-minify/binding-linux-x64-musl@0.110.0", "", { "os": "linux", "cpu": "x64" }, "sha512-QqzvALuOTtSckI8x467R4GNArzYDb/yEh6aNzLoeaY1O7vfT7SPDwlOEcchaTznutpeS9Dy8gUS/AfqtUHaufw=="], + "@oxc-minify/binding-linux-arm64-gnu": ["@oxc-minify/binding-linux-arm64-gnu@0.96.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Yl+KcTldsEJNcaYxxonwAXZ2q3gxIzn3kXYQWgKWdaGIpNhOCWqF+KE5WLsldoh5Ro5SHtomvb8GM6cXrIBMog=="], - "@oxc-minify/binding-openharmony-arm64": ["@oxc-minify/binding-openharmony-arm64@0.110.0", "", { "os": "none", "cpu": "arm64" }, "sha512-gAMssLs2Q3+uhLZxanh1DF+27Kaug3cf4PXb9AB7XK81DR+LVcKySXaoGYoOs20Co0fFSphd6rRzKge2qDK3dA=="], + "@oxc-minify/binding-linux-arm64-musl": ["@oxc-minify/binding-linux-arm64-musl@0.96.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-rNqoFWOWaxwMmUY5fspd/h5HfvgUlA3sv9CUdA2MpnHFiyoJNovR7WU8tGh+Yn0qOAs0SNH0a05gIthHig14IA=="], - "@oxc-minify/binding-wasm32-wasi": ["@oxc-minify/binding-wasm32-wasi@0.110.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-7Wqi5Zjl022bs2zXq+ICdalDPeDuCH/Nhbi8q2isLihAonMVIT0YH2hqqnNEylRNGYck+FJ6gRZwMpGCgrNxPg=="], + "@oxc-minify/binding-linux-riscv64-gnu": ["@oxc-minify/binding-linux-riscv64-gnu@0.96.0", "", { "os": "linux", "cpu": "none" }, "sha512-3paajIuzGnukHwSI3YBjYVqbd72pZd8NJxaayaNFR0AByIm8rmIT5RqFXbq8j2uhtpmNdZRXiu0em1zOmIScWA=="], - "@oxc-minify/binding-win32-arm64-msvc": ["@oxc-minify/binding-win32-arm64-msvc@0.110.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-ZPx+0Tj4dqn41ecyoGotlvekQKy6JxJCixn9Rw7h/dafZ3eDuBcEVh3c2ZoldXXsyMIt5ywI8IWzFZsjNedd5Q=="], + "@oxc-minify/binding-linux-s390x-gnu": ["@oxc-minify/binding-linux-s390x-gnu@0.96.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-9ESrpkB2XG0lQ89JlsxlZa86iQCOs+jkDZLl6O+u5wb7ynUy21bpJJ1joauCOSYIOUlSy3+LbtJLiqi7oSQt5Q=="], - "@oxc-minify/binding-win32-ia32-msvc": ["@oxc-minify/binding-win32-ia32-msvc@0.110.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-H0Oyd3RWBfpEyvJIrFK94RYiY7KKSQl11Ym7LMDwLEagelIAfRCkt1amHZhFa/S3ZRoaOJFXzEw4YKeSsjVFsg=="], + "@oxc-minify/binding-linux-x64-gnu": ["@oxc-minify/binding-linux-x64-gnu@0.96.0", "", { "os": "linux", "cpu": "x64" }, "sha512-UMM1jkns+p+WwwmdjC5giI3SfR2BCTga18x3C0cAu6vDVf4W37uTZeTtSIGmwatTBbgiq++Te24/DE0oCdm1iQ=="], - "@oxc-minify/binding-win32-x64-msvc": ["@oxc-minify/binding-win32-x64-msvc@0.110.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Hr3nK90+qXKJ2kepXwFIcNfQQIOBecB4FFCyaMMypthoEEhVP08heRynj4eSXZ8NL9hLjs3fQzH8PJXfpznRnQ=="], + "@oxc-minify/binding-linux-x64-musl": ["@oxc-minify/binding-linux-x64-musl@0.96.0", "", { "os": "linux", "cpu": "x64" }, "sha512-8b1naiC7MdP7xeMi7cQ5tb9W1rZAP9Qz/jBRqp1Y5EOZ1yhSGnf1QWuZ/0pCc+XiB9vEHXEY3Aki/H+86m2eOg=="], - "@oxc-transform/binding-android-arm-eabi": ["@oxc-transform/binding-android-arm-eabi@0.110.0", "", { "os": "android", "cpu": "arm" }, "sha512-sE9dxvqqAax1YYJ3t7j+h5ZSI9jl6dYuDfngl6ieZUrIy5P89/8JKVgAzgp8o3wQSo7ndpJvYsi1K4ZqrmbP7w=="], + "@oxc-minify/binding-wasm32-wasi": ["@oxc-minify/binding-wasm32-wasi@0.96.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.0.7" }, "cpu": "none" }, "sha512-bjGDjkGzo3GWU9Vg2qiFUrfoo5QxojPNV/2RHTlbIB5FWkkV4ExVjsfyqihFiAuj0NXIZqd2SAiEq9htVd3RFw=="], - "@oxc-transform/binding-android-arm64": ["@oxc-transform/binding-android-arm64@0.110.0", "", { "os": "android", "cpu": "arm64" }, "sha512-nqtbP4aMCtsCZ6qpHlHaQoWVHSBtlKzwaAgwEOvR+9DWqHjk31BHvpGiDXlMeed6CVNpl3lCbWgygb3RcSjcfw=="], + "@oxc-minify/binding-win32-arm64-msvc": ["@oxc-minify/binding-win32-arm64-msvc@0.96.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-4L4DlHUT47qMWQuTyUghpncR3NZHWtxvd0G1KgSjVgXf+cXzFdWQCWZZtCU0yrmOoVCNUf4S04IFCJyAe+Ie7A=="], - "@oxc-transform/binding-darwin-arm64": ["@oxc-transform/binding-darwin-arm64@0.110.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oeSeHnL4Z4cMXtc8V0/rwoVn0dgwlS9q0j6LcHn9dIhtFEdp3W0iSBF8YmMQA+E7sILeLDjsHmHE4Kp0sOScXw=="], + "@oxc-minify/binding-win32-x64-msvc": ["@oxc-minify/binding-win32-x64-msvc@0.96.0", "", { "os": "win32", "cpu": "x64" }, "sha512-T2ijfqZLpV2bgGGocXV4SXTuMoouqN0asYTIm+7jVOLvT5XgDogf3ZvCmiEnSWmxl21+r5wHcs8voU2iUROXAg=="], - "@oxc-transform/binding-darwin-x64": ["@oxc-transform/binding-darwin-x64@0.110.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-nL9K5x7OuZydobAGPylsEW9d4APs2qEkIBLMgQPA+kY8dtVD3IR87QsTbs4l4DBQYyun/+ay6qVCDlxqxdX2Jg=="], + "@oxc-transform/binding-android-arm64": ["@oxc-transform/binding-android-arm64@0.96.0", "", { "os": "android", "cpu": "arm64" }, "sha512-wOm+ZsqFvyZ7B9RefUMsj0zcXw77Z2pXA51nbSQyPXqr+g0/pDGxriZWP8Sdpz/e4AEaKPA9DvrwyOZxu7GRDQ=="], - "@oxc-transform/binding-freebsd-x64": ["@oxc-transform/binding-freebsd-x64@0.110.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-GS29zXXirDQhZEUq8xKJ1azAWMuUy3Ih3W5Bc5ddk12LRthO5wRLFcKIyeHpAXCoXymQ+LmxbMtbPf84GPxouw=="], + "@oxc-transform/binding-darwin-arm64": ["@oxc-transform/binding-darwin-arm64@0.96.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-td1sbcvzsyuoNRiNdIRodPXRtFFwxzPpC/6/yIUtRRhKn30XQcizxupIvQQVpJWWchxkphbBDh6UN+u+2CJ8Zw=="], - "@oxc-transform/binding-linux-arm-gnueabihf": ["@oxc-transform/binding-linux-arm-gnueabihf@0.110.0", "", { "os": "linux", "cpu": "arm" }, "sha512-glzDHak8ISyZJemCUi7RCvzNSl+MQ1ly9RceT2qRufhUsvNZ4C/2QLJ1HJwd2N6E88bO4laYn+RofdRzNnGGEA=="], + "@oxc-transform/binding-darwin-x64": ["@oxc-transform/binding-darwin-x64@0.96.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-xgqxnqhPYH2NYkgbqtnCJfhbXvxIf/pnhF/ig5UBK8PYpCEWIP/cfLpQRQ9DcQnRfuxi7RMIF6LdmB1AiS6Fkg=="], - "@oxc-transform/binding-linux-arm-musleabihf": ["@oxc-transform/binding-linux-arm-musleabihf@0.110.0", "", { "os": "linux", "cpu": "arm" }, "sha512-8JThvgJ2FRoTVfbp7e4wqeZqCZbtudM06SfZmNzND9kPNu/LVYygIR+72RWs+xm4bWkuYHg/islo/boNPtMT5Q=="], + "@oxc-transform/binding-freebsd-x64": ["@oxc-transform/binding-freebsd-x64@0.96.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1i67OXdl/rvSkcTXqDlh6qGRXYseEmf0rl/R+/i88scZ/o3A+FzlX56sThuaPzSSv9eVgesnoYUjIBJELFc1oA=="], - "@oxc-transform/binding-linux-arm64-gnu": ["@oxc-transform/binding-linux-arm64-gnu@0.110.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-IRh21Ub/g4bkHoErZ0AUWMlWfoZaS0A6EaOVtbcY70RSYIMlrsbjiFwJCzM+b/1DD1rXbH5tsGcH7GweTbfRqg=="], + "@oxc-transform/binding-linux-arm-gnueabihf": ["@oxc-transform/binding-linux-arm-gnueabihf@0.96.0", "", { "os": "linux", "cpu": "arm" }, "sha512-9MJBs0SWODsqyzO3eAnacXgJ/sZu1xqinjEwBzkcZ3tQI8nKhMADOzu2NzbVWDWujeoC8DESXaO08tujvUru+Q=="], - "@oxc-transform/binding-linux-arm64-musl": ["@oxc-transform/binding-linux-arm64-musl@0.110.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-e5JN94/oy+wevk76q+LMr+2klTTcO60uXa+Wkq558Ms7mdF2TvkKFI++d/JeiuIwJLTi/BxQ4qdT5FWcsHM/ug=="], + "@oxc-transform/binding-linux-arm-musleabihf": ["@oxc-transform/binding-linux-arm-musleabihf@0.96.0", "", { "os": "linux", "cpu": "arm" }, "sha512-BQom57I2ScccixljNYh2Wy+5oVZtF1LXiiUPxSLtDHbsanpEvV/+kzCagQpTjk1BVzSQzOxfEUWjvL7mY53pRQ=="], - "@oxc-transform/binding-linux-ppc64-gnu": ["@oxc-transform/binding-linux-ppc64-gnu@0.110.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Y3/Tnnz1GvDpmv8FXBIKtdZPsdZklOEPdrL6NHrN5i2u54BOkybFaDSptgWF53wOrJlTrcmAVSE6fRKK9XCM2Q=="], + "@oxc-transform/binding-linux-arm64-gnu": ["@oxc-transform/binding-linux-arm64-gnu@0.96.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-kaqvUzNu8LL4aBSXqcqGVLFG13GmJEplRI2+yqzkgAItxoP/LfFMdEIErlTWLGyBwd0OLiNMHrOvkcCQRWadVg=="], - "@oxc-transform/binding-linux-riscv64-gnu": ["@oxc-transform/binding-linux-riscv64-gnu@0.110.0", "", { "os": "linux", "cpu": "none" }, "sha512-Y0E35iA9/v9jlkNcP6tMJ+ZFOS0rLsWDqG6rU9z+X2R3fBFJBO9UARIK6ngx8upxk81y1TFR2CmBFhupfYdH6Q=="], + "@oxc-transform/binding-linux-arm64-musl": ["@oxc-transform/binding-linux-arm64-musl@0.96.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EiG/L3wEkPgTm4p906ufptyblBgtiQWTubGg/JEw82f8uLRroayr5zhbUqx40EgH037a3SfJthIyLZi7XPRFJw=="], - "@oxc-transform/binding-linux-riscv64-musl": ["@oxc-transform/binding-linux-riscv64-musl@0.110.0", "", { "os": "linux", "cpu": "none" }, "sha512-JOUSYFfHjBUs7xp2FHmZHb8eTYD/oEu0NklS6JgUauqnoXZHiTLPLVW2o2uVCqldnabYHcomuwI2iqVFYJNhTw=="], + "@oxc-transform/binding-linux-riscv64-gnu": ["@oxc-transform/binding-linux-riscv64-gnu@0.96.0", "", { "os": "linux", "cpu": "none" }, "sha512-r01CY6OxKGtVeYnvH4mGmtkQMlLkXdPWWNXwo5o7fE2s/fgZPMpqh8bAuXEhuMXipZRJrjxTk1+ZQ4KCHpMn3Q=="], - "@oxc-transform/binding-linux-s390x-gnu": ["@oxc-transform/binding-linux-s390x-gnu@0.110.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-7blgoXF9D3Ngzb7eun23pNrHJpoV/TtE6LObwlZ3Nmb4oZ6Z+yMvBVaoW68NarbmvNGfZ95zrOjgm6cVETLYBA=="], + "@oxc-transform/binding-linux-s390x-gnu": ["@oxc-transform/binding-linux-s390x-gnu@0.96.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-4djg2vYLGbVeS8YiA2K4RPPpZE4fxTGCX5g/bOMbCYyirDbmBAIop4eOAj8vOA9i1CcWbDtmp+PVJ1dSw7f3IQ=="], - "@oxc-transform/binding-linux-x64-gnu": ["@oxc-transform/binding-linux-x64-gnu@0.110.0", "", { "os": "linux", "cpu": "x64" }, "sha512-YQ2joGWCVDZVEU2cD/r/w49hVjDm/Qu1BvC/7zs8LvprzdLS/HyMXGF2oA0puw0b+AqgYaz3bhwKB2xexHyITQ=="], + "@oxc-transform/binding-linux-x64-gnu": ["@oxc-transform/binding-linux-x64-gnu@0.96.0", "", { "os": "linux", "cpu": "x64" }, "sha512-f6pcWVz57Y8jXa2OS7cz3aRNuks34Q3j61+3nQ4xTE8H1KbalcEvHNmM92OEddaJ8QLs9YcE0kUC6eDTbY34+A=="], - "@oxc-transform/binding-linux-x64-musl": ["@oxc-transform/binding-linux-x64-musl@0.110.0", "", { "os": "linux", "cpu": "x64" }, "sha512-fkjr5qE632ULmNgvFXWDR/8668WxERz3tU7TQFp6JebPBneColitjSkdx6VKNVXEoMmQnOvBIGeP5tUNT384oA=="], + "@oxc-transform/binding-linux-x64-musl": ["@oxc-transform/binding-linux-x64-musl@0.96.0", "", { "os": "linux", "cpu": "x64" }, "sha512-NSiRtFvR7Pbhv3mWyPMkTK38czIjcnK0+K5STo3CuzZRVbX1TM17zGdHzKBUHZu7v6IQ6/XsQ3ELa1BlEHPGWQ=="], - "@oxc-transform/binding-openharmony-arm64": ["@oxc-transform/binding-openharmony-arm64@0.110.0", "", { "os": "none", "cpu": "arm64" }, "sha512-HWH9Zj+lMrdSTqFRCZsvDWMz7OnMjbdGsm3xURXWfRZpuaz0bVvyuZNDQXc4FyyhRDsemICaJbU1bgeIpUJDGw=="], + "@oxc-transform/binding-wasm32-wasi": ["@oxc-transform/binding-wasm32-wasi@0.96.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.0.7" }, "cpu": "none" }, "sha512-A91ARLiuZHGN4hBds9s7bW3czUuLuHLsV+cz44iF9j8e1zX9m2hNGXf/acQRbg/zcFUXmjz5nmk8EkZyob876w=="], - "@oxc-transform/binding-wasm32-wasi": ["@oxc-transform/binding-wasm32-wasi@0.110.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-ejdxHmYfIcHDPhZUe3WklViLt9mDEJE5BzcW7+R1vc5i/5JFA8D0l7NUSsHBJ7FB8Bu9gF+5iMDm6cXGAgaghw=="], + "@oxc-transform/binding-win32-arm64-msvc": ["@oxc-transform/binding-win32-arm64-msvc@0.96.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-IedJf40djKgDObomhYjdRAlmSYUEdfqX3A3M9KfUltl9AghTBBLkTzUMA7O09oo71vYf5TEhbFM7+Vn5vqw7AQ=="], - "@oxc-transform/binding-win32-arm64-msvc": ["@oxc-transform/binding-win32-arm64-msvc@0.110.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-9VTwpXCZs7xkV+mKhQ62dVk7KLnLXtEUxNS2T4nLz3iMl1IJbA4h5oltK0JoobtiUAnbkV53QmMVGW8+Nh3bDQ=="], + "@oxc-transform/binding-win32-x64-msvc": ["@oxc-transform/binding-win32-x64-msvc@0.96.0", "", { "os": "win32", "cpu": "x64" }, "sha512-0fI0P0W7bSO/GCP/N5dkmtB9vBqCA4ggo1WmXTnxNJVmFFOtcA1vYm1I9jl8fxo+sucW2WnlpnI4fjKdo3JKxA=="], - "@oxc-transform/binding-win32-ia32-msvc": ["@oxc-transform/binding-win32-ia32-msvc@0.110.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-5y0fzuNON7/F2hh2P94vANFaRPJ/3DI1hVl5rseCT8VUVqOGIjWaza0YS/D1g6t1WwycW2LWDMi2raOKoWU5GQ=="], + "@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/darwin-arm64@0.11.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UJIOFeJZpFTJIGS+bMdFXcvjslvnXBEouMvzynfQD7RTazcFIRLbokYgEbhrN2P6B352Ut1TUtvR0CLAp/9QfA=="], - "@oxc-transform/binding-win32-x64-msvc": ["@oxc-transform/binding-win32-x64-msvc@0.110.0", "", { "os": "win32", "cpu": "x64" }, "sha512-QROrowwlrApI1fEScMknGWKM6GTM/Z2xwMnDqvSaEmzNazBsDUlE08Jasw610hFEsYAVU2K5sp/YaCa9ORdP4A=="], + "@oxlint-tsgolint/darwin-x64": ["@oxlint-tsgolint/darwin-x64@0.11.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-68O8YvexIm+ISZKl2vBFII1dMfLrteDyPcuCIecDuiBIj2tV0KYq13zpSCMz4dvJUWJW6RmOOGZKrkkvOAy6uQ=="], - "@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/darwin-arm64@0.11.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-FU4e+w09D+2rkCVdL7I7zMuQOJ2tuapVhBGPGY66VAct2FUwFDVmgU+rNJ2hHIdc9uHg24v+FD8PcfFYpask8Q=="], + "@oxlint-tsgolint/linux-arm64": ["@oxlint-tsgolint/linux-arm64@0.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-hXBInrFxPNbPPbPQYozo8YpSsFFYdtHBWRUiLMxul71vTy1CdSA7H5Qq2KbrKomr/ASmhvIDVAQZxh9hIJNHMA=="], - "@oxlint-tsgolint/darwin-x64": ["@oxlint-tsgolint/darwin-x64@0.11.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-7sm1d920HfFsC3hIP7SJVm11WhYufA8qnLQQVk7odTpSzVUAT1jtG8LdfFigzgb38zHszQbsqJ7OjAgIW/OgmA=="], + "@oxlint-tsgolint/linux-x64": ["@oxlint-tsgolint/linux-x64@0.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-aMaGctlwrJhaIQPOdVJR+AGHZGPm4D1pJ457l0SqZt4dLXAhuUt2ene6cUUGF+864R7bDyFVGZqbZHODYpENyA=="], - "@oxlint-tsgolint/linux-arm64": ["@oxlint-tsgolint/linux-arm64@0.11.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-eoJfdmHcpG9k8fufb8yL3rC3HC6QELoTEfs56lmGaRIHHmd1aj4MWDbGCqdRqPEp7oC5fVvFxi7wDkA1MDf99Q=="], + "@oxlint-tsgolint/win32-arm64": ["@oxlint-tsgolint/win32-arm64@0.11.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-ipOs6kKo8fz5n5LSHvcbyZFmEpEIsh2m7+B03RW3jGjBEPMiXb4PfKNuxnusFYTtJM9WaR3bCVm5UxeJTA8r3w=="], - "@oxlint-tsgolint/linux-x64": ["@oxlint-tsgolint/linux-x64@0.11.3", "", { "os": "linux", "cpu": "x64" }, "sha512-t7jGK0vBApuAGvOnCPTxsdX+1e9nMdvqU3zHCJWQ7yUDaJxki0bCy4zbKfUgVo8ePeVRgIKWwqLFBOVTXQ5AMQ=="], + "@oxlint-tsgolint/win32-x64": ["@oxlint-tsgolint/win32-x64@0.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-m2apsAXg6qU3ulQG45W/qshyEpOjoL+uaQyXJG5dBoDoa66XPtCaSkBlKltD0EwGu0aoB8lM4I5I3OzQ6raNhw=="], - "@oxlint-tsgolint/win32-arm64": ["@oxlint-tsgolint/win32-arm64@0.11.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-6ellG0zcWnj2b6Mr7fl19x+nlFIWGWoKCBlYnqNZ4CaziRYGpYx7PLwHhPJq331w7zzRRSnYqhyTrVluYjZADQ=="], + "@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@1.39.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lT3hNhIa02xCujI6YGgjmYGg3Ht/X9ag5ipUVETaMpx5Rd4BbTNWUPif1WN1YZHxt3KLCIqaAe7zVhatv83HOQ=="], - "@oxlint-tsgolint/win32-x64": ["@oxlint-tsgolint/win32-x64@0.11.3", "", { "os": "win32", "cpu": "x64" }, "sha512-rzvfaRJPK9eRYVWMXCt8JtvOsVFAsqScgsFhnXzsipU6W1Te0g+b4q068o7hZ3NRTjJxNgFJj8ayOkZ6NbX0tA=="], + "@oxlint/darwin-x64": ["@oxlint/darwin-x64@1.39.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-UT+rfTWd+Yr7iJeSLd/7nF8X4gTYssKh+n77hxl6Oilp3NnG1CKRHxZDy3o3lIBnwgzJkdyUAiYWO1bTMXQ1lA=="], - "@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@1.42.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ui5CdAcDsXPQwZQEXOOSWsilJWhgj9jqHCvYBm2tDE8zfwZZuF9q58+hGKH1x5y0SV4sRlyobB2Quq6uU6EgeA=="], + "@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@1.39.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qocBkvS2V6rH0t9AT3DfQunMnj3xkM7srs5/Ycj2j5ZqMoaWd/FxHNVJDFP++35roKSvsRJoS0mtA8/77jqm6Q=="], - "@oxlint/darwin-x64": ["@oxlint/darwin-x64@1.42.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-wo0M/hcpHRv7vFje99zHHqheOhVEwUOKjOgBKyi0M99xcLizv04kcSm1rTd6HSCeZgOtiJYZRVAlKhQOQw2byQ=="], + "@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@1.39.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-arZzAc1PPcz9epvGBBCMHICeyQloKtHX3eoOe62B3Dskn7gf6Q14wnDHr1r9Vp4vtcBATNq6HlKV14smdlC/qA=="], - "@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@1.42.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j4QzfCM8ks+OyM+KKYWDiBEQsm5RCW50H1Wz16wUyoFsobJ+X5qqcJxq6HvkE07m8euYmZelyB0WqsiDoz1v8g=="], + "@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@1.39.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZVt5qsECpuNprdWxAPpDBwoixr1VTcZ4qAEQA2l/wmFyVPDYFD3oBY/SWACNnWBddMrswjTg9O8ALxYWoEpmXw=="], - "@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@1.42.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-g5b1Uw7zo6yw4Ymzyd1etKzAY7xAaGA3scwB8tAp3QzuY7CYdfTwlhiLKSAKbd7T/JBgxOXAGNcLDorJyVTXcg=="], + "@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@1.39.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pB0hlGyKPbxr9NMIV783lD6cWL3MpaqnZRM9MWni4yBdHPTKyFNYdg5hGD0Bwg+UP4S2rOevq/+OO9x9Bi7E6g=="], - "@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@1.42.0", "", { "os": "linux", "cpu": "x64" }, "sha512-HnD99GD9qAbpV4q9iQil7mXZUJFpoBdDavfcC2CgGLPlawfcV5COzQPNwOgvPVkr7C0cBx6uNCq3S6r9IIiEIg=="], + "@oxlint/win32-arm64": ["@oxlint/win32-arm64@1.39.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gg2SFaJohI9+tIQVKXlPw3FsPQFi/eCSWiCgwPtPn5uzQxHRTeQEZKuluz1fuzR5U70TXubb2liZi4Dgl8LJQA=="], - "@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@1.42.0", "", { "os": "linux", "cpu": "x64" }, "sha512-8NTe8A78HHFn+nBi+8qMwIjgv9oIBh+9zqCPNLH56ah4vKOPvbePLI6NIv9qSkmzrBuu8SB+FJ2TH/G05UzbNA=="], - - "@oxlint/win32-arm64": ["@oxlint/win32-arm64@1.42.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-lAPS2YAuu+qFqoTNPFcNsxXjwSV0M+dOgAzzVTAN7Yo2ifj+oLOx0GsntWoM78PvQWI7Q827ZxqtU2ImBmDapA=="], - - "@oxlint/win32-x64": ["@oxlint/win32-x64@1.42.0", "", { "os": "win32", "cpu": "x64" }, "sha512-3/KmyUOHNriL6rLpaFfm9RJxdhpXY2/Ehx9UuorJr2pUA+lrZL15FAEx/DOszYm5r10hfzj40+efAHcCilNvSQ=="], + "@oxlint/win32-x64": ["@oxlint/win32-x64@1.39.0", "", { "os": "win32", "cpu": "x64" }, "sha512-sbi25lfj74hH+6qQtb7s1wEvd1j8OQbTaH8v3xTcDjrwm579Cyh0HBv1YSZ2+gsnVwfVDiCTL1D0JsNqYXszVA=="], "@panva/hkdf": ["@panva/hkdf@1.2.1", "", {}, "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw=="], - "@playwright/test": ["@playwright/test@1.58.0", "", { "dependencies": { "playwright": "1.58.0" }, "bin": { "playwright": "cli.js" } }, "sha512-fWza+Lpbj6SkQKCrU6si4iu+fD2dD3gxNHFhUPxsfXBPhnv3rRSQVd0NtBUT9Z/RhF/boCBcuUaMUSTRTopjZg=="], + "@playwright/test": ["@playwright/test@1.57.0", "", { "dependencies": { "playwright": "1.57.0" }, "bin": { "playwright": "cli.js" } }, "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA=="], + + "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], + "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.11", "", { "dependencies": { "@radix-ui/react-context": "1.1.3", "@radix-ui/react-primitive": "2.1.4", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q=="], + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], - "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + "@radix-ui/react-context": ["@radix-ui/react-context@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw=="], + + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="], "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], @@ -434,24 +431,40 @@ "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], + "@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="], + "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="], + "@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w=="], + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], - "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="], + "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="], + + "@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="], + + "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="], + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="], + + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="], + "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="], "@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q=="], + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="], + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], @@ -460,67 +473,67 @@ "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], + "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.0", "", { "dependencies": { "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA=="], + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="], + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="], + "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], "@resvg/resvg-wasm": ["@resvg/resvg-wasm@2.6.2", "", {}, "sha512-FqALmHI8D4o6lk/LRWDnhw95z5eO+eAa6ORjVg09YRR7BkcM6oPHU9uyC0gtQG5vpFLvgpeU4+zEAz2H8APHNw=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.0", "", { "os": "android", "cpu": "arm" }, "sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA=="], - - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.0", "", { "os": "android", "cpu": "arm64" }, "sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.54.0", "", { "os": "android", "cpu": "arm" }, "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.54.0", "", { "os": "android", "cpu": "arm64" }, "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.54.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.54.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.54.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.0", "", { "os": "linux", "cpu": "arm" }, "sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.54.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.54.0", "", { "os": "linux", "cpu": "arm" }, "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.54.0", "", { "os": "linux", "cpu": "arm" }, "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.54.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.0", "", { "os": "linux", "cpu": "none" }, "sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.54.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg=="], - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.0", "", { "os": "linux", "cpu": "none" }, "sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ=="], + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.54.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA=="], - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.0", "", { "os": "linux", "cpu": "none" }, "sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.0", "", { "os": "linux", "cpu": "none" }, "sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.54.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.54.0", "", { "os": "linux", "cpu": "x64" }, "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.0", "", { "os": "linux", "cpu": "x64" }, "sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.54.0", "", { "os": "linux", "cpu": "x64" }, "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.0", "", { "os": "linux", "cpu": "x64" }, "sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw=="], + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.54.0", "", { "os": "none", "cpu": "arm64" }, "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg=="], - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.54.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.0", "", { "os": "none", "cpu": "arm64" }, "sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.54.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ=="], + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.54.0", "", { "os": "win32", "cpu": "x64" }, "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA=="], - - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.0", "", { "os": "win32", "cpu": "x64" }, "sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.54.0", "", { "os": "win32", "cpu": "x64" }, "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg=="], "@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg=="], @@ -566,7 +579,7 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="], - "@tanstack/devtools": ["@tanstack/devtools@0.10.4", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/keyboard": "^1.3.3", "@solid-primitives/resize-observer": "^2.1.3", "@tanstack/devtools-client": "0.0.5", "@tanstack/devtools-event-bus": "0.4.0", "@tanstack/devtools-ui": "0.4.4", "clsx": "^2.1.1", "goober": "^2.1.16", "solid-js": "^1.9.9" } }, "sha512-GR/HMWe+eAZgSm/mOeuWMs/cXy3pEcrdMBU+OH0c6Qv1IXYv/xqru4aCSJPe+2/eJXng5ioqCsoVt9MztyU1mg=="], + "@tanstack/devtools": ["@tanstack/devtools@0.10.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/keyboard": "^1.3.3", "@solid-primitives/resize-observer": "^2.1.3", "@tanstack/devtools-client": "0.0.5", "@tanstack/devtools-event-bus": "0.4.0", "@tanstack/devtools-ui": "0.4.4", "clsx": "^2.1.1", "goober": "^2.1.16", "solid-js": "^1.9.9" } }, "sha512-M2HnKtaNf3Z8JDTNDq+X7/1gwOqSwTnCyC0GR+TYiRZM9mkY9GpvTqp6p6bx3DT8onu2URJiVxgHD9WK2e3MNQ=="], "@tanstack/devtools-client": ["@tanstack/devtools-client@0.0.5", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.0" } }, "sha512-hsNDE3iu4frt9cC2ppn1mNRnLKo2uc1/1hXAyY9z4UYb+o40M2clFAhiFoo4HngjfGJDV3x18KVVIq7W4Un+zA=="], @@ -578,49 +591,49 @@ "@tanstack/devtools-vite": ["@tanstack/devtools-vite@0.4.1", "", { "dependencies": { "@babel/core": "^7.28.4", "@babel/generator": "^7.28.3", "@babel/parser": "^7.28.4", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@tanstack/devtools-client": "0.0.5", "@tanstack/devtools-event-bus": "0.4.0", "chalk": "^5.6.2", "launch-editor": "^2.11.1", "picomatch": "^4.0.3" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0" } }, "sha512-PkMOomcWnl/pUkCqIjqL/csjPHtkMVBirDpJVOZR7XJZDxo5CuD7B+3KsujFCF4Dsn6QYlae97gCZvxi/CB76Q=="], - "@tanstack/history": ["@tanstack/history@1.154.14", "", {}, "sha512-xyIfof8eHBuub1CkBnbKNKQXeRZC4dClhmzePHVOEel4G7lk/dW+TQ16da7CFdeNLv6u6Owf5VoBQxoo6DFTSA=="], + "@tanstack/history": ["@tanstack/history@1.151.1", "", {}, "sha512-Z/eymNBuUGHYIea7nNX3xR5feqx418ChlwWOKklVpCVzEQ5Q3kNTUw+WK4HYUKxF+1uXFN01Dbuhhl7SmW1LJA=="], - "@tanstack/react-devtools": ["@tanstack/react-devtools@0.9.3", "", { "dependencies": { "@tanstack/devtools": "0.10.4" }, "peerDependencies": { "@types/react": ">=16.8", "@types/react-dom": ">=16.8", "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-SJTYWXWZkbWznwUwZ11awinPGB5StVIVyJXT0BFM1zUgjuajRwT8xRHl1oXVzVqqjJP5kfj89jkbFrcQPpq7Ng=="], + "@tanstack/react-devtools": ["@tanstack/react-devtools@0.9.2", "", { "dependencies": { "@tanstack/devtools": "0.10.3" }, "peerDependencies": { "@types/react": ">=16.8", "@types/react-dom": ">=16.8", "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-JNXvBO3jgq16GzTVm7p65n5zHNfMhnqF6Bm7CawjoqZrjEakxbM6Yvy63aKSIpbrdf+Wun2Xn8P0qD+vp56e1g=="], - "@tanstack/react-router": ["@tanstack/react-router@1.157.16", "", { "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.157.16", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-xwFQa7S7dhBhm3aJYwU79cITEYgAKSrcL6wokaROIvl2JyIeazn8jueWqUPJzFjv+QF6Q8euKRlKUEyb5q2ymg=="], + "@tanstack/react-router": ["@tanstack/react-router@1.151.6", "", { "dependencies": { "@tanstack/history": "1.151.1", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.151.6", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-KDbz7kacZCOoDrUwYljz4I/qjqVGq+bgUhpi/CWubi7by0GZ3JEECwFl/+k+4V6ATinJDjTNmCGwFcdwqjQDtA=="], - "@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.157.16", "", { "dependencies": { "@tanstack/router-devtools-core": "1.157.16" }, "peerDependencies": { "@tanstack/react-router": "^1.157.16", "@tanstack/router-core": "^1.157.16", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-g6ekyzumfLBX6T5e+Vu2r37Z2CFJKrWRFqIy3vZ6A3x7OcuPV8uXNjyrLSiT/IsGTiF8YzwI4nWJa4fyd7NlCw=="], + "@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.151.6", "", { "dependencies": { "@tanstack/router-devtools-core": "1.151.6" }, "peerDependencies": { "@tanstack/react-router": "^1.151.6", "@tanstack/router-core": "^1.151.6", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-mRRFzIAIOAWYcZrEr0FYy/1FmM51iWwUdK0J3nWuXjAIeEb7uizS0HkeNbzX5yxfGZgkplk23eCXIUmJcDuVRQ=="], - "@tanstack/react-start": ["@tanstack/react-start@1.157.16", "", { "dependencies": { "@tanstack/react-router": "1.157.16", "@tanstack/react-start-client": "1.157.16", "@tanstack/react-start-server": "1.157.16", "@tanstack/router-utils": "^1.154.7", "@tanstack/start-client-core": "1.157.16", "@tanstack/start-plugin-core": "1.157.16", "@tanstack/start-server-core": "1.157.16", "pathe": "^2.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "vite": ">=7.0.0" } }, "sha512-FO6UYjsZyNaC0ickSSvClqfVZemp9/HWnbRJQU2dOKYQsI+wnznhLp9IkgG90iFBLcuMAWhcNHMiIuz603GJBg=="], + "@tanstack/react-start": ["@tanstack/react-start@1.152.0", "", { "dependencies": { "@tanstack/react-router": "1.151.6", "@tanstack/react-start-client": "1.152.0", "@tanstack/react-start-server": "1.152.0", "@tanstack/router-utils": "^1.143.11", "@tanstack/start-client-core": "1.152.0", "@tanstack/start-plugin-core": "1.152.0", "@tanstack/start-server-core": "1.152.0", "pathe": "^2.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "vite": ">=7.0.0" } }, "sha512-btRNNIJGnXVEmD9yuiyNRvMITX5aocVJVdv/LQHxca4EcxntPEn1+HboPXFr6SDl9UNekH/6NZqv3LPz9tDm7A=="], - "@tanstack/react-start-client": ["@tanstack/react-start-client@1.157.16", "", { "dependencies": { "@tanstack/react-router": "1.157.16", "@tanstack/router-core": "1.157.16", "@tanstack/start-client-core": "1.157.16", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-r3XTxYPJXZ/szhbloxqT6CQtsoEjw8DjbnZh/3ZsQv2PLKTOl925cy7YVdQc2cWZyXtn5e19Ig78R+8tsoTpig=="], + "@tanstack/react-start-client": ["@tanstack/react-start-client@1.152.0", "", { "dependencies": { "@tanstack/react-router": "1.151.6", "@tanstack/router-core": "1.151.6", "@tanstack/start-client-core": "1.152.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-YI2VVdCLk86QP5Q0oMXZWdk551haIisuF5AIr+S9ZAF415s4AgDpKRlXX251aqiHXYvv4rFnV1c9o7w02031Cw=="], - "@tanstack/react-start-server": ["@tanstack/react-start-server@1.157.16", "", { "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/react-router": "1.157.16", "@tanstack/router-core": "1.157.16", "@tanstack/start-client-core": "1.157.16", "@tanstack/start-server-core": "1.157.16" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-1YkBss4SUQ+HqVC1yGN/j7VNwjvdHHd3K58fASe0bz+uf7GrkGJlRXPkMJdxJkkmefYHQfyBL+q7o723N4CMYA=="], + "@tanstack/react-start-server": ["@tanstack/react-start-server@1.152.0", "", { "dependencies": { "@tanstack/history": "1.151.1", "@tanstack/react-router": "1.151.6", "@tanstack/router-core": "1.151.6", "@tanstack/start-client-core": "1.152.0", "@tanstack/start-server-core": "1.152.0" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-oz3J3Ipj04IUAfVSE1DD41x8+7DDWHZez+fCqLnk8sw63ct013CeKwIvW9v1n0OtU6axekuVHHYBOsF5wO5/lg=="], "@tanstack/react-store": ["@tanstack/react-store@0.8.0", "", { "dependencies": { "@tanstack/store": "0.8.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow=="], - "@tanstack/router-core": ["@tanstack/router-core@1.157.16", "", { "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-eJuVgM7KZYTTr4uPorbUzUflmljMVcaX2g6VvhITLnHmg9SBx9RAgtQ1HmT+72mzyIbRSlQ1q0fY/m+of/fosA=="], + "@tanstack/router-core": ["@tanstack/router-core@1.151.6", "", { "dependencies": { "@tanstack/history": "1.151.1", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.1", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-eyqWx6vhKffkINWLujDF2sxAG9GE/XUdi3HrlD94ddJO9MBi/90a1HJaTYFSV8LmngjcRv8A3tt7OvFdv/UqhA=="], - "@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.157.16", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "@tanstack/router-core": "^1.157.16", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-XBJTs/kMZYK6J2zhbGucHNuypwDB1t2vi8K5To+V6dUnLGBEyfQTf01fegiF4rpL1yXgomdGnP6aTiOFgldbVg=="], + "@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.151.6", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "@tanstack/router-core": "^1.151.6", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-OHGGvEtnANEbEwjYCChbvCyCLk/3Cqh9G5bhM5DVqrZ+b9wfeu46IdEsbSi1JfuK2sCHNMS5MrJaE2HZPsFx6Q=="], - "@tanstack/router-generator": ["@tanstack/router-generator@1.157.16", "", { "dependencies": { "@tanstack/router-core": "1.157.16", "@tanstack/router-utils": "1.154.7", "@tanstack/virtual-file-routes": "1.154.7", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-Ae2M00VTFjjED7glSCi/mMLENRzhEym6NgjoOx7UVNbCC/rLU/5ASDe5VIlDa8QLEqP5Pj088Gi51gjmRuICvQ=="], + "@tanstack/router-generator": ["@tanstack/router-generator@1.151.6", "", { "dependencies": { "@tanstack/router-core": "1.151.6", "@tanstack/router-utils": "1.143.11", "@tanstack/virtual-file-routes": "1.145.4", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-IS4tkrkLIwI2EViGlUXCVgnKJ4EhWMM6w75XoJqd0X4t6K0/OiHkr3AQ0f2qZXbNciqLGxS88GLe5UiSAOS5Vw=="], - "@tanstack/router-plugin": ["@tanstack/router-plugin@1.157.16", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.157.16", "@tanstack/router-generator": "1.157.16", "@tanstack/router-utils": "1.154.7", "@tanstack/virtual-file-routes": "1.154.7", "babel-dead-code-elimination": "^1.0.11", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.157.16", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-YQg7L06xyCJAYyrEJNZGAnDL8oChILU+G/eSDIwEfcWn5iLk+47x1Gcdxr82++47PWmOPhzuTo8edDQXWs7kAA=="], + "@tanstack/router-plugin": ["@tanstack/router-plugin@1.151.6", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.151.6", "@tanstack/router-generator": "1.151.6", "@tanstack/router-utils": "1.143.11", "@tanstack/virtual-file-routes": "1.145.4", "babel-dead-code-elimination": "^1.0.11", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.151.6", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-Kz9wmAgcylung1KoXvEEVTW91PNh4U65MgVwSmz5fYQP7UqNHPvHqBFWKEu17a45SfZ4LufsNW3LTFT35tWGXg=="], - "@tanstack/router-utils": ["@tanstack/router-utils@1.154.7", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "ansis": "^4.1.0", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-61bGx32tMKuEpVRseu2sh1KQe8CfB7793Mch/kyQt0EP3tD7X0sXmimCl3truRiDGUtI0CaSoQV1NPjAII1RBA=="], + "@tanstack/router-utils": ["@tanstack/router-utils@1.143.11", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "ansis": "^4.1.0", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-N24G4LpfyK8dOlnP8BvNdkuxg1xQljkyl6PcrdiPSA301pOjatRT1y8wuCCJZKVVD8gkd0MpCZ0VEjRMGILOtA=="], - "@tanstack/start-client-core": ["@tanstack/start-client-core@1.157.16", "", { "dependencies": { "@tanstack/router-core": "1.157.16", "@tanstack/start-fn-stubs": "1.154.7", "@tanstack/start-storage-context": "1.157.16", "seroval": "^1.4.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-O+7H133MWQTkOxmXJNhrLXiOhDcBlxvpEcCd/N25Ga6eyZ7/P5vvFzNkSSxeQNkZV+RiPWnA5B75gT+U+buz3w=="], + "@tanstack/start-client-core": ["@tanstack/start-client-core@1.152.0", "", { "dependencies": { "@tanstack/router-core": "1.151.6", "@tanstack/start-fn-stubs": "1.151.3", "@tanstack/start-storage-context": "1.151.6", "seroval": "^1.4.1", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-G88urpJImiGZttawtciSj46Ko57TXO2pd11Zef6Yw1VLD6fJs+RF0q9N0P98SGs9Jm0oUjLusi3Ti550fkQ2qw=="], - "@tanstack/start-fn-stubs": ["@tanstack/start-fn-stubs@1.154.7", "", {}, "sha512-D69B78L6pcFN5X5PHaydv7CScQcKLzJeEYqs7jpuyyqGQHSUIZUjS955j+Sir8cHhuDIovCe2LmsYHeZfWf3dQ=="], + "@tanstack/start-fn-stubs": ["@tanstack/start-fn-stubs@1.151.3", "", {}, "sha512-/zWBnfsOwact936Bn0CxigudU1QRZdiNTsK7ME/LMXXA66XsDxkryX5+5FeGwU5ETNPfLAx6pRUet1mtUKnLCg=="], - "@tanstack/start-plugin-core": ["@tanstack/start-plugin-core@1.157.16", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.28.5", "@babel/types": "^7.28.5", "@rolldown/pluginutils": "1.0.0-beta.40", "@tanstack/router-core": "1.157.16", "@tanstack/router-generator": "1.157.16", "@tanstack/router-plugin": "1.157.16", "@tanstack/router-utils": "1.154.7", "@tanstack/start-client-core": "1.157.16", "@tanstack/start-server-core": "1.157.16", "babel-dead-code-elimination": "^1.0.11", "cheerio": "^1.0.0", "exsolve": "^1.0.7", "pathe": "^2.0.3", "srvx": "^0.10.1", "tinyglobby": "^0.2.15", "ufo": "^1.5.4", "vitefu": "^1.1.1", "xmlbuilder2": "^4.0.3", "zod": "^3.24.2" }, "peerDependencies": { "vite": ">=7.0.0" } }, "sha512-VmRXuvP5flryUAHeBM4Xb06n544qLtyA2cwmlQLRTUYtQiQEAdd9CvCGy8CPAly3f7eeXKqC7aX0v3MwWkLR8w=="], + "@tanstack/start-plugin-core": ["@tanstack/start-plugin-core@1.152.0", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.28.5", "@babel/types": "^7.28.5", "@rolldown/pluginutils": "1.0.0-beta.40", "@tanstack/router-core": "1.151.6", "@tanstack/router-generator": "1.151.6", "@tanstack/router-plugin": "1.151.6", "@tanstack/router-utils": "1.143.11", "@tanstack/start-client-core": "1.152.0", "@tanstack/start-server-core": "1.152.0", "babel-dead-code-elimination": "^1.0.11", "cheerio": "^1.0.0", "exsolve": "^1.0.7", "pathe": "^2.0.3", "srvx": "^0.10.0", "tinyglobby": "^0.2.15", "ufo": "^1.5.4", "vitefu": "^1.1.1", "xmlbuilder2": "^4.0.3", "zod": "^3.24.2" }, "peerDependencies": { "vite": ">=7.0.0" } }, "sha512-XT+ECwpmi6cQj6gb9QHo1XOOw+7zGXOvwnrijg3HSzNKFDlawHycgOotnViTOjtVjhHLpW1/yGyivWX5OC8wTw=="], - "@tanstack/start-server-core": ["@tanstack/start-server-core@1.157.16", "", { "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/router-core": "1.157.16", "@tanstack/start-client-core": "1.157.16", "@tanstack/start-storage-context": "1.157.16", "h3-v2": "npm:h3@2.0.1-rc.11", "seroval": "^1.4.2", "tiny-invariant": "^1.3.3" } }, "sha512-PEltFleYfiqz6+KcmzNXxc1lXgT7VDNKP6G6i1TirdHBDbRJ9CIY+ASLPlhrRwqwA2PL9PpFjXZl8u5bH/+Q9A=="], + "@tanstack/start-server-core": ["@tanstack/start-server-core@1.152.0", "", { "dependencies": { "@tanstack/history": "1.151.1", "@tanstack/router-core": "1.151.6", "@tanstack/start-client-core": "1.152.0", "@tanstack/start-storage-context": "1.151.6", "h3-v2": "npm:h3@2.0.1-rc.7", "seroval": "^1.4.1", "tiny-invariant": "^1.3.3" } }, "sha512-bMmc4tIhR3FClb1iWLw0zuhvY71b9k2iz8Jw0tSK/KKi++gl1bGyJjuPtx8kDbx5Ix1SoXp4QNdXuo7dv5INfQ=="], - "@tanstack/start-storage-context": ["@tanstack/start-storage-context@1.157.16", "", { "dependencies": { "@tanstack/router-core": "1.157.16" } }, "sha512-56izE0oihAw2YRwYUEds2H+uO5dyT2CahXCgWX62+l+FHou09M9mSep68n1lBKPdphC2ZU3cPV7wnvgeraJWHg=="], + "@tanstack/start-storage-context": ["@tanstack/start-storage-context@1.151.6", "", { "dependencies": { "@tanstack/router-core": "1.151.6" } }, "sha512-MvTcT40qnqatIpKjWSfMRxFzTkprGBxhX2c+em58iZLEsGksitMUWbprknD6AIUqjHty8V3LuhULks/o6tSugQ=="], "@tanstack/store": ["@tanstack/store@0.8.0", "", {}, "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ=="], - "@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.154.7", "", {}, "sha512-cHHDnewHozgjpI+MIVp9tcib6lYEQK5MyUr0ChHpHFGBl8Xei55rohFK0I0ve/GKoHeioaK42Smd8OixPp6CTg=="], + "@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.145.4", "", {}, "sha512-CI75JrfqSluhdGwLssgVeQBaCphgfkMQpi8MCY3UJX1hoGzXa8kHYJcUuIFMOLs1q7zqHy++EVVtMK03osR5wQ=="], "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], - "@testing-library/react": ["@testing-library/react@16.3.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="], + "@testing-library/react": ["@testing-library/react@16.3.1", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-gr4KtAWqIOQoucWYD/f6ki+j5chXfcPc74Col/6poTyqTmn7zRmodWahWRCp8tYd+GMqBonw6hstNzqjbs6gjw=="], "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], @@ -650,9 +663,9 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@25.1.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA=="], + "@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="], - "@types/react": ["@types/react@19.2.10", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw=="], + "@types/react": ["@types/react@19.2.8", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], @@ -668,21 +681,21 @@ "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.2", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.53", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ=="], - "@vitest/coverage-v8": ["@vitest/coverage-v8@4.0.18", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.0.18", "ast-v8-to-istanbul": "^0.3.10", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.1", "obug": "^2.1.1", "std-env": "^3.10.0", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "@vitest/browser": "4.0.18", "vitest": "4.0.18" }, "optionalPeers": ["@vitest/browser"] }, "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg=="], + "@vitest/coverage-v8": ["@vitest/coverage-v8@4.0.17", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.0.17", "ast-v8-to-istanbul": "^0.3.10", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.1", "obug": "^2.1.1", "std-env": "^3.10.0", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "@vitest/browser": "4.0.17", "vitest": "4.0.17" }, "optionalPeers": ["@vitest/browser"] }, "sha512-/6zU2FLGg0jsd+ePZcwHRy3+WpNTBBhDY56P4JTRqUN/Dp6CvOEa9HrikcQ4KfV2b2kAHUFB4dl1SuocWXSFEw=="], - "@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="], + "@vitest/expect": ["@vitest/expect@4.0.17", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.17", "@vitest/utils": "4.0.17", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-mEoqP3RqhKlbmUmntNDDCJeTDavDR+fVYkSOw8qRwJFaW/0/5zA9zFeTrHqNtcmwh6j26yMmwx2PqUDPzt5ZAQ=="], - "@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="], + "@vitest/mocker": ["@vitest/mocker@4.0.17", "", { "dependencies": { "@vitest/spy": "4.0.17", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-+ZtQhLA3lDh1tI2wxe3yMsGzbp7uuJSWBM1iTIKCbppWTSBN09PUC+L+fyNlQApQoR+Ps8twt2pbSSXg2fQVEQ=="], - "@vitest/pretty-format": ["@vitest/pretty-format@4.0.18", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw=="], + "@vitest/pretty-format": ["@vitest/pretty-format@4.0.17", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-Ah3VAYmjcEdHg6+MwFE17qyLqBHZ+ni2ScKCiW2XrlSBV4H3Z7vYfPfz7CWQ33gyu76oc0Ai36+kgLU3rfF4nw=="], - "@vitest/runner": ["@vitest/runner@4.0.18", "", { "dependencies": { "@vitest/utils": "4.0.18", "pathe": "^2.0.3" } }, "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw=="], + "@vitest/runner": ["@vitest/runner@4.0.17", "", { "dependencies": { "@vitest/utils": "4.0.17", "pathe": "^2.0.3" } }, "sha512-JmuQyf8aMWoo/LmNFppdpkfRVHJcsgzkbCA+/Bk7VfNH7RE6Ut2qxegeyx2j3ojtJtKIbIGy3h+KxGfYfk28YQ=="], - "@vitest/snapshot": ["@vitest/snapshot@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA=="], + "@vitest/snapshot": ["@vitest/snapshot@4.0.17", "", { "dependencies": { "@vitest/pretty-format": "4.0.17", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-npPelD7oyL+YQM2gbIYvlavlMVWUfNNGZPcu0aEUQXt7FXTuqhmgiYupPnAanhKvyP6Srs2pIbWo30K0RbDtRQ=="], - "@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="], + "@vitest/spy": ["@vitest/spy@4.0.17", "", {}, "sha512-I1bQo8QaP6tZlTomQNWKJE6ym4SHf3oLS7ceNjozxxgzavRAgZDc06T7kD8gb9bXKEgcLNt00Z+kZO6KaJ62Ew=="], - "@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="], + "@vitest/utils": ["@vitest/utils@4.0.17", "", { "dependencies": { "@vitest/pretty-format": "4.0.17", "tinyrainbow": "^3.0.3" } }, "sha512-RG6iy+IzQpa9SB8HAFHJ9Y+pTzI+h8553MrciN9eC6TFBErqrQaTas4vG+MVj8S4uKk8uTT2p0vgZPnTdxd96w=="], "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], @@ -712,11 +725,11 @@ "ast-v8-to-istanbul": ["ast-v8-to-istanbul@0.3.10", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^9.0.1" } }, "sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ=="], - "babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="], + "babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.11", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-mwq3W3e/pKSI6TG8lXMiDWvEi1VXYlSBlJlB3l+I0bAb5u1RNUl88udos85eOPNK3m5EXK9uO7d2g08pesTySQ=="], "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="], "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], @@ -728,7 +741,7 @@ "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], - "caniuse-lite": ["caniuse-lite@1.0.30001766", "", {}, "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA=="], + "caniuse-lite": ["caniuse-lite@1.0.30001762", "", {}, "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], @@ -744,19 +757,19 @@ "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], - "cheerio": ["cheerio@1.2.0", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.1.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.19.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg=="], + "cheerio": ["cheerio@1.1.2", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.0.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.12.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg=="], "cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="], "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], - "clawhub": ["clawhub@workspace:packages/clawdhub"], + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], - "clawhub-schema": ["clawhub-schema@workspace:packages/schema"], + "clawhub": ["clawhub@workspace:packages/clawdhub"], "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], - "cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="], + "cli-spinners": ["cli-spinners@3.3.0", "", {}, "sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], @@ -768,7 +781,7 @@ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - "convex": ["convex@1.31.7", "", { "dependencies": { "esbuild": "0.27.0", "prettier": "^3.0.0" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-PtNMe1mAIOvA8Yz100QTOaIdgt2rIuWqencVXrb4McdhxBHZ8IJ1eXTnrgCC9HydyilGT1pOn+KNqT14mqn9fQ=="], + "convex": ["convex@1.31.6", "", { "dependencies": { "esbuild": "0.27.0", "prettier": "^3.0.0" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-9cIsOzepa3s9DURRF+fZHxbNuzLgilg9XGQCc45v0Xx4FemqeIezpPFSJF9WHC9ckk43TDUUXLecvLVt9djPkw=="], "convex-helpers": ["convex-helpers@0.1.111", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "convex": "^1.25.4", "hono": "^4.0.5", "react": "^17.0.2 || ^18.0.0 || ^19.0.0", "typescript": "^5.5", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@standard-schema/spec", "hono", "react", "typescript", "zod"], "bin": { "convex-helpers": "bin.cjs" } }, "sha512-0O59Ohi8HVc3+KULxSC6JHsw8cQJyc8gZ7OAfNRVX7T5Wy6LhPx3l8veYN9avKg7UiPlO7m1eBiQMHKclIyXyQ=="], @@ -776,7 +789,7 @@ "cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="], - "crossws": ["crossws@0.4.4", "", { "peerDependencies": { "srvx": ">=0.7.1" }, "optionalPeers": ["srvx"] }, "sha512-w6c4OdpRNnudVmcgr7brb/+/HmYjMQvYToO/oTrprTwxRUiom3LYWU1PMWuD006okbUWpII1Ea9/+kwpUfmyRg=="], + "crossws": ["crossws@0.4.1", "", { "peerDependencies": { "srvx": ">=0.7.1" }, "optionalPeers": ["srvx"] }, "sha512-E7WKBcHVhAVrY6JYD5kteNqVq1GSZxqGrdSiwXR9at+XHi43HJoCQKXcCczR5LBnBquFZPsB3o7HklulKoBU5w=="], "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], @@ -784,11 +797,11 @@ "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], - "cssstyle": ["cssstyle@5.3.7", "", { "dependencies": { "@asamuzakjp/css-color": "^4.1.1", "@csstools/css-syntax-patches-for-csstree": "^1.0.21", "css-tree": "^3.1.0", "lru-cache": "^11.2.4" } }, "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ=="], + "cssstyle": ["cssstyle@5.3.6", "", { "dependencies": { "@asamuzakjp/css-color": "^4.1.1", "@csstools/css-syntax-patches-for-csstree": "^1.0.21", "css-tree": "^3.1.0", "lru-cache": "^11.2.4" } }, "sha512-legscpSpgSAeGEe0TNcai97DKt9Vd9AsAdOL7Uoetb52Ar/8eJm3LIa39qpv8wWzLFlNG4vVvppQM+teaMPj3A=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - "data-urls": ["data-urls@6.0.1", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^15.1.0" } }, "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ=="], + "data-urls": ["data-urls@6.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^15.0.0" } }, "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA=="], "db0": ["db0@0.3.4", "", { "peerDependencies": { "@electric-sql/pglite": "*", "@libsql/client": "*", "better-sqlite3": "*", "drizzle-orm": "*", "mysql2": "*", "sqlite3": "*" }, "optionalPeers": ["@electric-sql/pglite", "@libsql/client", "better-sqlite3", "drizzle-orm", "mysql2", "sqlite3"] }, "sha512-RiXXi4WaNzPTHEOu8UPQKMooIbqOEyqA1t7Z6MsdxSCeb8iUC9ko3LcmsLmeUt2SM5bctfArZKkRQggKZz7JNw=="], @@ -796,7 +809,7 @@ "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], - "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + "decode-named-character-reference": ["decode-named-character-reference@1.2.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q=="], "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], @@ -806,7 +819,7 @@ "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], - "diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], + "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], "dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], @@ -820,7 +833,7 @@ "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], - "electron-to-chromium": ["electron-to-chromium@1.5.282", "", {}, "sha512-FCPkJtpst28UmFzd903iU7PdeVTfY0KAeJy+Lk0GLZRwgwYHn/irRcaCbQQOmr5Vytc/7rcavsYLvTM8RiHYhQ=="], + "electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="], "encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="], @@ -874,7 +887,7 @@ "h3": ["h3@2.0.1-rc.8", "", { "dependencies": { "rou3": "^0.7.12", "srvx": "^0.10.0" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-IIMQG7qnXx1Ls75suuMHH4xtcvTFxsUguDIZB+dgdYr1RftLj59FkeWF1dOr+jnejDs8Eo+ZKV1CMqogFeqGRQ=="], - "h3-v2": ["h3@2.0.1-rc.11", "", { "dependencies": { "rou3": "^0.7.12", "srvx": "^0.10.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-2myzjCqy32c1As9TjZW9fNZXtLqNedjFSrdFy2AjFBQQ3LzrnGoDdFDYfC0tV2e4vcyfJ2Sfo/F6NQhO2Ly/Mw=="], + "h3-v2": ["h3@2.0.1-rc.7", "", { "dependencies": { "rou3": "^0.7.12", "srvx": "^0.10.0" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-qbrRu1OLXmUYnysWOCVrYhtC/m8ZuXu/zCbo3U/KyphJxbPFiC76jHYwVrmEcss9uNAHO5BoUguQ46yEpgI2PA=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], @@ -888,7 +901,7 @@ "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], - "htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="], + "htmlparser2": ["htmlparser2@10.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.1", "entities": "^6.0.0" } }, "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g=="], "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], @@ -926,7 +939,7 @@ "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], - "isbot": ["isbot@5.1.34", "", {}, "sha512-aCMIBSKd/XPRYdiCQTLC8QHH4YT8B3JUADu+7COgYIZPvkeoMcUHMRjZLM9/7V8fCj+l7FSREc1lOPNjzogo/A=="], + "isbot": ["isbot@5.1.32", "", {}, "sha512-VNfjM73zz2IBZmdShMfAUg10prm6t7HFUQmNAEOAVS4YH92ZrZcvkMcGX6cIgBJAzWDzPent/EeAtYEHNPNPBQ=="], "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], @@ -980,7 +993,7 @@ "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], - "lru-cache": ["lru-cache@11.2.5", "", {}, "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw=="], + "lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="], "lucia": ["lucia@3.2.2", "", { "dependencies": { "@oslojs/crypto": "^1.0.1", "@oslojs/encoding": "^1.1.0" } }, "sha512-P1FlFBGCMPMXu+EGdVD9W4Mjm0DqsusmKgO7Xc33mI5X1bklmsQb0hfzPhXomQr9waWIBDsiOjvr1e6BTaUqpA=="], @@ -1090,15 +1103,17 @@ "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + "molthub-schema": ["molthub-schema@workspace:packages/schema"], + "monaco-editor": ["monaco-editor@0.55.1", "", { "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" } }, "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - "nf3": ["nf3@0.3.7", "", {}, "sha512-wL73kyZbBoeTWlvQWQ0gQDZnqp+aNlUN5YIqsc3fv5V/06LAlwrwt+G7TpugFLJIai0AhrmnKJ2kgW0xprj+yQ=="], + "nf3": ["nf3@0.1.12", "", {}, "sha512-qbMXT7RTGh74MYWPeqTIED8nDW70NXOULVHpdWcdZ7IVHVnAsMV9fNugSNnvooipDc1FMOzpis7T9nXJEbJhvQ=="], - "nitro": ["nitro@3.0.1-alpha.2", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.3", "db0": "^0.3.4", "h3": "^2.0.1-rc.11", "jiti": "^2.6.1", "nf3": "^0.3.5", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "oxc-minify": "^0.110.0", "oxc-transform": "^0.110.0", "srvx": "^0.10.1", "undici": "^7.18.2", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.5" }, "peerDependencies": { "rolldown": ">=1.0.0-beta.0", "rollup": "^4", "vite": "^7 || ^8 || >=8.0.0-0", "xml2js": "^0.6.2" }, "optionalPeers": ["rolldown", "rollup", "vite", "xml2js"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-YviDY5J/trS821qQ1fpJtpXWIdPYiOizC/meHavlm1Hfuhx//H+Egd1+4C5SegJRgtWMnRPW9n//6Woaw81cTQ=="], + "nitro": ["nitro@3.0.1-alpha.1", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.1", "db0": "^0.3.4", "h3": "2.0.1-rc.5", "jiti": "^2.6.1", "nf3": "^0.1.10", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "oxc-minify": "^0.96.0", "oxc-transform": "^0.96.0", "srvx": "^0.9.5", "undici": "^7.16.0", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.4" }, "peerDependencies": { "rolldown": "*", "rollup": "^4", "vite": "^7", "xml2js": "^0.6.2" }, "optionalPeers": ["rolldown", "rollup", "vite", "xml2js"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-U4AxIsXxdkxzkFrK0XAw0e5Qbojk8jQ50MjjRBtBakC4HurTtQoiZvF+lSe382jhuQZCfAyywGWOFa9QzXLFaw=="], "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -1118,15 +1133,15 @@ "only-allow": ["only-allow@1.2.2", "", { "dependencies": { "which-pm-runs": "1.1.0" }, "bin": { "only-allow": "bin.js" } }, "sha512-uxyNYDsCh5YIJ780G7hC5OHjVUr9reHsbZNMM80L9tZlTpb3hUzb36KXgW4ZUGtJKQnGA3xegmWg1BxhWV0jJA=="], - "ora": ["ora@9.1.0", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.2.2", "string-width": "^8.1.0" } }, "sha512-53uuLsXHOAJl5zLrUrzY9/kE+uIFEx7iaH4g2BIJQK4LZjY4LpCCYZVKDWIkL+F01wAaCg93duQ1whnK/AmY1A=="], + "ora": ["ora@9.0.0", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.2.2", "string-width": "^8.1.0", "strip-ansi": "^7.1.2" } }, "sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A=="], - "oxc-minify": ["oxc-minify@0.110.0", "", { "optionalDependencies": { "@oxc-minify/binding-android-arm-eabi": "0.110.0", "@oxc-minify/binding-android-arm64": "0.110.0", "@oxc-minify/binding-darwin-arm64": "0.110.0", "@oxc-minify/binding-darwin-x64": "0.110.0", "@oxc-minify/binding-freebsd-x64": "0.110.0", "@oxc-minify/binding-linux-arm-gnueabihf": "0.110.0", "@oxc-minify/binding-linux-arm-musleabihf": "0.110.0", "@oxc-minify/binding-linux-arm64-gnu": "0.110.0", "@oxc-minify/binding-linux-arm64-musl": "0.110.0", "@oxc-minify/binding-linux-ppc64-gnu": "0.110.0", "@oxc-minify/binding-linux-riscv64-gnu": "0.110.0", "@oxc-minify/binding-linux-riscv64-musl": "0.110.0", "@oxc-minify/binding-linux-s390x-gnu": "0.110.0", "@oxc-minify/binding-linux-x64-gnu": "0.110.0", "@oxc-minify/binding-linux-x64-musl": "0.110.0", "@oxc-minify/binding-openharmony-arm64": "0.110.0", "@oxc-minify/binding-wasm32-wasi": "0.110.0", "@oxc-minify/binding-win32-arm64-msvc": "0.110.0", "@oxc-minify/binding-win32-ia32-msvc": "0.110.0", "@oxc-minify/binding-win32-x64-msvc": "0.110.0" } }, "sha512-KWGTzPo83QmGrXC4ml83PM9HDwUPtZFfasiclUvTV4i3/0j7xRRqINVkrL77CbQnoWura3CMxkRofjQKVDuhBw=="], + "oxc-minify": ["oxc-minify@0.96.0", "", { "optionalDependencies": { "@oxc-minify/binding-android-arm64": "0.96.0", "@oxc-minify/binding-darwin-arm64": "0.96.0", "@oxc-minify/binding-darwin-x64": "0.96.0", "@oxc-minify/binding-freebsd-x64": "0.96.0", "@oxc-minify/binding-linux-arm-gnueabihf": "0.96.0", "@oxc-minify/binding-linux-arm-musleabihf": "0.96.0", "@oxc-minify/binding-linux-arm64-gnu": "0.96.0", "@oxc-minify/binding-linux-arm64-musl": "0.96.0", "@oxc-minify/binding-linux-riscv64-gnu": "0.96.0", "@oxc-minify/binding-linux-s390x-gnu": "0.96.0", "@oxc-minify/binding-linux-x64-gnu": "0.96.0", "@oxc-minify/binding-linux-x64-musl": "0.96.0", "@oxc-minify/binding-wasm32-wasi": "0.96.0", "@oxc-minify/binding-win32-arm64-msvc": "0.96.0", "@oxc-minify/binding-win32-x64-msvc": "0.96.0" } }, "sha512-dXeeGrfPJJ4rMdw+NrqiCRtbzVX2ogq//R0Xns08zql2HjV3Zi2SBJ65saqfDaJzd2bcHqvGWH+M44EQCHPAcA=="], - "oxc-transform": ["oxc-transform@0.110.0", "", { "optionalDependencies": { "@oxc-transform/binding-android-arm-eabi": "0.110.0", "@oxc-transform/binding-android-arm64": "0.110.0", "@oxc-transform/binding-darwin-arm64": "0.110.0", "@oxc-transform/binding-darwin-x64": "0.110.0", "@oxc-transform/binding-freebsd-x64": "0.110.0", "@oxc-transform/binding-linux-arm-gnueabihf": "0.110.0", "@oxc-transform/binding-linux-arm-musleabihf": "0.110.0", "@oxc-transform/binding-linux-arm64-gnu": "0.110.0", "@oxc-transform/binding-linux-arm64-musl": "0.110.0", "@oxc-transform/binding-linux-ppc64-gnu": "0.110.0", "@oxc-transform/binding-linux-riscv64-gnu": "0.110.0", "@oxc-transform/binding-linux-riscv64-musl": "0.110.0", "@oxc-transform/binding-linux-s390x-gnu": "0.110.0", "@oxc-transform/binding-linux-x64-gnu": "0.110.0", "@oxc-transform/binding-linux-x64-musl": "0.110.0", "@oxc-transform/binding-openharmony-arm64": "0.110.0", "@oxc-transform/binding-wasm32-wasi": "0.110.0", "@oxc-transform/binding-win32-arm64-msvc": "0.110.0", "@oxc-transform/binding-win32-ia32-msvc": "0.110.0", "@oxc-transform/binding-win32-x64-msvc": "0.110.0" } }, "sha512-/fymQNzzUoKZweH0nC5yvbI2eR0yWYusT9TEKDYVgOgYrf9Qmdez9lUFyvxKR9ycx+PTHi/reIOzqf3wkShQsw=="], + "oxc-transform": ["oxc-transform@0.96.0", "", { "optionalDependencies": { "@oxc-transform/binding-android-arm64": "0.96.0", "@oxc-transform/binding-darwin-arm64": "0.96.0", "@oxc-transform/binding-darwin-x64": "0.96.0", "@oxc-transform/binding-freebsd-x64": "0.96.0", "@oxc-transform/binding-linux-arm-gnueabihf": "0.96.0", "@oxc-transform/binding-linux-arm-musleabihf": "0.96.0", "@oxc-transform/binding-linux-arm64-gnu": "0.96.0", "@oxc-transform/binding-linux-arm64-musl": "0.96.0", "@oxc-transform/binding-linux-riscv64-gnu": "0.96.0", "@oxc-transform/binding-linux-s390x-gnu": "0.96.0", "@oxc-transform/binding-linux-x64-gnu": "0.96.0", "@oxc-transform/binding-linux-x64-musl": "0.96.0", "@oxc-transform/binding-wasm32-wasi": "0.96.0", "@oxc-transform/binding-win32-arm64-msvc": "0.96.0", "@oxc-transform/binding-win32-x64-msvc": "0.96.0" } }, "sha512-dQPNIF+gHpSkmC0+Vg9IktNyhcn28Y8R3eTLyzn52UNymkasLicl3sFAtz7oEVuFmCpgGjaUTKkwk+jW2cHpDQ=="], - "oxlint": ["oxlint@1.42.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.42.0", "@oxlint/darwin-x64": "1.42.0", "@oxlint/linux-arm64-gnu": "1.42.0", "@oxlint/linux-arm64-musl": "1.42.0", "@oxlint/linux-x64-gnu": "1.42.0", "@oxlint/linux-x64-musl": "1.42.0", "@oxlint/win32-arm64": "1.42.0", "@oxlint/win32-x64": "1.42.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.11.2" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-qnspC/lrp8FgKNaONLLn14dm+W5t0SSlus6V5NJpgI2YNT1tkFYZt4fBf14ESxf9AAh98WBASnW5f0gtw462Lg=="], + "oxlint": ["oxlint@1.39.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.39.0", "@oxlint/darwin-x64": "1.39.0", "@oxlint/linux-arm64-gnu": "1.39.0", "@oxlint/linux-arm64-musl": "1.39.0", "@oxlint/linux-x64-gnu": "1.39.0", "@oxlint/linux-x64-musl": "1.39.0", "@oxlint/win32-arm64": "1.39.0", "@oxlint/win32-x64": "1.39.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.10.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-wSiLr0wjG+KTU6c1LpVoQk7JZ7l8HCKlAkVDVTJKWmCGazsNxexxnOXl7dsar92mQcRnzko5g077ggP3RINSjA=="], - "oxlint-tsgolint": ["oxlint-tsgolint@0.11.3", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "0.11.3", "@oxlint-tsgolint/darwin-x64": "0.11.3", "@oxlint-tsgolint/linux-arm64": "0.11.3", "@oxlint-tsgolint/linux-x64": "0.11.3", "@oxlint-tsgolint/win32-arm64": "0.11.3", "@oxlint-tsgolint/win32-x64": "0.11.3" }, "bin": { "tsgolint": "bin/tsgolint.js" } }, "sha512-zkuGXJzE5WIoGQ6CHG3GbxncPNrvUG9giTKdXMqKrlieCRxa9hGMvMJM+7DFxKSaryVAEFrTQJNrGJHpeMmFPg=="], + "oxlint-tsgolint": ["oxlint-tsgolint@0.11.1", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "0.11.1", "@oxlint-tsgolint/darwin-x64": "0.11.1", "@oxlint-tsgolint/linux-arm64": "0.11.1", "@oxlint-tsgolint/linux-x64": "0.11.1", "@oxlint-tsgolint/win32-arm64": "0.11.1", "@oxlint-tsgolint/win32-x64": "0.11.1" }, "bin": { "tsgolint": "bin/tsgolint.js" } }, "sha512-WulCp+0/6RvpM4zPv+dAXybf03QvRA8ATxaBlmj4XMIQqTs5jeq3cUTk48WCt4CpLwKhyyGZPHmjLl1KHQ/cvA=="], "p-retry": ["p-retry@7.1.1", "", { "dependencies": { "is-network-error": "^1.1.0" } }, "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w=="], @@ -1146,9 +1161,9 @@ "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - "playwright": ["playwright@1.58.0", "", { "dependencies": { "playwright-core": "1.58.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ=="], + "playwright": ["playwright@1.57.0", "", { "dependencies": { "playwright-core": "1.57.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw=="], - "playwright-core": ["playwright-core@1.58.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw=="], + "playwright-core": ["playwright-core@1.57.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ=="], "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], @@ -1156,7 +1171,7 @@ "preact-render-to-string": ["preact-render-to-string@6.5.11", "", { "peerDependencies": { "preact": ">=10" } }, "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw=="], - "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], + "prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="], "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], @@ -1164,9 +1179,9 @@ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], + "react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], - "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + "react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="], "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], @@ -1198,7 +1213,7 @@ "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], - "rollup": ["rollup@4.57.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.0", "@rollup/rollup-android-arm64": "4.57.0", "@rollup/rollup-darwin-arm64": "4.57.0", "@rollup/rollup-darwin-x64": "4.57.0", "@rollup/rollup-freebsd-arm64": "4.57.0", "@rollup/rollup-freebsd-x64": "4.57.0", "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", "@rollup/rollup-linux-arm-musleabihf": "4.57.0", "@rollup/rollup-linux-arm64-gnu": "4.57.0", "@rollup/rollup-linux-arm64-musl": "4.57.0", "@rollup/rollup-linux-loong64-gnu": "4.57.0", "@rollup/rollup-linux-loong64-musl": "4.57.0", "@rollup/rollup-linux-ppc64-gnu": "4.57.0", "@rollup/rollup-linux-ppc64-musl": "4.57.0", "@rollup/rollup-linux-riscv64-gnu": "4.57.0", "@rollup/rollup-linux-riscv64-musl": "4.57.0", "@rollup/rollup-linux-s390x-gnu": "4.57.0", "@rollup/rollup-linux-x64-gnu": "4.57.0", "@rollup/rollup-linux-x64-musl": "4.57.0", "@rollup/rollup-openbsd-x64": "4.57.0", "@rollup/rollup-openharmony-arm64": "4.57.0", "@rollup/rollup-win32-arm64-msvc": "4.57.0", "@rollup/rollup-win32-ia32-msvc": "4.57.0", "@rollup/rollup-win32-x64-gnu": "4.57.0", "@rollup/rollup-win32-x64-msvc": "4.57.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA=="], + "rollup": ["rollup@4.54.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.54.0", "@rollup/rollup-android-arm64": "4.54.0", "@rollup/rollup-darwin-arm64": "4.54.0", "@rollup/rollup-darwin-x64": "4.54.0", "@rollup/rollup-freebsd-arm64": "4.54.0", "@rollup/rollup-freebsd-x64": "4.54.0", "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", "@rollup/rollup-linux-arm-musleabihf": "4.54.0", "@rollup/rollup-linux-arm64-gnu": "4.54.0", "@rollup/rollup-linux-arm64-musl": "4.54.0", "@rollup/rollup-linux-loong64-gnu": "4.54.0", "@rollup/rollup-linux-ppc64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-musl": "4.54.0", "@rollup/rollup-linux-s390x-gnu": "4.54.0", "@rollup/rollup-linux-x64-gnu": "4.54.0", "@rollup/rollup-linux-x64-musl": "4.54.0", "@rollup/rollup-openharmony-arm64": "4.54.0", "@rollup/rollup-win32-arm64-msvc": "4.54.0", "@rollup/rollup-win32-ia32-msvc": "4.54.0", "@rollup/rollup-win32-x64-gnu": "4.54.0", "@rollup/rollup-win32-x64-msvc": "4.54.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw=="], "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], @@ -1210,9 +1225,9 @@ "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "seroval": ["seroval@1.5.0", "", {}, "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw=="], + "seroval": ["seroval@1.4.2", "", {}, "sha512-N3HEHRCZYn3cQbsC4B5ldj9j+tHdf4JZoYPlcI4rRYu0Xy4qN8MQf1Z08EibzB0WpgRG5BGK08FTrmM66eSzKQ=="], - "seroval-plugins": ["seroval-plugins@1.5.0", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA=="], + "seroval-plugins": ["seroval-plugins@1.4.2", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-X7p4MEDTi+60o2sXZ4bnDBhgsUYDSkQEvzYZuJyFqWg9jcoPsHts5nrg5O956py2wyt28lUrBxk0M0/wU8URpA=="], "server-only": ["server-only@0.0.1", "", {}, "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA=="], @@ -1224,7 +1239,7 @@ "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], - "solid-js": ["solid-js@1.9.11", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q=="], + "solid-js": ["solid-js@1.9.10", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", "seroval-plugins": "~1.3.0" } }, "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew=="], "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], @@ -1232,7 +1247,7 @@ "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], - "srvx": ["srvx@0.10.1", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-A//xtfak4eESMWWydSRFUVvCTQbSwivnGCEf8YGPe2eHU0+Z6znfUTCPF0a7oV3sObSOcrXHlL6Bs9vVctfXdg=="], + "srvx": ["srvx@0.10.0", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-NqIsR+wQCfkvvwczBh8J8uM4wTZx41K2lLSEp/3oMp917ODVVMtW5Me4epCmQ3gH8D+0b+/t4xxkUKutyhimTA=="], "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], @@ -1242,7 +1257,7 @@ "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], - "string-width": ["string-width@8.1.1", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw=="], + "string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="], "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], @@ -1296,9 +1311,9 @@ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], + "ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="], - "undici": ["undici@7.19.2", "", {}, "sha512-4VQSpGEGsWzk0VYxyB/wVX/Q7qf9t5znLRgs0dzszr9w9Fej/8RVNQ+S20vdXSAyra/bJ7ZQfGv6ZMj7UEbzSg=="], + "undici": ["undici@7.16.0", "", {}, "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], @@ -1312,13 +1327,13 @@ "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], - "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], + "unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], "unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], - "unstorage": ["unstorage@2.0.0-alpha.5", "", { "peerDependencies": { "@azure/app-configuration": "^1.9.0", "@azure/cosmos": "^4.7.0", "@azure/data-tables": "^13.3.1", "@azure/identity": "^4.13.0", "@azure/keyvault-secrets": "^4.10.0", "@azure/storage-blob": "^12.29.1", "@capacitor/preferences": "^6.0.3 || ^7.0.0", "@deno/kv": ">=0.12.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.35.6", "@vercel/blob": ">=0.27.3", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "chokidar": "^4 || ^5", "db0": ">=0.3.4", "idb-keyval": "^6.2.2", "ioredis": "^5.8.2", "lru-cache": "^11.2.2", "mongodb": "^6 || ^7", "ofetch": "*", "uploadthing": "^7.7.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "chokidar", "db0", "idb-keyval", "ioredis", "lru-cache", "mongodb", "ofetch", "uploadthing"] }, "sha512-Sj8btci21Twnd6M+N+MHhjg3fVn6lAPElPmvFTe0Y/wR0WImErUdA1PzlAaUavHylJ7uDiFwlZDQKm0elG4b7g=="], + "unstorage": ["unstorage@2.0.0-alpha.4", "", { "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6.0.3 || ^7.0.0", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "chokidar": "^4.0.3", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "lru-cache": "^11.2.2", "mongodb": "^6.20.0", "ofetch": "*", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "chokidar", "db0", "idb-keyval", "ioredis", "lru-cache", "mongodb", "ofetch", "uploadthing"] }, "sha512-ywXZMZRfrvmO1giJeMTCw6VUn0ALYxVl8pFqJPStiyQUvgJImejtAHrKvXPj4QGJAoS/iLGcVGF6ljN/lkh1bw=="], "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], @@ -1334,11 +1349,11 @@ "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], - "vite-tsconfig-paths": ["vite-tsconfig-paths@6.0.5", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" } }, "sha512-f/WvY6ekHykUF1rWJUAbCU7iS/5QYDIugwpqJA+ttwKbxSbzNlqlE8vZSrsnxNQciUW+z6lvhlXMaEyZn9MSig=="], + "vite-tsconfig-paths": ["vite-tsconfig-paths@6.0.4", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3", "vite": "*" } }, "sha512-iIsEJ+ek5KqRTK17pmxtgIxXtqr3qDdE6OxrP9mVeGhVDNXRJTKN/l9oMbujTQNzMLe6XZ8qmpztfbkPu2TiFQ=="], "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], - "vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="], + "vitest": ["vitest@4.0.17", "", { "dependencies": { "@vitest/expect": "4.0.17", "@vitest/mocker": "4.0.17", "@vitest/pretty-format": "4.0.17", "@vitest/runner": "4.0.17", "@vitest/snapshot": "4.0.17", "@vitest/spy": "4.0.17", "@vitest/utils": "4.0.17", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.17", "@vitest/browser-preview": "4.0.17", "@vitest/browser-webdriverio": "4.0.17", "@vitest/ui": "4.0.17", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-FQMeF0DJdWY0iOnbv466n/0BudNdKj1l5jYgl5JVTwjSsZSlqyXFt/9+1sEyhR6CLowbZpV7O1sCHrzBhucKKg=="], "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], @@ -1356,7 +1371,7 @@ "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], @@ -1380,9 +1395,75 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], + "@radix-ui/react-arrow/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-collection/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-collection/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-dialog/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], + "@radix-ui/react-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-dropdown-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-dropdown-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-focus-scope/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-navigation-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-navigation-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-popper/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-popper/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-portal/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-scroll-area/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-scroll-area/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-select/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-select/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-switch/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-switch/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-tabs/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-tabs/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-toggle/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-toggle-group/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-toggle-group/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-tooltip/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-tooltip/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-ryJnSmj4UhrGLZZPJ6PKVb4wNPAIkW6iyLy+0TRwazd3L1u0wzMe8RfqevAh2HbcSkoeLiSYnOVDOys4JSGYyg=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Z82FDl1ByxqPEPrAYYeTQVlx2FSHPe1qwX465c+96IRS3fTdSYRoJcRxg3g2fEG5I69z1dSEWQlNRRr0/677mg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], @@ -1392,8 +1473,6 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@tanstack/start-plugin-core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], - "@tanstack/start-plugin-core/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.40", "", {}, "sha512-s3GeJKSQOwBlzdUrj4ISjJj5SfSh+aqn0wjOar4Bx95iV1ETI7F6S/5hLcfAxZ9kXDcyrAkxPlqmd1ZITttf+w=="], "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], @@ -1404,13 +1483,11 @@ "convex/esbuild": ["esbuild@0.27.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.0", "@esbuild/android-arm": "0.27.0", "@esbuild/android-arm64": "0.27.0", "@esbuild/android-x64": "0.27.0", "@esbuild/darwin-arm64": "0.27.0", "@esbuild/darwin-x64": "0.27.0", "@esbuild/freebsd-arm64": "0.27.0", "@esbuild/freebsd-x64": "0.27.0", "@esbuild/linux-arm": "0.27.0", "@esbuild/linux-arm64": "0.27.0", "@esbuild/linux-ia32": "0.27.0", "@esbuild/linux-loong64": "0.27.0", "@esbuild/linux-mips64el": "0.27.0", "@esbuild/linux-ppc64": "0.27.0", "@esbuild/linux-riscv64": "0.27.0", "@esbuild/linux-s390x": "0.27.0", "@esbuild/linux-x64": "0.27.0", "@esbuild/netbsd-arm64": "0.27.0", "@esbuild/netbsd-x64": "0.27.0", "@esbuild/openbsd-arm64": "0.27.0", "@esbuild/openbsd-x64": "0.27.0", "@esbuild/openharmony-arm64": "0.27.0", "@esbuild/sunos-x64": "0.27.0", "@esbuild/win32-arm64": "0.27.0", "@esbuild/win32-ia32": "0.27.0", "@esbuild/win32-x64": "0.27.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA=="], - "data-urls/whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="], - "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - "htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + "nitro/h3": ["h3@2.0.1-rc.5", "", { "dependencies": { "rou3": "^0.7.9", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-qkohAzCab0nLzXNm78tBjZDvtKMTmtygS8BJLT3VPczAQofdqlFXDPkXdLMJN4r05+xqneG8snZJ0HgkERCZTg=="], - "nitro/h3": ["h3@2.0.1-rc.11", "", { "dependencies": { "rou3": "^0.7.12", "srvx": "^0.10.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-2myzjCqy32c1As9TjZW9fNZXtLqNedjFSrdFy2AjFBQQ3LzrnGoDdFDYfC0tV2e4vcyfJ2Sfo/F6NQhO2Ly/Mw=="], + "nitro/srvx": ["srvx@0.9.8", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-RZaxTKJEE/14HYn8COLuUOJAt0U55N9l1Xf6jj+T0GoA01EUH1Xz5JtSUOI+EHn+AEgPCVn7gk6jHJffrr06fQ=="], "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], @@ -1424,6 +1501,10 @@ "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "solid-js/seroval": ["seroval@1.3.2", "", {}, "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ=="], + + "solid-js/seroval-plugins": ["seroval-plugins@1.3.3", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w=="], + "strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "convex/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A=="], diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index 6352d18..8e0e3b9 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -9,11 +9,13 @@ */ import type * as auth from "../auth.js"; +import type * as automod from "../automod.js"; import type * as comments from "../comments.js"; import type * as crons from "../crons.js"; import type * as devSeed from "../devSeed.js"; import type * as devSeedExtra from "../devSeedExtra.js"; import type * as downloads from "../downloads.js"; +import type * as extensions from "../extensions.js"; import type * as githubBackups from "../githubBackups.js"; import type * as githubBackupsNode from "../githubBackupsNode.js"; import type * as githubImport from "../githubImport.js"; @@ -34,6 +36,7 @@ import type * as lib_githubSoulBackup from "../lib/githubSoulBackup.js"; import type * as lib_leaderboards from "../lib/leaderboards.js"; import type * as lib_moderation from "../lib/moderation.js"; import type * as lib_public from "../lib/public.js"; +import type * as lib_resource from "../lib/resource.js"; import type * as lib_searchText from "../lib/searchText.js"; import type * as lib_skillBackfill from "../lib/skillBackfill.js"; import type * as lib_skillPublish from "../lib/skillPublish.js"; @@ -70,11 +73,13 @@ import type { declare const fullApi: ApiFromModules<{ auth: typeof auth; + automod: typeof automod; comments: typeof comments; crons: typeof crons; devSeed: typeof devSeed; devSeedExtra: typeof devSeedExtra; downloads: typeof downloads; + extensions: typeof extensions; githubBackups: typeof githubBackups; githubBackupsNode: typeof githubBackupsNode; githubImport: typeof githubImport; @@ -95,6 +100,7 @@ declare const fullApi: ApiFromModules<{ "lib/leaderboards": typeof lib_leaderboards; "lib/moderation": typeof lib_moderation; "lib/public": typeof lib_public; + "lib/resource": typeof lib_resource; "lib/searchText": typeof lib_searchText; "lib/skillBackfill": typeof lib_skillBackfill; "lib/skillPublish": typeof lib_skillPublish; diff --git a/convex/automod.ts b/convex/automod.ts new file mode 100644 index 0000000..2d777d9 --- /dev/null +++ b/convex/automod.ts @@ -0,0 +1,257 @@ +import { v } from 'convex/values' +import { internal } from './_generated/api' +import type { Doc, Id } from './_generated/dataModel' +import { internalAction, internalMutation, internalQuery } from './_generated/server' + +const CURSOR_KEY = 'skills' +const DEFAULT_BATCH_SIZE = 25 +const MAX_BATCH_SIZE = 100 +const DEFAULT_MAX_BATCHES = 4 +const MAX_MAX_BATCHES = 10 +const OPENAI_AUTOMOD_MODEL = process.env.OPENAI_AUTOMOD_MODEL ?? 'gpt-4.1-mini' + +const SUSPICIOUS_EXTENSIONS = ['.exe', '.dll', '.bat', '.cmd', '.ps1', '.scr', '.com', '.msi'] +const SUSPICIOUS_PHRASES: Array<{ label: string; pattern: RegExp }> = [ + { label: 'free-nitro', pattern: /free\s+nitro/i }, + { label: 'steam-gift', pattern: /steam\s+gift|free\s+steam\s+wallet/i }, + { label: 'token-grabber', pattern: /token\s+grabber|token\s+stealer/i }, + { label: 'wallet-seed', pattern: /seed\s+phrase|wallet\s+drainer|crypto\s+airdrop/i }, + { label: 'credential-harvest', pattern: /passwords?\s+stealer|credential\s+harvest/i }, + { label: 'piracy', pattern: /crack(ed)?\s+version|license\s+key\s+generator/i }, +] + +function clampInt(value: number, min: number, max: number) { + return Math.max(min, Math.min(max, value)) +} + +function extractResponseText(payload: unknown) { + if (!payload || typeof payload !== 'object') return null + const output = (payload as { output?: unknown }).output + if (!Array.isArray(output)) return null + const chunks: string[] = [] + for (const item of output) { + if (!item || typeof item !== 'object') continue + const content = (item as { content?: unknown }).content + if (!Array.isArray(content)) continue + for (const part of content) { + if (!part || typeof part !== 'object') continue + if ((part as { type?: unknown }).type !== 'output_text') continue + const text = (part as { text?: unknown }).text + if (typeof text === 'string' && text.trim()) chunks.push(text) + } + } + const joined = chunks.join('\n').trim() + return joined || null +} + +function buildSkillText(skill: Doc<'skills'>, version: Doc<'skillVersions'> | null) { + const metadata = version?.parsed?.metadata ? JSON.stringify(version.parsed.metadata) : '' + const frontmatter = version?.parsed?.frontmatter ? JSON.stringify(version.parsed.frontmatter) : '' + const filePaths = version?.files?.map((file) => file.path).join('\n') ?? '' + return [skill.slug, skill.displayName, skill.summary ?? '', metadata, frontmatter, filePaths] + .filter(Boolean) + .join('\n') +} + +function scanSkillLocally(skill: Doc<'skills'>, version: Doc<'skillVersions'> | null) { + const findings: string[] = [] + const filePaths = version?.files?.map((file) => file.path.toLowerCase()) ?? [] + const matchedExtensions = new Set() + + for (const filePath of filePaths) { + for (const extension of SUSPICIOUS_EXTENSIONS) { + if (filePath.endsWith(extension)) { + matchedExtensions.add(extension) + } + } + } + + if (matchedExtensions.size > 0) { + findings.push(`bundled executables (${Array.from(matchedExtensions).join(', ')})`) + } + + const text = buildSkillText(skill, version) + for (const rule of SUSPICIOUS_PHRASES) { + if (rule.pattern.test(text)) { + findings.push(`phrase:${rule.label}`) + } + } + + return findings +} + +async function classifyWithOpenAI(args: { + skill: Doc<'skills'> + version: Doc<'skillVersions'> | null +}) { + const apiKey = process.env.OPENAI_API_KEY + if (!apiKey) return null + + const input = [ + `Skill: ${args.skill.slug}`, + `Display name: ${args.skill.displayName}`, + args.skill.summary ? `Summary: ${args.skill.summary}` : null, + `Latest version: ${args.version?.version ?? 'unknown'}`, + args.version?.files?.length + ? `Files: ${args.version.files.map((file) => file.path).slice(0, 80).join(', ')}` + : 'Files: none', + ] + .filter(Boolean) + .join('\n') + + const response = await fetch('https://api.openai.com/v1/responses', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify({ + model: OPENAI_AUTOMOD_MODEL, + instructions: + 'You are helping a marketplace moderator detect malware/scams in skill bundles. ' + + 'Return a JSON object with {"flag": boolean, "reason": string}. ' + + 'Flag true only if the input strongly suggests malware/scams (executables, credential theft, scams). ' + + 'Keep reason short and factual. Return only JSON.', + input, + max_output_tokens: 120, + }), + }) + + if (!response.ok) return null + const payload = (await response.json()) as unknown + const text = extractResponseText(payload) + if (!text) return null + try { + const parsed = JSON.parse(text) as { flag?: boolean; reason?: string } + if (typeof parsed.flag !== 'boolean') return null + return parsed + } catch { + return null + } +} + +export const getSkillAutomodCursorInternal = internalQuery({ + args: {}, + handler: async (ctx) => { + const cursor = await ctx.db + .query('automodCursors') + .withIndex('by_key', (q) => q.eq('key', CURSOR_KEY)) + .unique() + return cursor?.cursorUpdatedAt ?? null + }, +}) + +export const setSkillAutomodCursorInternal = internalMutation({ + args: { cursorUpdatedAt: v.number() }, + handler: async (ctx, args) => { + const existing = await ctx.db + .query('automodCursors') + .withIndex('by_key', (q) => q.eq('key', CURSOR_KEY)) + .unique() + if (existing) { + await ctx.db.patch(existing._id, { cursorUpdatedAt: args.cursorUpdatedAt, updatedAt: Date.now() }) + return + } + await ctx.db.insert('automodCursors', { + key: CURSOR_KEY, + cursorUpdatedAt: args.cursorUpdatedAt, + updatedAt: Date.now(), + }) + }, +}) + +export const getSkillAutomodBatchInternal = internalQuery({ + args: { + cursorUpdatedAt: v.optional(v.number()), + limit: v.optional(v.number()), + }, + handler: async (ctx, args) => { + const limit = clampInt(args.limit ?? DEFAULT_BATCH_SIZE, 1, MAX_BATCH_SIZE) + const cursor = args.cursorUpdatedAt ?? 0 + return ctx.db + .query('skills') + .withIndex('by_updated', (q) => (cursor ? q.gt('updatedAt', cursor) : q)) + .order('asc') + .take(limit) + }, +}) + +export const runSkillAutomodInternal: ReturnType = internalAction({ + args: { + batchSize: v.optional(v.number()), + maxBatches: v.optional(v.number()), + }, + handler: async ( + ctx, + args, + ): Promise< + | { ok: true; processed: number; reported: number; cursorUpdatedAt: number } + | { ok: false; reason: string } + > => { + const automodUserId = process.env.AUTOMOD_USER_ID as Id<'users'> | undefined + if (!automodUserId) { + return { ok: false as const, reason: 'AUTOMOD_USER_ID not configured' } + } + + const batchSize = clampInt(args.batchSize ?? DEFAULT_BATCH_SIZE, 1, MAX_BATCH_SIZE) + const maxBatches = clampInt(args.maxBatches ?? DEFAULT_MAX_BATCHES, 1, MAX_MAX_BATCHES) + let cursorUpdatedAt: number = + (await ctx.runQuery(internal.automod.getSkillAutomodCursorInternal, {})) ?? 0 + + let batches = 0 + let processed = 0 + let reported = 0 + + while (batches < maxBatches) { + const skills = (await ctx.runQuery(internal.automod.getSkillAutomodBatchInternal, { + cursorUpdatedAt, + limit: batchSize, + })) as Doc<'skills'>[] + + if (skills.length === 0) break + + for (const skill of skills) { + if (skill.softDeletedAt) { + cursorUpdatedAt = Math.max(cursorUpdatedAt, skill.updatedAt) + continue + } + + const version = skill.latestVersionId + ? ((await ctx.runQuery(internal.skills.getVersionByIdInternal, { + versionId: skill.latestVersionId, + })) as Doc<'skillVersions'> | null) + : null + + const findings = scanSkillLocally(skill, version) + let reason: string | null = null + + if (findings.length > 0) { + reason = `Automod heuristic flagged: ${findings.join(', ')}` + } else { + const aiResult = await classifyWithOpenAI({ skill, version }) + if (aiResult?.flag) { + reason = `Automod AI flagged: ${aiResult.reason ?? 'suspicious content'}` + } + } + + if (reason) { + const result = (await ctx.runMutation(internal.skills.reportInternal, { + skillId: skill._id, + userId: automodUserId, + reason, + })) as { reported: boolean } + if (result.reported) reported += 1 + } + + cursorUpdatedAt = Math.max(cursorUpdatedAt, skill.updatedAt) + processed += 1 + } + + await ctx.runMutation(internal.automod.setSkillAutomodCursorInternal, { cursorUpdatedAt }) + batches += 1 + if (skills.length < batchSize) break + } + + return { ok: true as const, processed, reported, cursorUpdatedAt } + }, +}) diff --git a/convex/comments.ts b/convex/comments.ts index a3dcc29..63e1a29 100644 --- a/convex/comments.ts +++ b/convex/comments.ts @@ -3,6 +3,7 @@ import type { Doc } from './_generated/dataModel' import { mutation, query } from './_generated/server' import { assertModerator, requireUser } from './lib/access' import { type PublicUser, toPublicUser } from './lib/public' +import { upsertResourceForSkill } from './lib/resource' export const listBySkill = query({ args: { skillId: v.id('skills'), limit: v.optional(v.number()) }, @@ -43,9 +44,19 @@ export const add = mutation({ deletedBy: undefined, }) + const now = Date.now() + const nextStats = { ...skill.stats, comments: skill.stats.comments + 1 } await ctx.db.patch(skill._id, { - stats: { ...skill.stats, comments: skill.stats.comments + 1 }, - updatedAt: Date.now(), + stats: nextStats, + updatedAt: now, + }) + await upsertResourceForSkill(ctx, skill, { + stats: nextStats, + statsDownloads: skill.statsDownloads, + statsStars: skill.statsStars, + statsInstallsCurrent: skill.statsInstallsCurrent, + statsInstallsAllTime: skill.statsInstallsAllTime, + updatedAt: now, }) }, }) @@ -70,9 +81,19 @@ export const remove = mutation({ const skill = await ctx.db.get(comment.skillId) if (skill) { + const now = Date.now() + const nextStats = { ...skill.stats, comments: Math.max(0, skill.stats.comments - 1) } await ctx.db.patch(skill._id, { - stats: { ...skill.stats, comments: Math.max(0, skill.stats.comments - 1) }, - updatedAt: Date.now(), + stats: nextStats, + updatedAt: now, + }) + await upsertResourceForSkill(ctx, skill, { + stats: nextStats, + statsDownloads: skill.statsDownloads, + statsStars: skill.statsStars, + statsInstallsCurrent: skill.statsInstallsCurrent, + statsInstallsAllTime: skill.statsInstallsAllTime, + updatedAt: now, }) } diff --git a/convex/crons.ts b/convex/crons.ts index c35b2ce..c73b945 100644 --- a/convex/crons.ts +++ b/convex/crons.ts @@ -31,4 +31,11 @@ crons.interval( {}, ) +crons.interval( + 'skill-automod', + { minutes: 15 }, + internal.automod.runSkillAutomodInternal, + { batchSize: 25, maxBatches: 4 }, +) + export default crons diff --git a/convex/devSeed.ts b/convex/devSeed.ts index 37f7317..177f1ca 100644 --- a/convex/devSeed.ts +++ b/convex/devSeed.ts @@ -1,7 +1,8 @@ import { v } from 'convex/values' import { internal } from './_generated/api' import type { ActionCtx } from './_generated/server' -import { internalAction, internalMutation } from './_generated/server' +import { action, internalAction, internalMutation } from './_generated/server' +import { assertAdmin, requireUserFromAction } from './lib/access' import { EMBEDDING_DIMENSIONS } from './lib/embeddings' import { parseClawdisMetadata, parseFrontmatter } from './lib/skills' @@ -287,6 +288,21 @@ export const seedNixSkills: ReturnType = internalAction({ handler: seedNixSkillsHandler, }) +const AUTH_BYPASS = process.env.AUTH_BYPASS === 'true' + +export const seedNixSkillsPublic = action({ + args: { + reset: v.optional(v.boolean()), + }, + handler: async (ctx, args) => { + if (!AUTH_BYPASS) { + const { user } = await requireUserFromAction(ctx) + assertAdmin(user) + } + return seedNixSkillsHandler(ctx, args) + }, +}) + async function seedPadelSkillHandler( ctx: ActionCtx, args: SeedActionArgs, @@ -377,7 +393,31 @@ export const seedSkillMutation = internalMutation({ updatedAt: now, })) + const resourceId = await ctx.db.insert('resources', { + type: 'skill', + slug: args.slug, + displayName: args.displayName, + summary: args.summary, + ownerUserId: userId, + ownerHandle: 'local', + softDeletedAt: undefined, + statsDownloads: 0, + statsStars: 0, + statsInstallsCurrent: 0, + statsInstallsAllTime: 0, + stats: { + downloads: 0, + installsCurrent: 0, + installsAllTime: 0, + stars: 0, + versions: 0, + comments: 0, + }, + createdAt: now, + updatedAt: now, + }) const skillId = await ctx.db.insert('skills', { + resourceId, slug: args.slug, displayName: args.displayName, summary: args.summary, @@ -385,7 +425,6 @@ export const seedSkillMutation = internalMutation({ latestVersionId: undefined, tags: {}, softDeletedAt: undefined, - badges: { redactionApproved: undefined }, statsDownloads: 0, statsStars: 0, statsInstallsCurrent: 0, diff --git a/convex/devSeedExtra.ts b/convex/devSeedExtra.ts index 8b7bdf5..2912598 100644 --- a/convex/devSeedExtra.ts +++ b/convex/devSeedExtra.ts @@ -11,7 +11,8 @@ import { internal } from './_generated/api' import type { Id } from './_generated/dataModel' import type { ActionCtx } from './_generated/server' import { internalAction, internalMutation } from './_generated/server' -import { parseClawdisMetadata, parseFrontmatter } from './lib/skills' +import { parseFrontmatter, parseMoltbotMetadata } from './lib/skills' +import { upsertResourceForSkill } from './lib/resource' type SeedSkillSpec = { slug: string @@ -66,7 +67,7 @@ Use this skill to ${summary.toLowerCase()}. summary, version: '0.1.0', metadata: { - clawdbot: { + moltbot: { nix: { plugin: `github:example/${slug}`, systems: ['aarch64-darwin', 'x86_64-linux'], @@ -478,19 +479,29 @@ export const applyRandomStats = internalMutation({ }), }, handler: async (ctx, args) => { + const skill = await ctx.db.get(args.skillId) + if (!skill) return + const nextStats = { + downloads: args.stats.downloads, + stars: args.stats.stars, + installsCurrent: args.stats.installsCurrent, + installsAllTime: args.stats.installsAllTime, + versions: 1, + comments: 0, + } await ctx.db.patch(args.skillId, { statsDownloads: args.stats.downloads, statsStars: args.stats.stars, statsInstallsCurrent: args.stats.installsCurrent, statsInstallsAllTime: args.stats.installsAllTime, - stats: { - downloads: args.stats.downloads, - stars: args.stats.stars, - installsCurrent: args.stats.installsCurrent, - installsAllTime: args.stats.installsAllTime, - versions: 1, - comments: 0, - }, + stats: nextStats, + }) + await upsertResourceForSkill(ctx, skill, { + statsDownloads: args.stats.downloads, + statsStars: args.stats.stars, + statsInstallsCurrent: args.stats.installsCurrent, + statsInstallsAllTime: args.stats.installsAllTime, + stats: nextStats, }) }, }) @@ -505,7 +516,7 @@ export const seedExtraSkillsInternal = internalAction({ for (const spec of EXTRA_SEED_SKILLS) { const skillMd = injectMetadata(spec.rawSkillMd, spec.metadata) const frontmatter = parseFrontmatter(skillMd) - const clawdis = parseClawdisMetadata(frontmatter) + const moltbot = parseMoltbotMetadata(frontmatter) const storageId = await ctx.storage.store(new Blob([skillMd], { type: 'text/markdown' })) const result = (await ctx.runMutation(internal.devSeed.seedSkillMutation, { @@ -513,7 +524,7 @@ export const seedExtraSkillsInternal = internalAction({ storageId, metadata: spec.metadata, frontmatter, - clawdis, + moltbot, skillMd, slug: spec.slug, displayName: spec.displayName, diff --git a/convex/extensions.ts b/convex/extensions.ts new file mode 100644 index 0000000..424cd45 --- /dev/null +++ b/convex/extensions.ts @@ -0,0 +1,110 @@ +import { paginationOptsValidator } from 'convex/server' +import { v } from 'convex/values' +import { paginator } from 'convex-helpers/server/pagination' +import type { Doc } from './_generated/dataModel' +import type { QueryCtx } from './_generated/server' +import { query } from './_generated/server' +import { getResourceBadgeMap, getResourceBadgeMaps, type ResourceBadgeMap } from './lib/badges' +import schema from './schema' + +export type PublicExtension = Pick< + Doc<'resources'>, + | '_id' + | '_creationTime' + | 'type' + | 'slug' + | 'displayName' + | 'summary' + | 'ownerUserId' + | 'ownerHandle' + | 'stats' + | 'createdAt' + | 'updatedAt' +> & { + badges?: ResourceBadgeMap +} + +async function buildPublicExtensions(ctx: QueryCtx, resources: Doc<'resources'>[]) { + const badgeMapByResourceId = await getResourceBadgeMaps( + ctx, + resources.map((resource) => resource._id), + ) + return resources + .map((resource) => + toPublicExtension({ + ...resource, + badges: badgeMapByResourceId.get(resource._id) ?? {}, + }), + ) + .filter(Boolean) +} + +export const listPublicPage = query({ + args: { + paginationOpts: paginationOptsValidator, + }, + handler: async (ctx, args) => { + const result = await paginator(ctx.db, schema) + .query('resources') + .withIndex('by_type_active_updated', (q) => + q.eq('type', 'extension').eq('softDeletedAt', undefined), + ) + .order('desc') + .paginate(args.paginationOpts) + + const page = await buildPublicExtensions(ctx, result.page) + return { + ...result, + page, + } + }, +}) + +export const listByOwner = query({ + args: { ownerUserId: v.id('users'), limit: v.optional(v.number()) }, + handler: async (ctx, args) => { + const limit = args.limit ?? 50 + const resources = await ctx.db + .query('resources') + .withIndex('by_type_owner_updated', (q) => + q.eq('type', 'extension').eq('ownerUserId', args.ownerUserId), + ) + .order('desc') + .take(Math.min(limit * 3, 200)) + const filtered = resources.filter((resource) => !resource.softDeletedAt).slice(0, limit) + return buildPublicExtensions(ctx, filtered) + }, +}) + +export const getBySlug = query({ + args: { slug: v.string() }, + handler: async (ctx, args) => { + const resource = await ctx.db + .query('resources') + .withIndex('by_type_slug', (q) => q.eq('type', 'extension').eq('slug', args.slug)) + .unique() + if (!resource) return null + const badges = await getResourceBadgeMap(ctx, resource._id) + return toPublicExtension({ ...resource, badges }) + }, +}) + +function toPublicExtension( + resource: (Doc<'resources'> & { badges?: ResourceBadgeMap }) | null | undefined, +): PublicExtension | null { + if (!resource || resource.softDeletedAt || resource.type !== 'extension') return null + return { + _id: resource._id, + _creationTime: resource._creationTime, + type: resource.type, + slug: resource.slug, + displayName: resource.displayName, + summary: resource.summary, + ownerUserId: resource.ownerUserId, + ownerHandle: resource.ownerHandle, + stats: resource.stats, + badges: resource.badges, + createdAt: resource.createdAt, + updatedAt: resource.updatedAt, + } +} diff --git a/convex/githubBackupsNode.ts b/convex/githubBackupsNode.ts index 8000b5c..d13dad1 100644 --- a/convex/githubBackupsNode.ts +++ b/convex/githubBackupsNode.ts @@ -7,6 +7,7 @@ import type { ActionCtx } from './_generated/server' import { internalAction } from './_generated/server' import { backupSkillToGitHub, + deleteSkillFromGitHub, fetchGitHubSkillMeta, getGitHubBackupContext, isGitHubBackupConfigured, @@ -78,6 +79,23 @@ export const backupSkillForPublishInternal = internalAction({ }, }) +export const deleteSkillBackupInternal = internalAction({ + args: { + slug: v.string(), + ownerHandles: v.array(v.string()), + }, + handler: async (_ctx, args) => { + if (!isGitHubBackupConfigured()) { + return { skipped: true as const, deleted: false as const } + } + const result = await deleteSkillFromGitHub({ + slug: args.slug, + ownerHandles: args.ownerHandles, + }) + return { skipped: false as const, deleted: result.deleted } + }, +}) + export async function syncGitHubBackupsInternalHandler( ctx: ActionCtx, args: SyncGitHubBackupsInternalArgs, diff --git a/convex/http.ts b/convex/http.ts index ec3902f..d09ba07 100644 --- a/convex/http.ts +++ b/convex/http.ts @@ -1,4 +1,4 @@ -import { ApiRoutes, LegacyApiRoutes } from 'clawhub-schema' +import { ApiRoutes, LegacyApiRoutes } from 'molthub-schema' import { httpRouter } from 'convex/server' import { auth } from './auth' import { downloadZip } from './downloads' diff --git a/convex/httpApi.ts b/convex/httpApi.ts index 4d4dce6..7099b18 100644 --- a/convex/httpApi.ts +++ b/convex/httpApi.ts @@ -5,7 +5,7 @@ import { CliSkillDeleteRequestSchema, CliTelemetrySyncRequestSchema, parseArk, -} from 'clawhub-schema' +} from 'molthub-schema' import { api, internal } from './_generated/api' import type { Id } from './_generated/dataModel' import type { ActionCtx } from './_generated/server' diff --git a/convex/httpApiV1.handlers.test.ts b/convex/httpApiV1.handlers.test.ts index c3d9e7e..245d48e 100644 --- a/convex/httpApiV1.handlers.test.ts +++ b/convex/httpApiV1.handlers.test.ts @@ -213,7 +213,7 @@ describe('httpApiV1 handlers', () => { changelog: 'c', files: [], }, - owner: { handle: 'p', displayName: 'Peter', image: null }, + owner: { handle: 'p', userId: 'users:1', displayName: 'Peter', image: null }, } } if ('versionId' in args) return { version: '1.0.0' } diff --git a/convex/httpApiV1.ts b/convex/httpApiV1.ts index 7e4c6d9..a7092ea 100644 --- a/convex/httpApiV1.ts +++ b/convex/httpApiV1.ts @@ -1,4 +1,4 @@ -import { CliPublishRequestSchema, parseArk } from 'clawhub-schema' +import { CliPublishRequestSchema, parseArk } from 'molthub-schema' import { api, internal } from './_generated/api' import type { Doc, Id } from './_generated/dataModel' import type { ActionCtx } from './_generated/server' @@ -267,7 +267,7 @@ async function skillsGetRouterV1Handler(ctx: ActionCtx, request: Request) { owner: result.owner ? { handle: result.owner.handle ?? null, - userId: result.owner._id, + userId: '_id' in result.owner ? result.owner._id : null, displayName: result.owner.displayName ?? null, image: result.owner.image ?? null, } @@ -886,6 +886,7 @@ async function soulsGetRouterV1Handler(ctx: ActionCtx, request: Request) { owner: result.owner ? { handle: result.owner.handle ?? null, + userId: '_id' in result.owner ? result.owner._id : null, displayName: result.owner.displayName ?? null, image: result.owner.image ?? null, } diff --git a/convex/lib/access.ts b/convex/lib/access.ts index e042e19..c4cc9d7 100644 --- a/convex/lib/access.ts +++ b/convex/lib/access.ts @@ -5,9 +5,25 @@ import type { ActionCtx, MutationCtx, QueryCtx } from '../_generated/server' export type Role = 'admin' | 'moderator' | 'user' +const AUTH_BYPASS = process.env.AUTH_BYPASS === 'true' +const BYPASS_HANDLE = 'local' + +async function getBypassUser(ctx: MutationCtx | QueryCtx) { + const user = await ctx.db + .query('users') + .withIndex('handle', (q) => q.eq('handle', BYPASS_HANDLE)) + .unique() + if (!user || user.deletedAt) throw new Error('User not found') + return user +} + export async function requireUser(ctx: MutationCtx | QueryCtx) { const userId = await getAuthUserId(ctx) - if (!userId) throw new Error('Unauthorized') + if (!userId) { + if (!AUTH_BYPASS) throw new Error('Unauthorized') + const user = await getBypassUser(ctx) + return { userId: user._id, user } + } const user = await ctx.db.get(userId) if (!user || user.deletedAt) throw new Error('User not found') return { userId, user } @@ -15,7 +31,14 @@ export async function requireUser(ctx: MutationCtx | QueryCtx) { export async function requireUserFromAction(ctx: ActionCtx) { const userId = await getAuthUserId(ctx) - if (!userId) throw new Error('Unauthorized') + if (!userId) { + if (!AUTH_BYPASS) throw new Error('Unauthorized') + const user = await ctx.runQuery(internal.users.getByHandleInternal, { + handle: BYPASS_HANDLE, + }) + if (!user || user.deletedAt) throw new Error('User not found') + return { userId: user._id, user: user as Doc<'users'> } + } const user = await ctx.runQuery(internal.users.getByIdInternal, { userId }) if (!user || user.deletedAt) throw new Error('User not found') return { userId, user: user as Doc<'users'> } diff --git a/convex/lib/badges.ts b/convex/lib/badges.ts index 37a899f..39218a3 100644 --- a/convex/lib/badges.ts +++ b/convex/lib/badges.ts @@ -1,50 +1,50 @@ import type { Doc, Id } from '../_generated/dataModel' import type { QueryCtx } from '../_generated/server' -type BadgeKind = Doc<'skillBadges'>['kind'] +type BadgeKind = Doc<'resourceBadges'>['kind'] -export type SkillBadgeMap = Partial; at: number }>> +export type ResourceBadgeMap = Partial; at: number }>> -export type SkillBadgeSource = { badges?: SkillBadgeMap | null } +export type ResourceBadgeSource = { badges?: ResourceBadgeMap | null } type BadgeCtx = Pick -export function isSkillHighlighted(skill: SkillBadgeSource) { - return Boolean(skill.badges?.highlighted) +export function isResourceHighlighted(resource: ResourceBadgeSource) { + return Boolean(resource.badges?.highlighted) } -export function isSkillOfficial(skill: SkillBadgeSource) { - return Boolean(skill.badges?.official) +export function isResourceOfficial(resource: ResourceBadgeSource) { + return Boolean(resource.badges?.official) } -export function isSkillDeprecated(skill: SkillBadgeSource) { - return Boolean(skill.badges?.deprecated) +export function isResourceDeprecated(resource: ResourceBadgeSource) { + return Boolean(resource.badges?.deprecated) } -export function buildBadgeMap(records: Doc<'skillBadges'>[]): SkillBadgeMap { - return records.reduce((acc, record) => { +export function buildBadgeMap(records: Doc<'resourceBadges'>[]): ResourceBadgeMap { + return records.reduce((acc, record) => { acc[record.kind] = { byUserId: record.byUserId, at: record.at } return acc }, {}) } -export async function getSkillBadgeMap( +export async function getResourceBadgeMap( ctx: BadgeCtx, - skillId: Id<'skills'>, -): Promise { + resourceId: Id<'resources'>, +): Promise { const records = await ctx.db - .query('skillBadges') - .withIndex('by_skill', (q) => q.eq('skillId', skillId)) + .query('resourceBadges') + .withIndex('by_resource', (q) => q.eq('resourceId', resourceId)) .collect() return buildBadgeMap(records) } -export async function getSkillBadgeMaps( +export async function getResourceBadgeMaps( ctx: BadgeCtx, - skillIds: Array>, -): Promise, SkillBadgeMap>> { + resourceIds: Array>, +): Promise, ResourceBadgeMap>> { const entries = await Promise.all( - skillIds.map(async (skillId) => [skillId, await getSkillBadgeMap(ctx, skillId)] as const), + resourceIds.map(async (resourceId) => [resourceId, await getResourceBadgeMap(ctx, resourceId)] as const), ) return new Map(entries) } diff --git a/convex/lib/githubBackup.ts b/convex/lib/githubBackup.ts index c5ee141..d62dd33 100644 --- a/convex/lib/githubBackup.ts +++ b/convex/lib/githubBackup.ts @@ -233,6 +233,82 @@ export async function backupSkillToGitHub( ) } +export async function deleteSkillFromGitHub( + params: { slug: string; ownerHandles: string[] }, + context?: GitHubBackupContext, +) { + if (!isGitHubBackupConfigured()) return { deleted: false as const } + + const resolved = context ?? (await getGitHubBackupContext()) + const ref = await githubGet( + resolved.token, + `/repos/${resolved.repoOwner}/${resolved.repoName}/git/ref/heads/${resolved.branch}`, + ) + const baseCommitSha = ref.object.sha + const baseCommit = await githubGet( + resolved.token, + `/repos/${resolved.repoOwner}/${resolved.repoName}/git/commits/${baseCommitSha}`, + ) + const baseTreeSha = baseCommit.tree.sha + const existingTree = await githubGet( + resolved.token, + `/repos/${resolved.repoOwner}/${resolved.repoName}/git/trees/${baseTreeSha}?recursive=1`, + ) + + const ownerHandles = Array.from( + new Set(params.ownerHandles.map((handle) => handle.trim()).filter(Boolean)), + ) + if (ownerHandles.length === 0) return { deleted: false as const } + + const prefixes = ownerHandles.map( + (handle) => `${buildSkillRoot(resolved.root, handle, params.slug)}/`, + ) + + const entriesToDelete = (existingTree.tree ?? []).filter( + (entry) => entry.type === 'blob' && prefixes.some((prefix) => entry.path?.startsWith(prefix)), + ) + + if (entriesToDelete.length === 0) { + return { deleted: false as const } + } + + const treeEntries = entriesToDelete.map((entry) => ({ + path: entry.path ?? '', + mode: '100644' as const, + type: 'blob' as const, + sha: null, + })) + + const newTree = await githubPost<{ sha: string }>( + resolved.token, + `/repos/${resolved.repoOwner}/${resolved.repoName}/git/trees`, + { + base_tree: baseTreeSha, + tree: treeEntries, + }, + ) + + const commit = await githubPost( + resolved.token, + `/repos/${resolved.repoOwner}/${resolved.repoName}/git/commits`, + { + message: `skill: delete ${params.slug}`, + tree: newTree.sha, + parents: [baseCommitSha], + }, + ) + + await githubPatch( + resolved.token, + `/repos/${resolved.repoOwner}/${resolved.repoName}/git/refs/heads/${resolved.branch}`, + { + sha: commit.sha, + }, + ) + + return { deleted: true as const } +} + function buildMetaFile( params: BackupParams, existing: MetaFile | null, diff --git a/convex/lib/githubImport.ts b/convex/lib/githubImport.ts index de7f7db..890565a 100644 --- a/convex/lib/githubImport.ts +++ b/convex/lib/githubImport.ts @@ -1,4 +1,4 @@ -import { TEXT_FILE_EXTENSION_SET } from 'clawhub-schema' +import { TEXT_FILE_EXTENSION_SET } from 'molthub-schema' import { zipSync } from 'fflate' import semver from 'semver' import { parseFrontmatter } from './skills' diff --git a/convex/lib/moderation.ts b/convex/lib/moderation.ts index 095afdc..b660148 100644 --- a/convex/lib/moderation.ts +++ b/convex/lib/moderation.ts @@ -47,3 +47,7 @@ export function deriveModerationFlags({ return Array.from(flags) } + +export function isSkillPublic(skill: Pick, 'moderationStatus'>) { + return !skill.moderationStatus || skill.moderationStatus === 'active' +} diff --git a/convex/lib/public.ts b/convex/lib/public.ts index 84d1df0..f331b3b 100644 --- a/convex/lib/public.ts +++ b/convex/lib/public.ts @@ -1,4 +1,5 @@ import type { Doc } from '../_generated/dataModel' +import type { ResourceBadgeMap } from './badges' export type PublicUser = Pick< Doc<'users'>, @@ -13,15 +14,15 @@ export type PublicSkill = Pick< | 'displayName' | 'summary' | 'ownerUserId' - | 'canonicalSkillId' | 'forkOf' | 'latestVersionId' | 'tags' - | 'badges' | 'stats' | 'createdAt' | 'updatedAt' -> +> & { + badges?: ResourceBadgeMap +} export type PublicSoul = Pick< Doc<'souls'>, @@ -36,7 +37,9 @@ export type PublicSoul = Pick< | 'stats' | 'createdAt' | 'updatedAt' -> +> & { + badges?: ResourceBadgeMap +} export function toPublicUser(user: Doc<'users'> | null | undefined): PublicUser | null { if (!user || user.deletedAt) return null @@ -51,7 +54,9 @@ export function toPublicUser(user: Doc<'users'> | null | undefined): PublicUser } } -export function toPublicSkill(skill: Doc<'skills'> | null | undefined): PublicSkill | null { +export function toPublicSkill( + skill: (Doc<'skills'> & { badges?: ResourceBadgeMap }) | null | undefined, +): PublicSkill | null { if (!skill || skill.softDeletedAt) return null if (skill.moderationStatus && skill.moderationStatus !== 'active') return null if (skill.moderationFlags?.includes('blocked.malware')) return null @@ -62,7 +67,6 @@ export function toPublicSkill(skill: Doc<'skills'> | null | undefined): PublicSk displayName: skill.displayName, summary: skill.summary, ownerUserId: skill.ownerUserId, - canonicalSkillId: skill.canonicalSkillId, forkOf: skill.forkOf, latestVersionId: skill.latestVersionId, tags: skill.tags, @@ -73,7 +77,9 @@ export function toPublicSkill(skill: Doc<'skills'> | null | undefined): PublicSk } } -export function toPublicSoul(soul: Doc<'souls'> | null | undefined): PublicSoul | null { +export function toPublicSoul( + soul: (Doc<'souls'> & { stats?: Doc<'souls'>['stats']; badges?: ResourceBadgeMap }) | null | undefined, +): PublicSoul | null { if (!soul || soul.softDeletedAt) return null return { _id: soul._id, @@ -85,6 +91,7 @@ export function toPublicSoul(soul: Doc<'souls'> | null | undefined): PublicSoul latestVersionId: soul.latestVersionId, tags: soul.tags, stats: soul.stats, + badges: soul.badges, createdAt: soul.createdAt, updatedAt: soul.updatedAt, } diff --git a/convex/lib/resource.ts b/convex/lib/resource.ts new file mode 100644 index 0000000..a655cec --- /dev/null +++ b/convex/lib/resource.ts @@ -0,0 +1,96 @@ +import type { Doc, Id } from '../_generated/dataModel' +import type { MutationCtx } from '../_generated/server' + +type ResourceInsert = Omit, '_id' | '_creationTime'> + +async function resolveOwnerHandle(ctx: MutationCtx, ownerUserId: Id<'users'>) { + const owner = await ctx.db.get(ownerUserId) + return owner?.handle ?? owner?._id ?? undefined +} + +function buildSkillResource(skill: Doc<'skills'>, ownerHandle?: string): ResourceInsert { + return { + type: 'skill', + slug: skill.slug, + displayName: skill.displayName, + summary: skill.summary, + ownerUserId: skill.ownerUserId, + ownerHandle, + softDeletedAt: skill.softDeletedAt, + statsDownloads: skill.statsDownloads, + statsStars: skill.statsStars, + statsInstallsCurrent: skill.statsInstallsCurrent, + statsInstallsAllTime: skill.statsInstallsAllTime, + stats: skill.stats, + createdAt: skill.createdAt, + updatedAt: skill.updatedAt, + } +} + +function buildSoulResource(soul: Doc<'souls'>, ownerHandle?: string): ResourceInsert { + return { + type: 'soul', + slug: soul.slug, + displayName: soul.displayName, + summary: soul.summary, + ownerUserId: soul.ownerUserId, + ownerHandle, + softDeletedAt: soul.softDeletedAt, + statsDownloads: soul.stats.downloads, + statsStars: soul.stats.stars, + statsInstallsCurrent: undefined, + statsInstallsAllTime: undefined, + stats: { + downloads: soul.stats.downloads, + stars: soul.stats.stars, + versions: soul.stats.versions, + comments: soul.stats.comments, + }, + createdAt: soul.createdAt, + updatedAt: soul.updatedAt, + } +} + +export async function upsertResourceForSkill( + ctx: MutationCtx, + skill: Doc<'skills'>, + overrides?: Partial, +) { + const resolvedOwnerHandle = overrides?.ownerHandle ?? (await resolveOwnerHandle(ctx, skill.ownerUserId)) + if (skill.resourceId) { + const existing = await ctx.db.get(skill.resourceId) + if (existing) { + await ctx.db.patch(skill.resourceId, { ...overrides, ownerHandle: resolvedOwnerHandle }) + return skill.resourceId + } + } + const resourceId = await ctx.db.insert('resources', { + ...buildSkillResource(skill, resolvedOwnerHandle), + ...overrides, + ownerHandle: resolvedOwnerHandle, + }) + await ctx.db.patch(skill._id, { resourceId }) + return resourceId +} + +export async function upsertResourceForSoul( + ctx: MutationCtx, + soul: Doc<'souls'>, + overrides?: Partial, +) { + const resolvedOwnerHandle = overrides?.ownerHandle ?? (await resolveOwnerHandle(ctx, soul.ownerUserId)) + if (soul.resourceId) { + const existing = await ctx.db.get(soul.resourceId) + if (existing) { + await ctx.db.patch(soul.resourceId, { ...overrides, ownerHandle: resolvedOwnerHandle }) + return soul.resourceId + } + } + const resourceId = await ctx.db.insert('resources', { + ...buildSoulResource(soul, resolvedOwnerHandle), + ...overrides, + ownerHandle: resolvedOwnerHandle, + }) + await ctx.db.patch(soul._id, { resourceId }) + return resourceId +} diff --git a/convex/lib/skillPublish.ts b/convex/lib/skillPublish.ts index c72910c..16c6960 100644 --- a/convex/lib/skillPublish.ts +++ b/convex/lib/skillPublish.ts @@ -3,10 +3,10 @@ import semver from 'semver' import { api, internal } from '../_generated/api' import type { Doc, Id } from '../_generated/dataModel' import type { ActionCtx, MutationCtx } from '../_generated/server' -import { getSkillBadgeMap, isSkillHighlighted } from './badges' +import { getResourceBadgeMap, isResourceHighlighted } from './badges' import { generateChangelogForPublish } from './changelog' import { generateEmbedding } from './embeddings' -import type { PublicUser } from './public' +import type { PublicSkill, PublicUser } from './public' import { buildEmbeddingText, getFrontmatterMetadata, @@ -220,14 +220,16 @@ export async function queueHighlightedWebhook(ctx: MutationCtx, skillId: Id<'ski const owner = await ctx.db.get(skill.ownerUserId) const latestVersion = skill.latestVersionId ? await ctx.db.get(skill.latestVersionId) : null - const badges = await getSkillBadgeMap(ctx, skillId) + const badges = skill.resourceId + ? await getResourceBadgeMap(ctx, skill.resourceId) + : {} const payload: WebhookSkillPayload = { slug: skill.slug, displayName: skill.displayName, summary: skill.summary ?? undefined, version: latestVersion?.version ?? undefined, ownerHandle: owner?.handle ?? owner?.name ?? undefined, - highlighted: isSkillHighlighted({ badges }), + highlighted: isResourceHighlighted({ badges }), tags: Object.keys(skill.tags ?? {}), } @@ -264,7 +266,7 @@ async function schedulePublishWebhook( ) { const result = (await ctx.runQuery(api.skills.getBySlug, { slug: params.slug, - })) as { skill: Doc<'skills'>; owner: PublicUser | null } | null + })) as { skill: PublicSkill; owner: PublicUser | null } | null if (!result?.skill) return const payload: WebhookSkillPayload = { @@ -273,7 +275,7 @@ async function schedulePublishWebhook( summary: result.skill.summary ?? undefined, version: params.version, ownerHandle: result.owner?.handle ?? result.owner?.name ?? undefined, - highlighted: isSkillHighlighted(result.skill), + highlighted: isResourceHighlighted(result.skill), tags: Object.keys(result.skill.tags ?? {}), } diff --git a/convex/lib/skills.ts b/convex/lib/skills.ts index 485d66d..9c11ba9 100644 --- a/convex/lib/skills.ts +++ b/convex/lib/skills.ts @@ -7,7 +7,7 @@ import { parseArk, type SkillInstallSpec, TEXT_FILE_EXTENSION_SET, -} from 'clawhub-schema' +} from 'molthub-schema' import { parse as parseYaml } from 'yaml' export type ParsedSkillFrontmatter = Record @@ -59,42 +59,45 @@ export function getFrontmatterMetadata(frontmatter: ParsedSkillFrontmatter) { return undefined } -export function parseClawdisMetadata(frontmatter: ParsedSkillFrontmatter) { +export function parseMoltbotMetadata(frontmatter: ParsedSkillFrontmatter) { const metadata = getFrontmatterMetadata(frontmatter) const metadataRecord = metadata && typeof metadata === 'object' && !Array.isArray(metadata) ? (metadata as Record) : undefined + const moltbotMeta = metadataRecord?.moltbot const clawdbotMeta = metadataRecord?.clawdbot const clawdisMeta = metadataRecord?.clawdis const metadataSource = - clawdbotMeta && typeof clawdbotMeta === 'object' && !Array.isArray(clawdbotMeta) - ? (clawdbotMeta as Record) - : clawdisMeta && typeof clawdisMeta === 'object' && !Array.isArray(clawdisMeta) - ? (clawdisMeta as Record) - : undefined - const clawdisRaw = metadataSource ?? frontmatter.clawdis - if (!clawdisRaw || typeof clawdisRaw !== 'object' || Array.isArray(clawdisRaw)) return undefined + moltbotMeta && typeof moltbotMeta === 'object' && !Array.isArray(moltbotMeta) + ? (moltbotMeta as Record) + : clawdbotMeta && typeof clawdbotMeta === 'object' && !Array.isArray(clawdbotMeta) + ? (clawdbotMeta as Record) + : clawdisMeta && typeof clawdisMeta === 'object' && !Array.isArray(clawdisMeta) + ? (clawdisMeta as Record) + : undefined + const moltbotRaw = metadataSource ?? frontmatter.moltbot ?? frontmatter.clawdis + if (!moltbotRaw || typeof moltbotRaw !== 'object' || Array.isArray(moltbotRaw)) return undefined try { - const clawdisObj = clawdisRaw as Record + const moltbotObj = moltbotRaw as Record const requiresRaw = - typeof clawdisObj.requires === 'object' && clawdisObj.requires !== null - ? (clawdisObj.requires as Record) + typeof moltbotObj.requires === 'object' && moltbotObj.requires !== null + ? (moltbotObj.requires as Record) : undefined - const installRaw = Array.isArray(clawdisObj.install) ? (clawdisObj.install as unknown[]) : [] + const installRaw = Array.isArray(moltbotObj.install) ? (moltbotObj.install as unknown[]) : [] const install = installRaw .map((entry) => parseInstallSpec(entry)) .filter((entry): entry is SkillInstallSpec => Boolean(entry)) - const osRaw = normalizeStringList(clawdisObj.os) + const osRaw = normalizeStringList(moltbotObj.os) const metadata: ClawdisSkillMetadata = {} - if (typeof clawdisObj.always === 'boolean') metadata.always = clawdisObj.always - if (typeof clawdisObj.emoji === 'string') metadata.emoji = clawdisObj.emoji - if (typeof clawdisObj.homepage === 'string') metadata.homepage = clawdisObj.homepage - if (typeof clawdisObj.skillKey === 'string') metadata.skillKey = clawdisObj.skillKey - if (typeof clawdisObj.primaryEnv === 'string') metadata.primaryEnv = clawdisObj.primaryEnv - if (typeof clawdisObj.cliHelp === 'string') metadata.cliHelp = clawdisObj.cliHelp + if (typeof moltbotObj.always === 'boolean') metadata.always = moltbotObj.always + if (typeof moltbotObj.emoji === 'string') metadata.emoji = moltbotObj.emoji + if (typeof moltbotObj.homepage === 'string') metadata.homepage = moltbotObj.homepage + if (typeof moltbotObj.skillKey === 'string') metadata.skillKey = moltbotObj.skillKey + if (typeof moltbotObj.primaryEnv === 'string') metadata.primaryEnv = moltbotObj.primaryEnv + if (typeof moltbotObj.cliHelp === 'string') metadata.cliHelp = moltbotObj.cliHelp if (osRaw.length > 0) metadata.os = osRaw if (requiresRaw) { @@ -112,17 +115,21 @@ export function parseClawdisMetadata(frontmatter: ParsedSkillFrontmatter) { } if (install.length > 0) metadata.install = install - const nix = parseNixPluginSpec(clawdisObj.nix) + const nix = parseNixPluginSpec(moltbotObj.nix) if (nix) metadata.nix = nix - const config = parseClawdbotConfigSpec(clawdisObj.config) + const config = parseClawdbotConfigSpec(moltbotObj.config) if (config) metadata.config = config - return parseArk(ClawdisSkillMetadataSchema, metadata, 'Clawdis metadata') + return parseArk(ClawdisSkillMetadataSchema, metadata, 'Moltbot metadata') } catch { return undefined } } +export function parseClawdisMetadata(frontmatter: ParsedSkillFrontmatter) { + return parseMoltbotMetadata(frontmatter) +} + export function isTextFile(path: string, contentType?: string | null) { const trimmed = path.trim().toLowerCase() if (!trimmed) return false diff --git a/convex/lib/webhooks.ts b/convex/lib/webhooks.ts index 2d797a7..141a3b8 100644 --- a/convex/lib/webhooks.ts +++ b/convex/lib/webhooks.ts @@ -81,9 +81,8 @@ export function buildDiscordPayload( } export function buildSkillUrl(skill: WebhookSkillPayload, siteUrl: string) { - const owner = skill.ownerHandle?.trim() - if (owner) return `${siteUrl}/${owner}/${skill.slug}` - return `${siteUrl}/skills/${skill.slug}` + const owner = skill.ownerHandle?.trim() || 'unknown' + return `${siteUrl}/skills/${owner}/${skill.slug}` } function buildDescription(event: WebhookEvent, skill: WebhookSkillPayload) { diff --git a/convex/maintenance.ts b/convex/maintenance.ts index ed4105e..4518b03 100644 --- a/convex/maintenance.ts +++ b/convex/maintenance.ts @@ -265,11 +265,9 @@ type FingerprintBackfillPageResult = { isDone: boolean } -type BadgeBackfillStats = { - skillsScanned: number - skillsPatched: number - highlightsPatched: number -} +type BadgeKind = Doc<'resourceBadges'>['kind'] + +type LegacySkillBadges = Partial; at: number }>> type SkillBadgeTableBackfillStats = { skillsScanned: number @@ -278,11 +276,12 @@ type SkillBadgeTableBackfillStats = { type BadgeBackfillPageItem = { skillId: Id<'skills'> + resourceId?: Id<'resources'> ownerUserId: Id<'users'> createdAt?: number updatedAt?: number batch?: string - badges?: Doc<'skills'>['badges'] + legacyBadges?: LegacySkillBadges } type BadgeBackfillPageResult = { @@ -291,7 +290,56 @@ type BadgeBackfillPageResult = { isDone: boolean } -type BadgeKind = Doc<'skillBadges'>['kind'] +type ModerationBackfillStats = { + skillsScanned: number + recordsUpserted: number +} + +type ModerationBackfillPageItem = { + skillId: Id<'skills'> + notes?: string + reason?: string + reviewedAt?: number + hiddenAt?: number + hiddenBy?: Id<'users'> +} + +type ModerationBackfillPageResult = { + items: ModerationBackfillPageItem[] + cursor: string | null + isDone: boolean +} + +type ReportStatsBackfillStats = { + skillsScanned: number + recordsUpserted: number +} + +type ReportStatsBackfillPageItem = { + skillId: Id<'skills'> + reportCount?: number + lastReportedAt?: number +} + +type ReportStatsBackfillPageResult = { + items: ReportStatsBackfillPageItem[] + cursor: string | null + isDone: boolean +} + +type ResourceBackfillStats = { + skillsScanned: number + skillsUpdated: number + soulsScanned: number + soulsUpdated: number + resourcesInserted: number +} + +type ResourceBadgeBackfillStats = { + badgesScanned: number + badgesInserted: number + badgesSkipped: number +} export const getSkillFingerprintBackfillPageInternal = internalQuery({ args: { @@ -507,60 +555,26 @@ export const getSkillBadgeBackfillPageInternal = internalQuery({ .order('asc') .paginate({ cursor: args.cursor ?? null, numItems: batchSize }) - const items: BadgeBackfillPageItem[] = page.map((skill) => ({ - skillId: skill._id, - ownerUserId: skill.ownerUserId, - createdAt: skill.createdAt ?? undefined, - updatedAt: skill.updatedAt ?? undefined, - batch: skill.batch ?? undefined, - badges: skill.badges ?? undefined, - })) + const items: BadgeBackfillPageItem[] = page.map((skill) => { + const legacyBadges = (skill as Doc<'skills'> & { badges?: LegacySkillBadges }).badges + return { + skillId: skill._id, + resourceId: skill.resourceId ?? undefined, + ownerUserId: skill.ownerUserId, + createdAt: skill.createdAt ?? undefined, + updatedAt: skill.updatedAt ?? undefined, + batch: skill.batch ?? undefined, + legacyBadges: legacyBadges ?? undefined, + } + }) return { items, cursor: continueCursor, isDone } }, }) -export const applySkillBadgeBackfillPatchInternal = internalMutation({ +export const upsertResourceBadgeRecordInternal = internalMutation({ args: { - skillId: v.id('skills'), - badges: v.optional( - v.object({ - redactionApproved: v.optional( - v.object({ - byUserId: v.id('users'), - at: v.number(), - }), - ), - highlighted: v.optional( - v.object({ - byUserId: v.id('users'), - at: v.number(), - }), - ), - official: v.optional( - v.object({ - byUserId: v.id('users'), - at: v.number(), - }), - ), - deprecated: v.optional( - v.object({ - byUserId: v.id('users'), - at: v.number(), - }), - ), - }), - ), - }, - handler: async (ctx, args) => { - await ctx.db.patch(args.skillId, { badges: args.badges ?? undefined, updatedAt: Date.now() }) - return { ok: true as const } - }, -}) - -export const upsertSkillBadgeRecordInternal = internalMutation({ - args: { - skillId: v.id('skills'), + resourceId: v.id('resources'), kind: v.union( v.literal('highlighted'), v.literal('official'), @@ -572,12 +586,14 @@ export const upsertSkillBadgeRecordInternal = internalMutation({ }, handler: async (ctx, args) => { const existing = await ctx.db - .query('skillBadges') - .withIndex('by_skill_kind', (q) => q.eq('skillId', args.skillId).eq('kind', args.kind)) + .query('resourceBadges') + .withIndex('by_resource_kind', (q) => + q.eq('resourceId', args.resourceId).eq('kind', args.kind), + ) .unique() if (existing) return { inserted: false as const } - await ctx.db.insert('skillBadges', { - skillId: args.skillId, + await ctx.db.insert('resourceBadges', { + resourceId: args.resourceId, kind: args.kind, byUserId: args.byUserId, at: args.at, @@ -592,20 +608,22 @@ export type BadgeBackfillActionArgs = { maxBatches?: number } -export type BadgeBackfillActionResult = { ok: true; stats: BadgeBackfillStats } +export type SkillBadgeTableBackfillActionResult = { + ok: true + stats: SkillBadgeTableBackfillStats +} -export async function backfillSkillBadgesInternalHandler( +export async function backfillSkillBadgeTableInternalHandler( ctx: ActionCtx, args: BadgeBackfillActionArgs, -): Promise { +): Promise { const dryRun = Boolean(args.dryRun) const batchSize = clampInt(args.batchSize ?? DEFAULT_BATCH_SIZE, 1, MAX_BATCH_SIZE) const maxBatches = clampInt(args.maxBatches ?? DEFAULT_MAX_BATCHES, 1, MAX_MAX_BATCHES) - const totals: BadgeBackfillStats = { + const totals: SkillBadgeTableBackfillStats = { skillsScanned: 0, - skillsPatched: 0, - highlightsPatched: 0, + recordsInserted: 0, } let cursor: string | null = null @@ -622,26 +640,68 @@ export async function backfillSkillBadgesInternalHandler( for (const item of page.items) { totals.skillsScanned++ + if (!item.resourceId) continue + const badges = item.legacyBadges ?? {} + const entries: Array<{ kind: BadgeKind; byUserId: Id<'users'>; at: number }> = [] + + if (badges.redactionApproved) { + entries.push({ + kind: 'redactionApproved', + byUserId: badges.redactionApproved.byUserId, + at: badges.redactionApproved.at, + }) + } + + if (badges.official) { + entries.push({ + kind: 'official', + byUserId: badges.official.byUserId, + at: badges.official.at, + }) + } + + if (badges.deprecated) { + entries.push({ + kind: 'deprecated', + byUserId: badges.deprecated.byUserId, + at: badges.deprecated.at, + }) + } - const shouldHighlight = item.batch === 'highlighted' && !item.badges?.highlighted - if (!shouldHighlight) continue + const highlighted = + badges.highlighted ?? + (item.batch === 'highlighted' + ? { + byUserId: item.ownerUserId, + at: item.updatedAt ?? item.createdAt ?? Date.now(), + } + : undefined) - totals.skillsPatched++ - totals.highlightsPatched++ + if (highlighted) { + entries.push({ + kind: 'highlighted', + byUserId: highlighted.byUserId, + at: highlighted.at, + }) + } + if (entries.length === 0) continue if (dryRun) continue - const at = item.updatedAt ?? item.createdAt ?? Date.now() - await ctx.runMutation(internal.maintenance.applySkillBadgeBackfillPatchInternal, { - skillId: item.skillId, - badges: { - ...item.badges, - highlighted: { - byUserId: item.ownerUserId, - at, + for (const entry of entries) { + const result = await ctx.runMutation( + internal.maintenance.upsertResourceBadgeRecordInternal, + { + resourceId: item.resourceId, + kind: entry.kind, + byUserId: entry.byUserId, + at: entry.at, }, - }, - }) + ) + if (result.inserted) { + totals.recordsInserted++ + } + } } if (isDone) break @@ -654,37 +714,37 @@ export async function backfillSkillBadgesInternalHandler( return { ok: true as const, stats: totals } } -export const backfillSkillBadgesInternal = internalAction({ +export const backfillSkillBadgeTableInternal = internalAction({ args: { dryRun: v.optional(v.boolean()), batchSize: v.optional(v.number()), maxBatches: v.optional(v.number()), }, - handler: backfillSkillBadgesInternalHandler, + handler: backfillSkillBadgeTableInternalHandler, }) -export const backfillSkillBadges: ReturnType = action({ +export const backfillSkillBadgeTable: ReturnType = action({ args: { dryRun: v.optional(v.boolean()), batchSize: v.optional(v.number()), maxBatches: v.optional(v.number()), }, - handler: async (ctx, args): Promise => { + handler: async (ctx, args): Promise => { const { user } = await requireUserFromAction(ctx) assertRole(user, ['admin']) return ctx.runAction( - internal.maintenance.backfillSkillBadgesInternal, + internal.maintenance.backfillSkillBadgeTableInternal, args, - ) as Promise + ) as Promise }, }) -export const scheduleBackfillSkillBadges: ReturnType = action({ +export const scheduleBackfillSkillBadgeTable: ReturnType = action({ args: { dryRun: v.optional(v.boolean()) }, handler: async (ctx, args) => { const { user } = await requireUserFromAction(ctx) assertRole(user, ['admin']) - await ctx.scheduler.runAfter(0, internal.maintenance.backfillSkillBadgesInternal, { + await ctx.scheduler.runAfter(0, internal.maintenance.backfillSkillBadgeTableInternal, { dryRun: Boolean(args.dryRun), batchSize: DEFAULT_BATCH_SIZE, maxBatches: DEFAULT_MAX_BATCHES, @@ -693,95 +753,127 @@ export const scheduleBackfillSkillBadges: ReturnType = action({ }, }) -export type SkillBadgeTableBackfillActionResult = { - ok: true - stats: SkillBadgeTableBackfillStats -} +export const getSkillModerationBackfillPageInternal = internalQuery({ + args: { + cursor: v.optional(v.string()), + batchSize: v.optional(v.number()), + }, + handler: async (ctx, args): Promise => { + const batchSize = clampInt(args.batchSize ?? DEFAULT_BATCH_SIZE, 1, MAX_BATCH_SIZE) + const { page, isDone, continueCursor } = await ctx.db + .query('skills') + .order('asc') + .paginate({ cursor: args.cursor ?? null, numItems: batchSize }) -export async function backfillSkillBadgeTableInternalHandler( + const items: ModerationBackfillPageItem[] = page.map((skill) => { + const legacy = skill as Doc<'skills'> & { + moderationNotes?: string + moderationReason?: string + lastReviewedAt?: number + hiddenAt?: number + hiddenBy?: Id<'users'> + } + return { + skillId: skill._id, + notes: legacy.moderationNotes ?? undefined, + reason: legacy.moderationReason ?? undefined, + reviewedAt: legacy.lastReviewedAt ?? undefined, + hiddenAt: legacy.hiddenAt ?? undefined, + hiddenBy: legacy.hiddenBy ?? undefined, + } + }) + + return { items, cursor: continueCursor, isDone } + }, +}) + +export const upsertSkillModerationRecordInternal = internalMutation({ + args: { + skillId: v.id('skills'), + notes: v.optional(v.string()), + reason: v.optional(v.string()), + reviewedAt: v.optional(v.number()), + hiddenAt: v.optional(v.number()), + hiddenBy: v.optional(v.id('users')), + }, + handler: async (ctx, args) => { + const existing = await ctx.db + .query('skillModeration') + .withIndex('by_skill', (q) => q.eq('skillId', args.skillId)) + .unique() + + const patch: Partial> = {} + if (typeof args.notes === 'string' && !existing?.notes) patch.notes = args.notes + if (typeof args.reason === 'string' && !existing?.reason) patch.reason = args.reason + if (typeof args.reviewedAt === 'number' && !existing?.reviewedAt) { + patch.reviewedAt = args.reviewedAt + } + if (typeof args.hiddenAt === 'number' && !existing?.hiddenAt) patch.hiddenAt = args.hiddenAt + if (args.hiddenBy && !existing?.hiddenBy) patch.hiddenBy = args.hiddenBy + + if (Object.keys(patch).length === 0) { + return { upserted: false as const } + } + + if (existing) { + await ctx.db.patch(existing._id, patch) + return { upserted: true as const, inserted: false as const } + } + + await ctx.db.insert('skillModeration', { skillId: args.skillId, ...patch }) + return { upserted: true as const, inserted: true as const } + }, +}) + +export type ModerationBackfillActionResult = { ok: true; stats: ModerationBackfillStats } + +export async function backfillSkillModerationInternalHandler( ctx: ActionCtx, args: BadgeBackfillActionArgs, -): Promise { +): Promise { const dryRun = Boolean(args.dryRun) const batchSize = clampInt(args.batchSize ?? DEFAULT_BATCH_SIZE, 1, MAX_BATCH_SIZE) const maxBatches = clampInt(args.maxBatches ?? DEFAULT_MAX_BATCHES, 1, MAX_MAX_BATCHES) - const totals: SkillBadgeTableBackfillStats = { + const totals: ModerationBackfillStats = { skillsScanned: 0, - recordsInserted: 0, + recordsUpserted: 0, } let cursor: string | null = null let isDone = false for (let i = 0; i < maxBatches; i++) { - const page = (await ctx.runQuery(internal.maintenance.getSkillBadgeBackfillPageInternal, { + const page = (await ctx.runQuery(internal.maintenance.getSkillModerationBackfillPageInternal, { cursor: cursor ?? undefined, batchSize, - })) as BadgeBackfillPageResult + })) as ModerationBackfillPageResult cursor = page.cursor isDone = page.isDone for (const item of page.items) { totals.skillsScanned++ - const badges = item.badges ?? {} - const entries: Array<{ kind: BadgeKind; byUserId: Id<'users'>; at: number }> = [] - - if (badges.redactionApproved) { - entries.push({ - kind: 'redactionApproved', - byUserId: badges.redactionApproved.byUserId, - at: badges.redactionApproved.at, - }) - } - - if (badges.official) { - entries.push({ - kind: 'official', - byUserId: badges.official.byUserId, - at: badges.official.at, - }) - } - - if (badges.deprecated) { - entries.push({ - kind: 'deprecated', - byUserId: badges.deprecated.byUserId, - at: badges.deprecated.at, - }) - } - - const highlighted = - badges.highlighted ?? - (item.batch === 'highlighted' - ? { - byUserId: item.ownerUserId, - at: item.updatedAt ?? item.createdAt ?? Date.now(), - } - : undefined) - if (highlighted) { - entries.push({ - kind: 'highlighted', - byUserId: highlighted.byUserId, - at: highlighted.at, - }) - } + const hasValues = + typeof item.notes === 'string' || + typeof item.reason === 'string' || + typeof item.reviewedAt === 'number' || + typeof item.hiddenAt === 'number' || + Boolean(item.hiddenBy) + if (!hasValues) continue + totals.recordsUpserted++ if (dryRun) continue - for (const entry of entries) { - const result = await ctx.runMutation(internal.maintenance.upsertSkillBadgeRecordInternal, { - skillId: item.skillId, - kind: entry.kind, - byUserId: entry.byUserId, - at: entry.at, - }) - if (result.inserted) { - totals.recordsInserted++ - } - } + await ctx.runMutation(internal.maintenance.upsertSkillModerationRecordInternal, { + skillId: item.skillId, + notes: item.notes, + reason: item.reason, + reviewedAt: item.reviewedAt, + hiddenAt: item.hiddenAt, + hiddenBy: item.hiddenBy, + }) } if (isDone) break @@ -794,37 +886,621 @@ export async function backfillSkillBadgeTableInternalHandler( return { ok: true as const, stats: totals } } -export const backfillSkillBadgeTableInternal = internalAction({ +export const backfillSkillModerationInternal = internalAction({ args: { dryRun: v.optional(v.boolean()), batchSize: v.optional(v.number()), maxBatches: v.optional(v.number()), }, - handler: backfillSkillBadgeTableInternalHandler, + handler: backfillSkillModerationInternalHandler, }) -export const backfillSkillBadgeTable: ReturnType = action({ +export const backfillSkillModeration: ReturnType = action({ args: { dryRun: v.optional(v.boolean()), batchSize: v.optional(v.number()), maxBatches: v.optional(v.number()), }, - handler: async (ctx, args): Promise => { + handler: async (ctx, args): Promise => { const { user } = await requireUserFromAction(ctx) assertRole(user, ['admin']) return ctx.runAction( - internal.maintenance.backfillSkillBadgeTableInternal, + internal.maintenance.backfillSkillModerationInternal, args, - ) as Promise + ) as Promise }, }) -export const scheduleBackfillSkillBadgeTable: ReturnType = action({ +export const scheduleBackfillSkillModeration: ReturnType = action({ args: { dryRun: v.optional(v.boolean()) }, handler: async (ctx, args) => { const { user } = await requireUserFromAction(ctx) assertRole(user, ['admin']) - await ctx.scheduler.runAfter(0, internal.maintenance.backfillSkillBadgeTableInternal, { + await ctx.scheduler.runAfter(0, internal.maintenance.backfillSkillModerationInternal, { + dryRun: Boolean(args.dryRun), + batchSize: DEFAULT_BATCH_SIZE, + maxBatches: DEFAULT_MAX_BATCHES, + }) + return { ok: true as const } + }, +}) + +export const getSkillReportStatsBackfillPageInternal = internalQuery({ + args: { + cursor: v.optional(v.string()), + batchSize: v.optional(v.number()), + }, + handler: async (ctx, args): Promise => { + const batchSize = clampInt(args.batchSize ?? DEFAULT_BATCH_SIZE, 1, MAX_BATCH_SIZE) + const { page, isDone, continueCursor } = await ctx.db + .query('skills') + .order('asc') + .paginate({ cursor: args.cursor ?? null, numItems: batchSize }) + + const items: ReportStatsBackfillPageItem[] = page.map((skill) => { + const legacy = skill as Doc<'skills'> & { + reportCount?: number + lastReportedAt?: number + } + return { + skillId: skill._id, + reportCount: legacy.reportCount ?? undefined, + lastReportedAt: legacy.lastReportedAt ?? undefined, + } + }) + + return { items, cursor: continueCursor, isDone } + }, +}) + +export const upsertSkillReportStatsInternal = internalMutation({ + args: { + skillId: v.id('skills'), + reportCount: v.number(), + lastReportedAt: v.optional(v.number()), + }, + handler: async (ctx, args) => { + const existing = await ctx.db + .query('skillReportStats') + .withIndex('by_skill', (q) => q.eq('skillId', args.skillId)) + .unique() + if (existing) return { inserted: false as const } + await ctx.db.insert('skillReportStats', { + skillId: args.skillId, + reportCount: args.reportCount, + lastReportedAt: args.lastReportedAt ?? undefined, + }) + return { inserted: true as const } + }, +}) + +export type ReportStatsBackfillActionResult = { ok: true; stats: ReportStatsBackfillStats } + +export async function backfillSkillReportStatsInternalHandler( + ctx: ActionCtx, + args: BadgeBackfillActionArgs, +): Promise { + const dryRun = Boolean(args.dryRun) + const batchSize = clampInt(args.batchSize ?? DEFAULT_BATCH_SIZE, 1, MAX_BATCH_SIZE) + const maxBatches = clampInt(args.maxBatches ?? DEFAULT_MAX_BATCHES, 1, MAX_MAX_BATCHES) + + const totals: ReportStatsBackfillStats = { + skillsScanned: 0, + recordsUpserted: 0, + } + + let cursor: string | null = null + let isDone = false + + for (let i = 0; i < maxBatches; i++) { + const page = (await ctx.runQuery(internal.maintenance.getSkillReportStatsBackfillPageInternal, { + cursor: cursor ?? undefined, + batchSize, + })) as ReportStatsBackfillPageResult + + cursor = page.cursor + isDone = page.isDone + + for (const item of page.items) { + totals.skillsScanned++ + const hasReportCount = typeof item.reportCount === 'number' && item.reportCount > 0 + const hasLastReportedAt = typeof item.lastReportedAt === 'number' + if (!hasReportCount && !hasLastReportedAt) continue + + totals.recordsUpserted++ + if (dryRun) continue + + await ctx.runMutation(internal.maintenance.upsertSkillReportStatsInternal, { + skillId: item.skillId, + reportCount: item.reportCount ?? 0, + lastReportedAt: item.lastReportedAt, + }) + } + + if (isDone) break + } + + if (!isDone) { + throw new ConvexError('Backfill incomplete (maxBatches reached)') + } + + return { ok: true as const, stats: totals } +} + +export const backfillSkillReportStatsInternal = internalAction({ + args: { + dryRun: v.optional(v.boolean()), + batchSize: v.optional(v.number()), + maxBatches: v.optional(v.number()), + }, + handler: backfillSkillReportStatsInternalHandler, +}) + +export const backfillSkillReportStats: ReturnType = action({ + args: { + dryRun: v.optional(v.boolean()), + batchSize: v.optional(v.number()), + maxBatches: v.optional(v.number()), + }, + handler: async (ctx, args): Promise => { + const { user } = await requireUserFromAction(ctx) + assertRole(user, ['admin']) + return ctx.runAction( + internal.maintenance.backfillSkillReportStatsInternal, + args, + ) as Promise + }, +}) + +export const scheduleBackfillSkillReportStats: ReturnType = action({ + args: { dryRun: v.optional(v.boolean()) }, + handler: async (ctx, args) => { + const { user } = await requireUserFromAction(ctx) + assertRole(user, ['admin']) + await ctx.scheduler.runAfter(0, internal.maintenance.backfillSkillReportStatsInternal, { + dryRun: Boolean(args.dryRun), + batchSize: DEFAULT_BATCH_SIZE, + maxBatches: DEFAULT_MAX_BATCHES, + }) + return { ok: true as const } + }, +}) + +type SkillResourceBackfillPageItem = { + skillId: Id<'skills'> + hasResource: boolean + hasOwnerHandle: boolean +} + +type SoulResourceBackfillPageItem = { + soulId: Id<'souls'> + hasResource: boolean + hasOwnerHandle: boolean +} + +type ResourceBackfillPageResult = { + items: T[] + cursor: string | null + isDone: boolean +} + +export const getSkillResourceBackfillPageInternal = internalQuery({ + args: { + cursor: v.optional(v.string()), + batchSize: v.optional(v.number()), + }, + handler: async (ctx, args): Promise> => { + const batchSize = clampInt(args.batchSize ?? DEFAULT_BATCH_SIZE, 1, MAX_BATCH_SIZE) + const { page, isDone, continueCursor } = await ctx.db + .query('skills') + .order('asc') + .paginate({ cursor: args.cursor ?? null, numItems: batchSize }) + + const items: SkillResourceBackfillPageItem[] = [] + for (const skill of page) { + const resource = skill.resourceId ? await ctx.db.get(skill.resourceId) : null + items.push({ + skillId: skill._id, + hasResource: Boolean(resource), + hasOwnerHandle: Boolean(resource?.ownerHandle), + }) + } + + return { items, cursor: continueCursor, isDone } + }, +}) + +export const getSoulResourceBackfillPageInternal = internalQuery({ + args: { + cursor: v.optional(v.string()), + batchSize: v.optional(v.number()), + }, + handler: async (ctx, args): Promise> => { + const batchSize = clampInt(args.batchSize ?? DEFAULT_BATCH_SIZE, 1, MAX_BATCH_SIZE) + const { page, isDone, continueCursor } = await ctx.db + .query('souls') + .order('asc') + .paginate({ cursor: args.cursor ?? null, numItems: batchSize }) + + const items: SoulResourceBackfillPageItem[] = [] + for (const soul of page) { + const resource = soul.resourceId ? await ctx.db.get(soul.resourceId) : null + items.push({ + soulId: soul._id, + hasResource: Boolean(resource), + hasOwnerHandle: Boolean(resource?.ownerHandle), + }) + } + + return { items, cursor: continueCursor, isDone } + }, +}) + +export const upsertSkillResourceInternal = internalMutation({ + args: { skillId: v.id('skills') }, + handler: async (ctx, args) => { + const skill = await ctx.db.get(args.skillId) + if (!skill) return { ok: false as const, inserted: false as const } + + const owner = await ctx.db.get(skill.ownerUserId) + const ownerHandle = owner?.handle ?? owner?._id ?? undefined + + if (skill.resourceId) { + const existing = await ctx.db.get(skill.resourceId) + if (existing) { + if (!existing.ownerHandle) { + await ctx.db.patch(existing._id, { ownerHandle }) + } + return { ok: true as const, inserted: false as const } + } + } + + const resourceId = await ctx.db.insert('resources', { + type: 'skill', + slug: skill.slug, + displayName: skill.displayName, + summary: skill.summary, + ownerUserId: skill.ownerUserId, + ownerHandle, + softDeletedAt: skill.softDeletedAt, + statsDownloads: skill.statsDownloads, + statsStars: skill.statsStars, + statsInstallsCurrent: skill.statsInstallsCurrent, + statsInstallsAllTime: skill.statsInstallsAllTime, + stats: skill.stats, + createdAt: skill.createdAt, + updatedAt: skill.updatedAt, + }) + + await ctx.db.patch(skill._id, { resourceId }) + return { ok: true as const, inserted: true as const } + }, +}) + +export const upsertSoulResourceInternal = internalMutation({ + args: { soulId: v.id('souls') }, + handler: async (ctx, args) => { + const soul = await ctx.db.get(args.soulId) + if (!soul) return { ok: false as const, inserted: false as const } + + const owner = await ctx.db.get(soul.ownerUserId) + const ownerHandle = owner?.handle ?? owner?._id ?? undefined + + if (soul.resourceId) { + const existing = await ctx.db.get(soul.resourceId) + if (existing) { + if (!existing.ownerHandle) { + await ctx.db.patch(existing._id, { ownerHandle }) + } + return { ok: true as const, inserted: false as const } + } + } + + const resourceId = await ctx.db.insert('resources', { + type: 'soul', + slug: soul.slug, + displayName: soul.displayName, + summary: soul.summary, + ownerUserId: soul.ownerUserId, + ownerHandle, + softDeletedAt: soul.softDeletedAt, + statsDownloads: soul.stats.downloads, + statsStars: soul.stats.stars, + statsInstallsCurrent: undefined, + statsInstallsAllTime: undefined, + stats: { + downloads: soul.stats.downloads, + stars: soul.stats.stars, + versions: soul.stats.versions, + comments: soul.stats.comments, + }, + createdAt: soul.createdAt, + updatedAt: soul.updatedAt, + }) + + await ctx.db.patch(soul._id, { resourceId }) + return { ok: true as const, inserted: true as const } + }, +}) + +export type ResourceBackfillActionResult = { ok: true; stats: ResourceBackfillStats } + +export async function backfillResourcesInternalHandler( + ctx: ActionCtx, + args: BadgeBackfillActionArgs, +): Promise { + const dryRun = Boolean(args.dryRun) + const batchSize = clampInt(args.batchSize ?? DEFAULT_BATCH_SIZE, 1, MAX_BATCH_SIZE) + const maxBatches = clampInt(args.maxBatches ?? DEFAULT_MAX_BATCHES, 1, MAX_MAX_BATCHES) + + const totals: ResourceBackfillStats = { + skillsScanned: 0, + skillsUpdated: 0, + soulsScanned: 0, + soulsUpdated: 0, + resourcesInserted: 0, + } + + let cursor: string | null = null + let isDone = false + + for (let i = 0; i < maxBatches; i++) { + const page = (await ctx.runQuery(internal.maintenance.getSkillResourceBackfillPageInternal, { + cursor: cursor ?? undefined, + batchSize, + })) as ResourceBackfillPageResult + + cursor = page.cursor + isDone = page.isDone + + for (const item of page.items) { + totals.skillsScanned++ + if (item.hasResource && item.hasOwnerHandle) continue + totals.skillsUpdated++ + if (dryRun) continue + + const result = await ctx.runMutation(internal.maintenance.upsertSkillResourceInternal, { + skillId: item.skillId, + }) + if (result.inserted) totals.resourcesInserted++ + } + + if (isDone) break + } + + if (!isDone) { + throw new ConvexError('Skill resource backfill incomplete (maxBatches reached)') + } + + cursor = null + isDone = false + + for (let i = 0; i < maxBatches; i++) { + const page = (await ctx.runQuery(internal.maintenance.getSoulResourceBackfillPageInternal, { + cursor: cursor ?? undefined, + batchSize, + })) as ResourceBackfillPageResult + + cursor = page.cursor + isDone = page.isDone + + for (const item of page.items) { + totals.soulsScanned++ + if (item.hasResource && item.hasOwnerHandle) continue + totals.soulsUpdated++ + if (dryRun) continue + + const result = await ctx.runMutation(internal.maintenance.upsertSoulResourceInternal, { + soulId: item.soulId, + }) + if (result.inserted) totals.resourcesInserted++ + } + + if (isDone) break + } + + if (!isDone) { + throw new ConvexError('Soul resource backfill incomplete (maxBatches reached)') + } + + return { ok: true as const, stats: totals } +} + +export const backfillResourcesInternal = internalAction({ + args: { + dryRun: v.optional(v.boolean()), + batchSize: v.optional(v.number()), + maxBatches: v.optional(v.number()), + }, + handler: backfillResourcesInternalHandler, +}) + +export const backfillResources: ReturnType = action({ + args: { + dryRun: v.optional(v.boolean()), + batchSize: v.optional(v.number()), + maxBatches: v.optional(v.number()), + }, + handler: async (ctx, args): Promise => { + const { user } = await requireUserFromAction(ctx) + assertRole(user, ['admin']) + return ctx.runAction( + internal.maintenance.backfillResourcesInternal, + args, + ) as Promise + }, +}) + +export const scheduleBackfillResources: ReturnType = action({ + args: { dryRun: v.optional(v.boolean()) }, + handler: async (ctx, args) => { + const { user } = await requireUserFromAction(ctx) + assertRole(user, ['admin']) + await ctx.scheduler.runAfter(0, internal.maintenance.backfillResourcesInternal, { + dryRun: Boolean(args.dryRun), + batchSize: DEFAULT_BATCH_SIZE, + maxBatches: DEFAULT_MAX_BATCHES, + }) + return { ok: true as const } + }, +}) + +type LegacyBadgeBackfillPageResult = { + items: Doc<'skillBadges'>[] + cursor: string | null + isDone: boolean +} + +export const getLegacySkillBadgePageInternal = internalQuery({ + args: { + cursor: v.optional(v.string()), + batchSize: v.optional(v.number()), + }, + handler: async (ctx, args): Promise => { + const batchSize = clampInt(args.batchSize ?? DEFAULT_BATCH_SIZE, 1, MAX_BATCH_SIZE) + const { page, isDone, continueCursor } = await ctx.db + .query('skillBadges') + .order('asc') + .paginate({ cursor: args.cursor ?? null, numItems: batchSize }) + return { items: page, cursor: continueCursor, isDone } + }, +}) + +export const getSkillResourceIdInternal = internalQuery({ + args: { skillId: v.id('skills') }, + handler: async (ctx, args) => { + const skill = await ctx.db.get(args.skillId) + return { resourceId: skill?.resourceId ?? null } + }, +}) + +export const getResourceBadgeByKindInternal = internalQuery({ + args: { resourceId: v.id('resources'), kind: v.string() }, + handler: async (ctx, args) => { + return ctx.db + .query('resourceBadges') + .withIndex('by_resource_kind', (q) => + q.eq('resourceId', args.resourceId).eq('kind', args.kind as BadgeKind), + ) + .unique() + }, +}) + +export const insertResourceBadgeInternal = internalMutation({ + args: { + resourceId: v.id('resources'), + kind: v.union( + v.literal('highlighted'), + v.literal('official'), + v.literal('deprecated'), + v.literal('redactionApproved'), + ), + byUserId: v.id('users'), + at: v.number(), + }, + handler: async (ctx, args) => { + await ctx.db.insert('resourceBadges', { + resourceId: args.resourceId, + kind: args.kind, + byUserId: args.byUserId, + at: args.at, + }) + return { ok: true as const } + }, +}) + +export const backfillResourceBadgesFromLegacyInternal = internalAction({ + args: { + dryRun: v.optional(v.boolean()), + batchSize: v.optional(v.number()), + maxBatches: v.optional(v.number()), + }, + handler: async (ctx, args): Promise<{ ok: true; stats: ResourceBadgeBackfillStats }> => { + const dryRun = Boolean(args.dryRun) + const batchSize = clampInt(args.batchSize ?? DEFAULT_BATCH_SIZE, 1, MAX_BATCH_SIZE) + const maxBatches = clampInt(args.maxBatches ?? DEFAULT_MAX_BATCHES, 1, MAX_MAX_BATCHES) + + const totals: ResourceBadgeBackfillStats = { + badgesScanned: 0, + badgesInserted: 0, + badgesSkipped: 0, + } + + let cursor: string | null = null + let isDone = false + + for (let i = 0; i < maxBatches; i++) { + const page = (await ctx.runQuery(internal.maintenance.getLegacySkillBadgePageInternal, { + cursor: cursor ?? undefined, + batchSize, + })) as LegacyBadgeBackfillPageResult + + cursor = page.cursor + isDone = page.isDone + + for (const badge of page.items) { + totals.badgesScanned++ + const skillResult = (await ctx.runQuery(internal.maintenance.getSkillResourceIdInternal, { + skillId: badge.skillId, + })) as { resourceId: Id<'resources'> | null } + if (!skillResult.resourceId) { + totals.badgesSkipped++ + continue + } + const existing = await ctx.runQuery( + internal.maintenance.getResourceBadgeByKindInternal, + { + resourceId: skillResult.resourceId, + kind: badge.kind, + }, + ) + if (existing) { + totals.badgesSkipped++ + continue + } + if (dryRun) continue + await ctx.runMutation(internal.maintenance.insertResourceBadgeInternal, { + resourceId: skillResult.resourceId, + kind: badge.kind, + byUserId: badge.byUserId, + at: badge.at, + }) + totals.badgesInserted++ + } + + if (isDone) break + } + + if (!isDone) { + throw new ConvexError('Resource badge backfill incomplete (maxBatches reached)') + } + + return { ok: true as const, stats: totals } + }, +}) + +export const backfillResourceBadges: ReturnType = action({ + args: { + dryRun: v.optional(v.boolean()), + batchSize: v.optional(v.number()), + maxBatches: v.optional(v.number()), + }, + handler: async (ctx, args): Promise<{ ok: true; stats: ResourceBadgeBackfillStats }> => { + const { user } = await requireUserFromAction(ctx) + assertRole(user, ['admin']) + return ctx.runAction( + internal.maintenance.backfillResourceBadgesFromLegacyInternal, + args, + ) as Promise<{ ok: true; stats: ResourceBadgeBackfillStats }> + }, +}) + +export const scheduleBackfillResourceBadges: ReturnType = action({ + args: { dryRun: v.optional(v.boolean()) }, + handler: async (ctx, args) => { + const { user } = await requireUserFromAction(ctx) + assertRole(user, ['admin']) + await ctx.scheduler.runAfter(0, internal.maintenance.backfillResourceBadgesFromLegacyInternal, { dryRun: Boolean(args.dryRun), batchSize: DEFAULT_BATCH_SIZE, maxBatches: DEFAULT_MAX_BATCHES, diff --git a/convex/schema.ts b/convex/schema.ts index 5b8d10a..72eff48 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -25,16 +25,63 @@ const users = defineTable({ .index('phone', ['phone']) .index('handle', ['handle']) +const resources = defineTable({ + type: v.union(v.literal('skill'), v.literal('soul'), v.literal('extension')), + slug: v.string(), + displayName: v.string(), + summary: v.optional(v.string()), + ownerUserId: v.id('users'), + ownerHandle: v.optional(v.string()), + softDeletedAt: v.optional(v.number()), + statsDownloads: v.optional(v.number()), + statsStars: v.optional(v.number()), + statsInstallsCurrent: v.optional(v.number()), + statsInstallsAllTime: v.optional(v.number()), + stats: v.object({ + downloads: v.number(), + installsCurrent: v.optional(v.number()), + installsAllTime: v.optional(v.number()), + stars: v.number(), + versions: v.number(), + comments: v.number(), + }), + createdAt: v.number(), + updatedAt: v.number(), +}) + .index('by_type_slug', ['type', 'slug']) + .index('by_type_owner', ['type', 'ownerUserId']) + .index('by_type_owner_updated', ['type', 'ownerUserId', 'updatedAt']) + .index('by_type_updated', ['type', 'updatedAt']) + .index('by_type_active_updated', ['type', 'softDeletedAt', 'updatedAt']) + .index('by_type_stats_downloads', ['type', 'statsDownloads', 'updatedAt']) + .index('by_type_stats_stars', ['type', 'statsStars', 'updatedAt']) + .index('by_type_stats_installs_current', ['type', 'statsInstallsCurrent', 'updatedAt']) + .index('by_type_stats_installs_all_time', ['type', 'statsInstallsAllTime', 'updatedAt']) + .index('by_type_active_stats_downloads', ['type', 'softDeletedAt', 'statsDownloads', 'updatedAt']) + .index('by_type_active_stats_stars', ['type', 'softDeletedAt', 'statsStars', 'updatedAt']) + .index('by_type_active_stats_installs_current', [ + 'type', + 'softDeletedAt', + 'statsInstallsCurrent', + 'updatedAt', + ]) + .index('by_type_active_stats_installs_all_time', [ + 'type', + 'softDeletedAt', + 'statsInstallsAllTime', + 'updatedAt', + ]) + const skills = defineTable({ + resourceId: v.optional(v.id('resources')), slug: v.string(), displayName: v.string(), summary: v.optional(v.string()), ownerUserId: v.id('users'), - canonicalSkillId: v.optional(v.id('skills')), forkOf: v.optional( v.object({ skillId: v.id('skills'), - kind: v.union(v.literal('fork'), v.literal('duplicate')), + kind: v.literal('fork'), version: v.optional(v.string()), at: v.number(), }), @@ -42,43 +89,10 @@ const skills = defineTable({ latestVersionId: v.optional(v.id('skillVersions')), tags: v.record(v.string(), v.id('skillVersions')), softDeletedAt: v.optional(v.number()), - badges: v.object({ - redactionApproved: v.optional( - v.object({ - byUserId: v.id('users'), - at: v.number(), - }), - ), - highlighted: v.optional( - v.object({ - byUserId: v.id('users'), - at: v.number(), - }), - ), - official: v.optional( - v.object({ - byUserId: v.id('users'), - at: v.number(), - }), - ), - deprecated: v.optional( - v.object({ - byUserId: v.id('users'), - at: v.number(), - }), - ), - }), moderationStatus: v.optional( v.union(v.literal('active'), v.literal('hidden'), v.literal('removed')), ), - moderationNotes: v.optional(v.string()), - moderationReason: v.optional(v.string()), moderationFlags: v.optional(v.array(v.string())), - lastReviewedAt: v.optional(v.number()), - hiddenAt: v.optional(v.number()), - hiddenBy: v.optional(v.id('users')), - reportCount: v.optional(v.number()), - lastReportedAt: v.optional(v.number()), batch: v.optional(v.string()), statsDownloads: v.optional(v.number()), statsStars: v.optional(v.number()), @@ -97,6 +111,7 @@ const skills = defineTable({ }) .index('by_slug', ['slug']) .index('by_owner', ['ownerUserId']) + .index('by_resource', ['resourceId']) .index('by_updated', ['updatedAt']) .index('by_stats_downloads', ['statsDownloads', 'updatedAt']) .index('by_stats_stars', ['statsStars', 'updatedAt']) @@ -106,6 +121,7 @@ const skills = defineTable({ .index('by_active_updated', ['softDeletedAt', 'updatedAt']) const souls = defineTable({ + resourceId: v.optional(v.id('resources')), slug: v.string(), displayName: v.string(), summary: v.optional(v.string()), @@ -124,6 +140,15 @@ const souls = defineTable({ }) .index('by_slug', ['slug']) .index('by_owner', ['ownerUserId']) + .index('by_resource', ['resourceId']) + .index('by_updated', ['updatedAt']) + +const extensions = defineTable({ + resourceId: v.optional(v.id('resources')), + createdAt: v.number(), + updatedAt: v.number(), +}) + .index('by_resource', ['resourceId']) .index('by_updated', ['updatedAt']) const skillVersions = defineTable({ @@ -192,6 +217,21 @@ const skillVersionFingerprints = defineTable({ .index('by_fingerprint', ['fingerprint']) .index('by_skill_fingerprint', ['skillId', 'fingerprint']) +const resourceBadges = defineTable({ + resourceId: v.id('resources'), + kind: v.union( + v.literal('highlighted'), + v.literal('official'), + v.literal('deprecated'), + v.literal('redactionApproved'), + ), + byUserId: v.id('users'), + at: v.number(), +}) + .index('by_resource', ['resourceId']) + .index('by_resource_kind', ['resourceId', 'kind']) + .index('by_kind_at', ['kind', 'at']) + const skillBadges = defineTable({ skillId: v.id('skills'), kind: v.union( @@ -296,6 +336,12 @@ const skillStatUpdateCursors = defineTable({ updatedAt: v.number(), }).index('by_key', ['key']) +const automodCursors = defineTable({ + key: v.string(), + cursorUpdatedAt: v.optional(v.number()), + updatedAt: v.number(), +}).index('by_key', ['key']) + const soulEmbeddings = defineTable({ soulId: v.id('souls'), versionId: v.id('soulVersions'), @@ -335,6 +381,25 @@ const skillReports = defineTable({ .index('by_user', ['userId']) .index('by_skill_user', ['skillId', 'userId']) +const skillReportStats = defineTable({ + skillId: v.id('skills'), + reportCount: v.number(), + lastReportedAt: v.optional(v.number()), +}) + .index('by_skill', ['skillId']) + .index('by_last_reported', ['lastReportedAt']) + +const skillModeration = defineTable({ + skillId: v.id('skills'), + notes: v.optional(v.string()), + reason: v.optional(v.string()), + reviewedAt: v.optional(v.number()), + hiddenAt: v.optional(v.number()), + hiddenBy: v.optional(v.id('users')), +}) + .index('by_skill', ['skillId']) + .index('by_reviewed', ['reviewedAt']) + const soulComments = defineTable({ soulId: v.id('souls'), userId: v.id('users'), @@ -444,11 +509,14 @@ const userSkillRootInstalls = defineTable({ export default defineSchema({ ...authSchema, users, + resources, skills, souls, + extensions, skillVersions, soulVersions, skillVersionFingerprints, + resourceBadges, skillBadges, soulVersionFingerprints, skillEmbeddings, @@ -458,8 +526,11 @@ export default defineSchema({ skillStatBackfillState, skillStatEvents, skillStatUpdateCursors, + automodCursors, comments, skillReports, + skillReportStats, + skillModeration, soulComments, stars, soulStars, diff --git a/convex/search.ts b/convex/search.ts index 6cd09e4..eb061c9 100644 --- a/convex/search.ts +++ b/convex/search.ts @@ -2,7 +2,8 @@ import { v } from 'convex/values' import { internal } from './_generated/api' import type { Doc, Id } from './_generated/dataModel' import { action, internalQuery } from './_generated/server' -import { getSkillBadgeMaps, isSkillHighlighted, type SkillBadgeMap } from './lib/badges' +import { getResourceBadgeMaps, isResourceHighlighted, type ResourceBadgeMap } from './lib/badges' +import { isSkillPublic } from './lib/moderation' import { generateEmbedding } from './lib/embeddings' import { toPublicSkill, toPublicSoul } from './lib/public' import { matchesExactTokens, tokenize } from './lib/searchText' @@ -12,6 +13,7 @@ type HydratedEntry = { skill: NonNullable> version: Doc<'skillVersions'> | null ownerHandle: string | null + resourceId: Id<'resources'> | null } type SearchResult = HydratedEntry & { score: number } @@ -62,20 +64,23 @@ export const searchSkills: ReturnType = action({ results.map((result) => [result._id, result._score]), ) - const badgeMapEntries = (await ctx.runQuery(internal.search.getSkillBadgeMapsInternal, { - skillIds: hydrated.map((entry) => entry.skill._id), - })) as Array<[Id<'skills'>, SkillBadgeMap]> - const badgeMapBySkillId = new Map(badgeMapEntries) + const resourceIds = hydrated + .map((entry) => entry.resourceId) + .filter((resourceId): resourceId is Id<'resources'> => Boolean(resourceId)) + const badgeMapEntries = (await ctx.runQuery(internal.search.getResourceBadgeMapsInternal, { + resourceIds, + })) as Array<[Id<'resources'>, ResourceBadgeMap]> + const badgeMapByResourceId = new Map(badgeMapEntries) const hydratedWithBadges = hydrated.map((entry) => ({ ...entry, skill: { ...entry.skill, - badges: badgeMapBySkillId.get(entry.skill._id) ?? {}, + badges: entry.resourceId ? badgeMapByResourceId.get(entry.resourceId) ?? {} : {}, }, })) const filtered = args.highlightedOnly - ? hydratedWithBadges.filter((entry) => isSkillHighlighted(entry.skill)) + ? hydratedWithBadges.filter((entry) => isResourceHighlighted(entry.skill)) : hydratedWithBadges exactMatches = filtered.filter((entry) => @@ -105,10 +110,10 @@ export const searchSkills: ReturnType = action({ }, }) -export const getBadgeMapsForSkills = internalQuery({ - args: { skillIds: v.array(v.id('skills')) }, - handler: async (ctx, args): Promise, SkillBadgeMap]>> => { - const badgeMap = await getSkillBadgeMaps(ctx, args.skillIds) +export const getBadgeMapsForResources = internalQuery({ + args: { resourceIds: v.array(v.id('resources')) }, + handler: async (ctx, args): Promise, ResourceBadgeMap]>> => { + const badgeMap = await getResourceBadgeMaps(ctx, args.resourceIds) return Array.from(badgeMap.entries()) }, }) @@ -134,13 +139,38 @@ export const hydrateResults = internalQuery({ if (!embedding) return null const skill = await ctx.db.get(embedding.skillId) if (!skill || skill.softDeletedAt) return null - const [version, ownerHandle] = await Promise.all([ + if (!isSkillPublic(skill)) return null + const [version, resource] = await Promise.all([ ctx.db.get(embedding.versionId), - getOwnerHandle(skill.ownerUserId), + skill.resourceId ? ctx.db.get(skill.resourceId) : null, ]) - const publicSkill = toPublicSkill(skill) + const ownerHandle = resource?.ownerHandle ?? (await getOwnerHandle(skill.ownerUserId)) + const mergedSkill = resource + ? { + ...skill, + slug: resource.slug, + displayName: resource.displayName, + summary: resource.summary, + ownerUserId: resource.ownerUserId, + softDeletedAt: resource.softDeletedAt, + statsDownloads: resource.statsDownloads, + statsStars: resource.statsStars, + statsInstallsCurrent: resource.statsInstallsCurrent, + statsInstallsAllTime: resource.statsInstallsAllTime, + stats: resource.stats, + createdAt: resource.createdAt, + updatedAt: resource.updatedAt, + } + : skill + const publicSkill = toPublicSkill(mergedSkill) if (!publicSkill) return null - return { embeddingId, skill: publicSkill, version, ownerHandle } + return { + embeddingId, + skill: publicSkill, + version, + ownerHandle, + resourceId: skill.resourceId ?? null, + } }), ) @@ -243,10 +273,10 @@ export const hydrateSoulResults = internalQuery({ }, }) -export const getSkillBadgeMapsInternal = internalQuery({ - args: { skillIds: v.array(v.id('skills')) }, +export const getResourceBadgeMapsInternal = internalQuery({ + args: { resourceIds: v.array(v.id('resources')) }, handler: async (ctx, args) => { - const badgeMap = await getSkillBadgeMaps(ctx, args.skillIds) + const badgeMap = await getResourceBadgeMaps(ctx, args.resourceIds) return Array.from(badgeMap.entries()) }, }) diff --git a/convex/skillStatEvents.ts b/convex/skillStatEvents.ts index 03d6722..0720bcc 100644 --- a/convex/skillStatEvents.ts +++ b/convex/skillStatEvents.ts @@ -18,6 +18,7 @@ import { internal } from './_generated/api' import type { Doc, Id } from './_generated/dataModel' import type { MutationCtx } from './_generated/server' import { internalAction, internalMutation, internalQuery } from './_generated/server' +import { upsertResourceForSkill } from './lib/resource' import { applySkillStatDeltas, bumpDailySkillStats } from './lib/skillStats' /** @@ -244,6 +245,14 @@ export const processSkillStatEventsInternal = internalMutation({ ...patch, updatedAt: now, }) + await upsertResourceForSkill(ctx, skill, { + stats: patch.stats, + statsDownloads: patch.statsDownloads, + statsStars: patch.statsStars, + statsInstallsCurrent: patch.statsInstallsCurrent, + statsInstallsAllTime: patch.statsInstallsAllTime, + updatedAt: now, + }) } // Update daily stats for trending/leaderboards @@ -379,6 +388,14 @@ export const applyAggregatedStatsAndUpdateCursor = internalMutation({ ...patch, updatedAt: now, }) + await upsertResourceForSkill(ctx, skill, { + stats: patch.stats, + statsDownloads: patch.statsDownloads, + statsStars: patch.statsStars, + statsInstallsCurrent: patch.statsInstallsCurrent, + statsInstallsAllTime: patch.statsInstallsAllTime, + updatedAt: now, + }) } // Update daily stats for trending/leaderboards diff --git a/convex/skills.ts b/convex/skills.ts index 4c403db..c5a6f4d 100644 --- a/convex/skills.ts +++ b/convex/skills.ts @@ -1,4 +1,5 @@ import { paginationOptsValidator } from 'convex/server' +import { getAuthUserId } from '@convex-dev/auth/server' import { ConvexError, v } from 'convex/values' import { paginator } from 'convex-helpers/server/pagination' import { internal } from './_generated/api' @@ -6,11 +7,12 @@ import type { Doc, Id } from './_generated/dataModel' import type { MutationCtx, QueryCtx } from './_generated/server' import { action, internalMutation, internalQuery, mutation, query } from './_generated/server' import { assertAdmin, assertModerator, requireUser, requireUserFromAction } from './lib/access' -import { getSkillBadgeMap, getSkillBadgeMaps, isSkillHighlighted } from './lib/badges' +import { getResourceBadgeMap, getResourceBadgeMaps, isResourceHighlighted } from './lib/badges' import { generateChangelogPreview as buildChangelogPreview } from './lib/changelog' import { buildTrendingLeaderboard } from './lib/leaderboards' -import { deriveModerationFlags } from './lib/moderation' +import { deriveModerationFlags, isSkillPublic } from './lib/moderation' import { toPublicSkill, toPublicUser } from './lib/public' +import { upsertResourceForSkill } from './lib/resource' import { fetchText, type PublishResult, @@ -30,12 +32,77 @@ const MAX_LIST_LIMIT = 50 const MAX_PUBLIC_LIST_LIMIT = 200 const MAX_LIST_BULK_LIMIT = 200 const MAX_LIST_TAKE = 1000 +const AUTH_BYPASS = process.env.AUTH_BYPASS === 'true' async function resolveOwnerHandle(ctx: QueryCtx, ownerUserId: Id<'users'>) { const owner = await ctx.db.get(ownerUserId) return owner?.handle ?? owner?._id ?? null } +async function resolveSkillResourceId(ctx: MutationCtx, skill: Doc<'skills'>) { + if (skill.resourceId) return skill.resourceId + return upsertResourceForSkill(ctx, skill) +} + +type BadgeContext = Pick + +async function getBadgesForSkill(ctx: BadgeContext, skill: Doc<'skills'>) { + if (!skill.resourceId) return {} + return getResourceBadgeMap(ctx, skill.resourceId) +} + +async function getBadgeMapBySkillId(ctx: BadgeContext, skills: Doc<'skills'>[]) { + const resourceIds = skills + .map((skill) => skill.resourceId) + .filter((resourceId): resourceId is Id<'resources'> => Boolean(resourceId)) + const badgeMapByResourceId = await getResourceBadgeMaps(ctx, resourceIds) + return new Map( + skills.map((skill) => [ + skill._id, + skill.resourceId ? badgeMapByResourceId.get(skill.resourceId) ?? {} : {}, + ]), + ) +} + +async function upsertSkillModeration( + ctx: MutationCtx, + skillId: Id<'skills'>, + patch: Partial, 'notes' | 'reason' | 'reviewedAt' | 'hiddenAt' | 'hiddenBy'>>, +) { + const existing = await ctx.db + .query('skillModeration') + .withIndex('by_skill', (q) => q.eq('skillId', skillId)) + .unique() + if (existing) { + await ctx.db.patch(existing._id, patch) + return + } + await ctx.db.insert('skillModeration', { skillId, ...patch }) +} + +async function upsertSkillReportStats( + ctx: MutationCtx, + skillId: Id<'skills'>, + now: number, +) { + const existing = await ctx.db + .query('skillReportStats') + .withIndex('by_skill', (q) => q.eq('skillId', skillId)) + .unique() + if (existing) { + await ctx.db.patch(existing._id, { + reportCount: existing.reportCount + 1, + lastReportedAt: now, + }) + return + } + await ctx.db.insert('skillReportStats', { + skillId, + reportCount: 1, + lastReportedAt: now, + }) +} + type PublicSkillEntry = { skill: NonNullable> latestVersion: Doc<'skillVersions'> | null @@ -48,13 +115,41 @@ type ManagementSkillEntry = { owner: Doc<'users'> | null } -type BadgeKind = Doc<'skillBadges'>['kind'] +type ReportedSkillEntry = { + skill: Doc<'skills'> + latestVersion: Doc<'skillVersions'> | null + owner: Doc<'users'> | null + reportStats: Doc<'skillReportStats'> +} + +type BadgeKind = Doc<'resourceBadges'>['kind'] -async function buildPublicSkillEntries(ctx: QueryCtx, skills: Doc<'skills'>[]) { +function mergeResourceIntoSkill(skill: Doc<'skills'>, resource: Doc<'resources'>) { + return { + ...skill, + slug: resource.slug, + displayName: resource.displayName, + summary: resource.summary, + ownerUserId: resource.ownerUserId, + softDeletedAt: resource.softDeletedAt, + statsDownloads: resource.statsDownloads, + statsStars: resource.statsStars, + statsInstallsCurrent: resource.statsInstallsCurrent, + statsInstallsAllTime: resource.statsInstallsAllTime, + stats: resource.stats, + createdAt: resource.createdAt, + updatedAt: resource.updatedAt, + } +} + +async function buildPublicSkillEntriesFromSkillResources( + ctx: QueryCtx, + entries: Array<{ skill: Doc<'skills'>; resource: Doc<'resources'> }>, +) { const ownerHandleCache = new Map, Promise>() - const badgeMapBySkillId = await getSkillBadgeMaps( + const badgeMapByResourceId = await getResourceBadgeMaps( ctx, - skills.map((skill) => skill._id), + entries.map((entry) => entry.resource._id), ) const getOwnerHandle = (ownerUserId: Id<'users'>) => { @@ -65,28 +160,51 @@ async function buildPublicSkillEntries(ctx: QueryCtx, skills: Doc<'skills'>[]) { return handlePromise } - const entries = await Promise.all( - skills.map(async (skill) => { + const hydrated = await Promise.all( + entries.map(async (entry) => { + if (!isSkillPublic(entry.skill)) return null + const mergedSkill = mergeResourceIntoSkill(entry.skill, entry.resource) const [latestVersion, ownerHandle] = await Promise.all([ - skill.latestVersionId ? ctx.db.get(skill.latestVersionId) : null, - getOwnerHandle(skill.ownerUserId), + entry.skill.latestVersionId ? ctx.db.get(entry.skill.latestVersionId) : null, + entry.resource.ownerHandle + ? Promise.resolve(entry.resource.ownerHandle ?? null) + : getOwnerHandle(entry.resource.ownerUserId), ]) - const badges = badgeMapBySkillId.get(skill._id) ?? {} - const publicSkill = toPublicSkill({ ...skill, badges }) + const badges = badgeMapByResourceId.get(entry.resource._id) ?? {} + const publicSkill = toPublicSkill({ ...mergedSkill, badges }) if (!publicSkill) return null return { skill: publicSkill, latestVersion, ownerHandle } }), ) - return entries.filter((entry): entry is PublicSkillEntry => entry !== null) + return hydrated.filter((entry): entry is PublicSkillEntry => entry !== null) +} + +async function buildPublicSkillEntriesFromResources( + ctx: QueryCtx, + resources: Doc<'resources'>[], +) { + const skillEntries = await Promise.all( + resources.map(async (resource) => { + const skill = await ctx.db + .query('skills') + .withIndex('by_resource', (q) => q.eq('resourceId', resource._id)) + .unique() + if (!skill || skill.softDeletedAt || resource.softDeletedAt || !isSkillPublic(skill)) return null + return { skill, resource } + }), + ) + + const validEntries = skillEntries.filter( + (entry): entry is { skill: Doc<'skills'>; resource: Doc<'resources'> } => Boolean(entry), + ) + + return buildPublicSkillEntriesFromSkillResources(ctx, validEntries) } async function buildManagementSkillEntries(ctx: QueryCtx, skills: Doc<'skills'>[]) { const ownerCache = new Map, Promise | null>>() - const badgeMapBySkillId = await getSkillBadgeMaps( - ctx, - skills.map((skill) => skill._id), - ) + const badgeMapBySkillId = await getBadgeMapBySkillId(ctx, skills) const getOwner = (ownerUserId: Id<'users'>) => { const cached = ownerCache.get(ownerUserId) @@ -109,10 +227,7 @@ async function buildManagementSkillEntries(ctx: QueryCtx, skills: Doc<'skills'>[ } async function attachBadgesToSkills(ctx: QueryCtx, skills: Doc<'skills'>[]) { - const badgeMapBySkillId = await getSkillBadgeMaps( - ctx, - skills.map((skill) => skill._id), - ) + const badgeMapBySkillId = await getBadgeMapBySkillId(ctx, skills) return skills.map((skill) => ({ ...skill, badges: badgeMapBySkillId.get(skill._id) ?? {}, @@ -121,15 +236,20 @@ async function attachBadgesToSkills(ctx: QueryCtx, skills: Doc<'skills'>[]) { async function loadHighlightedSkills(ctx: QueryCtx, limit: number) { const entries = await ctx.db - .query('skillBadges') + .query('resourceBadges') .withIndex('by_kind_at', (q) => q.eq('kind', 'highlighted')) .order('desc') .take(MAX_LIST_TAKE) const skills: Doc<'skills'>[] = [] for (const badge of entries) { - const skill = await ctx.db.get(badge.skillId) - if (!skill || skill.softDeletedAt) continue + const resource = await ctx.db.get(badge.resourceId) + if (!resource || resource.softDeletedAt || resource.type !== 'skill') continue + const skill = await ctx.db + .query('skills') + .withIndex('by_resource', (q) => q.eq('resourceId', resource._id)) + .unique() + if (!skill || skill.softDeletedAt || !isSkillPublic(skill)) continue skills.push(skill) if (skills.length >= limit) break } @@ -139,31 +259,34 @@ async function loadHighlightedSkills(ctx: QueryCtx, limit: number) { async function upsertSkillBadge( ctx: MutationCtx, - skillId: Id<'skills'>, + skill: Doc<'skills'>, kind: BadgeKind, userId: Id<'users'>, at: number, ) { + const resourceId = await resolveSkillResourceId(ctx, skill) const existing = await ctx.db - .query('skillBadges') - .withIndex('by_skill_kind', (q) => q.eq('skillId', skillId).eq('kind', kind)) + .query('resourceBadges') + .withIndex('by_resource_kind', (q) => q.eq('resourceId', resourceId).eq('kind', kind)) .unique() if (existing) { await ctx.db.patch(existing._id, { byUserId: userId, at }) return existing._id } - return ctx.db.insert('skillBadges', { - skillId, + return ctx.db.insert('resourceBadges', { + resourceId, kind, byUserId: userId, at, }) } -async function removeSkillBadge(ctx: MutationCtx, skillId: Id<'skills'>, kind: BadgeKind) { +async function removeSkillBadge(ctx: MutationCtx, skill: Doc<'skills'>, kind: BadgeKind) { + const resourceId = skill.resourceId + if (!resourceId) return const existing = await ctx.db - .query('skillBadges') - .withIndex('by_skill_kind', (q) => q.eq('skillId', skillId).eq('kind', kind)) + .query('resourceBadges') + .withIndex('by_resource_kind', (q) => q.eq('resourceId', resourceId).eq('kind', kind)) .unique() if (existing) { await ctx.db.delete(existing._id) @@ -177,24 +300,34 @@ export const getBySlug = query({ .query('skills') .withIndex('by_slug', (q) => q.eq('slug', args.slug)) .unique() - if (!skill || skill.softDeletedAt) return null - const latestVersion = skill.latestVersionId ? await ctx.db.get(skill.latestVersionId) : null - const owner = toPublicUser(await ctx.db.get(skill.ownerUserId)) - const badges = await getSkillBadgeMap(ctx, skill._id) + if (!skill) return null + if (skill.softDeletedAt) { + if (!AUTH_BYPASS) { + const userId = await getAuthUserId(ctx) + if (!userId) return null + const user = await ctx.db.get(userId) + if (!user || (user.role !== 'admin' && user.role !== 'moderator')) return null + } + } + if (!isSkillPublic(skill)) return null + const [latestVersion, owner, resource, badges] = await Promise.all([ + skill.latestVersionId ? ctx.db.get(skill.latestVersionId) : null, + ctx.db.get(skill.ownerUserId), + skill.resourceId ? ctx.db.get(skill.resourceId) : null, + getBadgesForSkill(ctx, skill), + ]) const forkOfSkill = skill.forkOf?.skillId ? await ctx.db.get(skill.forkOf.skillId) : null const forkOfOwner = forkOfSkill ? await ctx.db.get(forkOfSkill.ownerUserId) : null - const canonicalSkill = skill.canonicalSkillId ? await ctx.db.get(skill.canonicalSkillId) : null - const canonicalOwner = canonicalSkill ? await ctx.db.get(canonicalSkill.ownerUserId) : null - - const publicSkill = toPublicSkill({ ...skill, badges }) + const mergedSkill = resource ? mergeResourceIntoSkill(skill, resource) : skill + const publicSkill = toPublicSkill({ ...mergedSkill, badges }) if (!publicSkill) return null return { skill: publicSkill, latestVersion, - owner, + owner: toPublicUser(owner), forkOf: forkOfSkill ? { kind: skill.forkOf?.kind ?? 'fork', @@ -209,18 +342,6 @@ export const getBySlug = query({ }, } : null, - canonical: canonicalSkill - ? { - skill: { - slug: canonicalSkill.slug, - displayName: canonicalSkill.displayName, - }, - owner: { - handle: canonicalOwner?.handle ?? canonicalOwner?.name ?? null, - userId: canonicalOwner?._id ?? null, - }, - } - : null, } }, }) @@ -257,7 +378,9 @@ export const list = query({ .withIndex('by_batch', (q) => q.eq('batch', args.batch)) .order('desc') .take(takeLimit) - const filtered = entries.filter((skill) => !skill.softDeletedAt).slice(0, limit) + const filtered = entries + .filter((skill) => !skill.softDeletedAt && isSkillPublic(skill)) + .slice(0, limit) const withBadges = await attachBadgesToSkills(ctx, filtered) return withBadges .map((skill) => toPublicSkill(skill)) @@ -265,23 +388,23 @@ export const list = query({ } const ownerUserId = args.ownerUserId if (ownerUserId) { - const entries = await ctx.db - .query('skills') - .withIndex('by_owner', (q) => q.eq('ownerUserId', ownerUserId)) + const resources = await ctx.db + .query('resources') + .withIndex('by_type_owner_updated', (q) => + q.eq('type', 'skill').eq('ownerUserId', ownerUserId), + ) .order('desc') .take(takeLimit) - const filtered = entries.filter((skill) => !skill.softDeletedAt).slice(0, limit) - const withBadges = await attachBadgesToSkills(ctx, filtered) - return withBadges - .map((skill) => toPublicSkill(skill)) - .filter((skill): skill is NonNullable => Boolean(skill)) + const entries = await buildPublicSkillEntriesFromResources(ctx, resources) + return entries.map((entry) => entry.skill).slice(0, limit) } - const entries = await ctx.db.query('skills').order('desc').take(takeLimit) - const filtered = entries.filter((skill) => !skill.softDeletedAt).slice(0, limit) - const withBadges = await attachBadgesToSkills(ctx, filtered) - return withBadges - .map((skill) => toPublicSkill(skill)) - .filter((skill): skill is NonNullable => Boolean(skill)) + const resources = await ctx.db + .query('resources') + .withIndex('by_type_updated', (q) => q.eq('type', 'skill')) + .order('desc') + .take(takeLimit) + const entries = await buildPublicSkillEntriesFromResources(ctx, resources) + return entries.map((entry) => entry.skill).slice(0, limit) }, }) @@ -307,16 +430,24 @@ export const listWithLatest = query({ } } else if (args.ownerUserId) { const ownerUserId = args.ownerUserId - entries = await ctx.db - .query('skills') - .withIndex('by_owner', (q) => q.eq('ownerUserId', ownerUserId)) + const resources = await ctx.db + .query('resources') + .withIndex('by_type_owner_updated', (q) => + q.eq('type', 'skill').eq('ownerUserId', ownerUserId), + ) .order('desc') .take(takeLimit) + return buildPublicSkillEntriesFromResources(ctx, resources) } else { - entries = await ctx.db.query('skills').order('desc').take(takeLimit) + const resources = await ctx.db + .query('resources') + .withIndex('by_type_updated', (q) => q.eq('type', 'skill')) + .order('desc') + .take(takeLimit) + return buildPublicSkillEntriesFromResources(ctx, resources) } - const filtered = entries.filter((skill) => !skill.softDeletedAt) + const filtered = entries.filter((skill) => !skill.softDeletedAt && isSkillPublic(skill)) const withBadges = await attachBadgesToSkills(ctx, filtered) const ordered = args.batch === 'highlighted' @@ -399,97 +530,129 @@ export const listReportedSkills = query({ assertModerator(user) const limit = clampInt(args.limit ?? 25, 1, MAX_LIST_BULK_LIMIT) const takeLimit = Math.min(limit * 5, MAX_LIST_TAKE) - const entries = await ctx.db.query('skills').order('desc').take(takeLimit) - const reported = entries - .filter((skill) => (skill.reportCount ?? 0) > 0) - .sort((a, b) => (b.lastReportedAt ?? 0) - (a.lastReportedAt ?? 0)) - .slice(0, limit) - return buildManagementSkillEntries(ctx, reported) + + const reportStats = await ctx.db + .query('skillReportStats') + .withIndex('by_last_reported', (q) => q) + .order('desc') + .take(takeLimit) + + const results: ReportedSkillEntry[] = [] + for (const stats of reportStats) { + if (stats.reportCount <= 0) continue + const skill = await ctx.db.get(stats.skillId) + if (!skill || skill.softDeletedAt) continue + const [latestVersion, owner] = await Promise.all([ + skill.latestVersionId ? ctx.db.get(skill.latestVersionId) : null, + ctx.db.get(skill.ownerUserId), + ]) + results.push({ skill, latestVersion, owner, reportStats: stats }) + if (results.length >= limit) break + } + + return results }, }) -export const listDuplicateCandidates = query({ - args: { limit: v.optional(v.number()) }, +export const getLatestSkillEmbeddingInternal = internalQuery({ + args: { skillId: v.id('skills') }, handler: async (ctx, args) => { - const { user } = await requireUser(ctx) - assertModerator(user) - const limit = clampInt(args.limit ?? 20, 1, MAX_LIST_BULK_LIMIT) - const takeLimit = Math.min(limit * 5, MAX_LIST_TAKE) - const skills = await ctx.db.query('skills').order('desc').take(takeLimit) - const entries = skills.filter((skill) => !skill.softDeletedAt).slice(0, limit) - - const results: Array<{ - skill: Doc<'skills'> - latestVersion: Doc<'skillVersions'> | null - fingerprint: string | null - matches: Array<{ skill: Doc<'skills'>; owner: Doc<'users'> | null }> - owner: Doc<'users'> | null - }> = [] + const embeddings = await ctx.db + .query('skillEmbeddings') + .withIndex('by_skill', (q) => q.eq('skillId', args.skillId)) + .collect() + return embeddings.find((entry) => entry.isLatest) ?? embeddings[0] ?? null + }, +}) - for (const skill of entries) { - const latestVersion = skill.latestVersionId ? await ctx.db.get(skill.latestVersionId) : null - const fingerprint = latestVersion?.fingerprint ?? null - if (!fingerprint) continue +export const findSimilarSkills = action({ + args: { skillId: v.id('skills'), limit: v.optional(v.number()) }, + handler: async (ctx, args) => { + const { user } = await requireUserFromAction(ctx) + assertModerator(user) + const limit = clampInt(args.limit ?? 8, 1, 25) - const matchedFingerprints = await ctx.db - .query('skillVersionFingerprints') - .withIndex('by_fingerprint', (q) => q.eq('fingerprint', fingerprint)) - .take(10) - - const matchEntries: Array<{ skill: Doc<'skills'>; owner: Doc<'users'> | null }> = [] - for (const match of matchedFingerprints) { - if (match.skillId === skill._id) continue - const matchSkill = await ctx.db.get(match.skillId) - if (!matchSkill || matchSkill.softDeletedAt) continue - const matchOwner = await ctx.db.get(matchSkill.ownerUserId) - matchEntries.push({ skill: matchSkill, owner: matchOwner }) - } + const sourceEmbedding = (await ctx.runQuery(internal.skills.getLatestSkillEmbeddingInternal, { + skillId: args.skillId, + })) as Doc<'skillEmbeddings'> | null - if (matchEntries.length === 0) continue + if (!sourceEmbedding) return [] - const owner = await ctx.db.get(skill.ownerUserId) - results.push({ - skill, - latestVersion, - fingerprint, - matches: matchEntries, - owner, - }) - } + const results = await ctx.vectorSearch('skillEmbeddings', 'by_embedding', { + vector: sourceEmbedding.embedding, + limit: Math.min(limit * 4, 50), + filter: (q) => q.or(q.eq('visibility', 'latest'), q.eq('visibility', 'latest-approved')), + }) - return results + const filtered = results.filter((result) => result._id !== sourceEmbedding._id) + if (filtered.length === 0) return [] + + const hydrated = (await ctx.runQuery(internal.search.hydrateResults, { + embeddingIds: filtered.map((result) => result._id), + })) as Array<{ + embeddingId: Id<'skillEmbeddings'> + skill: NonNullable> + version: Doc<'skillVersions'> | null + ownerHandle: string | null + resourceId: Id<'resources'> | null + }> + + const scoreById = new Map(filtered.map((result) => [result._id, result._score])) + + const entries = hydrated + .filter((entry) => entry.skill._id !== args.skillId) + .map((entry) => ({ + skill: entry.skill, + latestVersion: entry.version, + ownerHandle: entry.ownerHandle, + score: scoreById.get(entry.embeddingId) ?? 0, + })) + + return entries.slice(0, limit) }, }) +async function createSkillReport( + ctx: MutationCtx, + args: { skillId: Id<'skills'>; userId: Id<'users'>; reason?: string }, +) { + const skill = await ctx.db.get(args.skillId) + if (!skill || skill.softDeletedAt) throw new Error('Skill not found') + + const existing = await ctx.db + .query('skillReports') + .withIndex('by_skill_user', (q) => q.eq('skillId', args.skillId).eq('userId', args.userId)) + .unique() + if (existing) return { ok: true as const, reported: false, alreadyReported: true } + + const now = Date.now() + const reason = args.reason?.trim() + await ctx.db.insert('skillReports', { + skillId: args.skillId, + userId: args.userId, + reason: reason ? reason.slice(0, 500) : undefined, + createdAt: now, + }) + + await upsertSkillReportStats(ctx, skill._id, now) + await ctx.db.patch(skill._id, { updatedAt: now }) + await upsertResourceForSkill(ctx, skill, { updatedAt: now }) + + return { ok: true as const, reported: true, alreadyReported: false } +} + export const report = mutation({ args: { skillId: v.id('skills'), reason: v.optional(v.string()) }, handler: async (ctx, args) => { const { userId } = await requireUser(ctx) - const skill = await ctx.db.get(args.skillId) - if (!skill || skill.softDeletedAt) throw new Error('Skill not found') - - const existing = await ctx.db - .query('skillReports') - .withIndex('by_skill_user', (q) => q.eq('skillId', args.skillId).eq('userId', userId)) - .unique() - if (existing) return { ok: true as const, reported: false, alreadyReported: true } - - const now = Date.now() - const reason = args.reason?.trim() - await ctx.db.insert('skillReports', { - skillId: args.skillId, - userId, - reason: reason ? reason.slice(0, 500) : undefined, - createdAt: now, - }) - - await ctx.db.patch(skill._id, { - reportCount: (skill.reportCount ?? 0) + 1, - lastReportedAt: now, - updatedAt: now, - }) + return createSkillReport(ctx, { ...args, userId }) + }, +}) - return { ok: true as const, reported: true, alreadyReported: false } +export const reportInternal = internalMutation({ + args: { skillId: v.id('skills'), userId: v.id('users'), reason: v.optional(v.string()) }, + handler: async (ctx, args) => { + return createSkillReport(ctx, args) }, }) @@ -515,41 +678,43 @@ export const listPublicPage = query({ if (sort === 'updated') { const { page, isDone, continueCursor } = await ctx.db - .query('skills') - .withIndex('by_updated', (q) => q) + .query('resources') + .withIndex('by_type_active_updated', (q) => + q.eq('type', 'skill').eq('softDeletedAt', undefined), + ) .order('desc') .paginate({ cursor: args.cursor ?? null, numItems: limit }) - const skills = page.filter((skill) => !skill.softDeletedAt) - const items = await buildPublicSkillEntries(ctx, skills) + const items = await buildPublicSkillEntriesFromResources(ctx, page) return { items, nextCursor: isDone ? null : continueCursor } } if (sort === 'trending') { const entries = await getTrendingEntries(ctx, limit) - const skills: Doc<'skills'>[] = [] + const pairs: Array<{ skill: Doc<'skills'>; resource: Doc<'resources'> }> = [] for (const entry of entries) { const skill = await ctx.db.get(entry.skillId) - if (!skill || skill.softDeletedAt) continue - skills.push(skill) - if (skills.length >= limit) break + if (!skill || skill.softDeletedAt || !skill.resourceId) continue + const resource = await ctx.db.get(skill.resourceId) + if (!resource || resource.softDeletedAt) continue + pairs.push({ skill, resource }) + if (pairs.length >= limit) break } - const items = await buildPublicSkillEntries(ctx, skills) + const items = await buildPublicSkillEntriesFromSkillResources(ctx, pairs) return { items, nextCursor: null } } - const index = sortToIndex(sort) + const index = sortToResourceIndex(sort) const page = await ctx.db - .query('skills') - .withIndex(index, (q) => q) + .query('resources') + .withIndex(index, (q) => q.eq('type', 'skill').eq('softDeletedAt', undefined)) .order('desc') - .take(Math.min(limit * 5, MAX_LIST_TAKE)) + .take(Math.min(limit, MAX_LIST_TAKE)) - const filtered = page.filter((skill) => !skill.softDeletedAt).slice(0, limit) - const items = await buildPublicSkillEntries(ctx, filtered) + const items = await buildPublicSkillEntriesFromResources(ctx, page) return { items, nextCursor: null } }, }) @@ -570,13 +735,15 @@ export const listPublicPageV2 = query({ // Use the new index to filter out soft-deleted skills at query time. // softDeletedAt === undefined means active (non-deleted) skills only. const result = await paginator(ctx.db, schema) - .query('skills') - .withIndex('by_active_updated', (q) => q.eq('softDeletedAt', undefined)) + .query('resources') + .withIndex('by_type_active_updated', (q) => + q.eq('type', 'skill').eq('softDeletedAt', undefined), + ) .order('desc') .paginate(args.paginationOpts) // Build the public skill entries (fetch latestVersion + ownerHandle) - const items = await buildPublicSkillEntries(ctx, result.page) + const items = await buildPublicSkillEntriesFromResources(ctx, result.page) return { ...result, @@ -585,22 +752,22 @@ export const listPublicPageV2 = query({ }, }) -function sortToIndex( +function sortToResourceIndex( sort: 'downloads' | 'stars' | 'installsCurrent' | 'installsAllTime', ): - | 'by_stats_downloads' - | 'by_stats_stars' - | 'by_stats_installs_current' - | 'by_stats_installs_all_time' { + | 'by_type_active_stats_downloads' + | 'by_type_active_stats_stars' + | 'by_type_active_stats_installs_current' + | 'by_type_active_stats_installs_all_time' { switch (sort) { case 'downloads': - return 'by_stats_downloads' + return 'by_type_active_stats_downloads' case 'stars': - return 'by_stats_stars' + return 'by_type_active_stats_stars' case 'installsCurrent': - return 'by_stats_installs_current' + return 'by_type_active_stats_installs_current' case 'installsAllTime': - return 'by_stats_installs_all_time' + return 'by_type_active_stats_installs_all_time' } } @@ -843,11 +1010,13 @@ export const updateTags = mutation({ } const latestEntry = args.tags.find((entry) => entry.tag === 'latest') + const now = Date.now() await ctx.db.patch(skill._id, { tags: nextTags, latestVersionId: latestEntry ? latestEntry.versionId : skill.latestVersionId, - updatedAt: Date.now(), + updatedAt: now, }) + await upsertResourceForSkill(ctx, skill, { updatedAt: now }) if (latestEntry) { const embeddings = await ctx.db @@ -877,15 +1046,14 @@ export const setRedactionApproved = mutation({ const now = Date.now() if (args.approved) { - await upsertSkillBadge(ctx, skill._id, 'redactionApproved', user._id, now) + await upsertSkillBadge(ctx, skill, 'redactionApproved', user._id, now) } else { - await removeSkillBadge(ctx, skill._id, 'redactionApproved') + await removeSkillBadge(ctx, skill, 'redactionApproved') } - await ctx.db.patch(skill._id, { - lastReviewedAt: now, - updatedAt: now, - }) + await ctx.db.patch(skill._id, { updatedAt: now }) + await upsertResourceForSkill(ctx, skill, { updatedAt: now }) + await upsertSkillModeration(ctx, skill._id, { reviewedAt: now }) const embeddings = await ctx.db .query('skillEmbeddings') @@ -917,22 +1085,23 @@ export const setBatch = mutation({ assertModerator(user) const skill = await ctx.db.get(args.skillId) if (!skill) throw new Error('Skill not found') - const existingBadges = await getSkillBadgeMap(ctx, skill._id) - const previousHighlighted = isSkillHighlighted({ badges: existingBadges }) + const existingBadges = await getBadgesForSkill(ctx, skill) + const previousHighlighted = isResourceHighlighted({ badges: existingBadges }) const nextBatch = args.batch?.trim() || undefined const nextHighlighted = nextBatch === 'highlighted' const now = Date.now() if (nextHighlighted) { - await upsertSkillBadge(ctx, skill._id, 'highlighted', user._id, now) + await upsertSkillBadge(ctx, skill, 'highlighted', user._id, now) } else { - await removeSkillBadge(ctx, skill._id, 'highlighted') + await removeSkillBadge(ctx, skill, 'highlighted') } await ctx.db.patch(skill._id, { batch: nextBatch, updatedAt: now, }) + await upsertResourceForSkill(ctx, skill, { updatedAt: now }) await ctx.db.insert('auditLogs', { actorUserId: user._id, action: 'badge.highlighted', @@ -960,10 +1129,16 @@ export const setSoftDeleted = mutation({ await ctx.db.patch(skill._id, { softDeletedAt: args.deleted ? now : undefined, moderationStatus: args.deleted ? 'hidden' : 'active', + updatedAt: now, + }) + await upsertResourceForSkill(ctx, skill, { + softDeletedAt: args.deleted ? now : undefined, + updatedAt: now, + }) + await upsertSkillModeration(ctx, skill._id, { + reviewedAt: now, hiddenAt: args.deleted ? now : undefined, hiddenBy: args.deleted ? user._id : undefined, - lastReviewedAt: now, - updatedAt: now, }) const embeddings = await ctx.db @@ -987,6 +1162,28 @@ export const setSoftDeleted = mutation({ metadata: { slug: skill.slug, softDeletedAt: args.deleted ? now : null }, createdAt: now, }) + + if (args.deleted) { + const [resource, owner] = await Promise.all([ + skill.resourceId ? ctx.db.get(skill.resourceId) : null, + ctx.db.get(skill.ownerUserId), + ]) + const ownerHandles = [ + resource?.ownerHandle, + owner?.handle, + owner?.displayName, + owner?.name, + owner?._id, + ] + .filter((value): value is string => Boolean(value)) + .map((value) => String(value)) + if (ownerHandles.length > 0) { + void ctx.scheduler.runAfter(0, internal.githubBackupsNode.deleteSkillBackupInternal, { + slug: skill.slug, + ownerHandles, + }) + } + } }, }) @@ -1006,9 +1203,10 @@ export const changeOwner = mutation({ const now = Date.now() await ctx.db.patch(skill._id, { ownerUserId: args.ownerUserId, - lastReviewedAt: now, updatedAt: now, }) + await upsertResourceForSkill(ctx, skill, { ownerUserId: args.ownerUserId, updatedAt: now }) + await upsertSkillModeration(ctx, skill._id, { reviewedAt: now }) const embeddings = await ctx.db .query('skillEmbeddings') @@ -1032,69 +1230,6 @@ export const changeOwner = mutation({ }, }) -export const setDuplicate = mutation({ - args: { skillId: v.id('skills'), canonicalSlug: v.optional(v.string()) }, - handler: async (ctx, args) => { - const { user } = await requireUser(ctx) - assertModerator(user) - const skill = await ctx.db.get(args.skillId) - if (!skill) throw new Error('Skill not found') - - const now = Date.now() - const canonicalSlug = args.canonicalSlug?.trim().toLowerCase() - - if (!canonicalSlug) { - await ctx.db.patch(skill._id, { - canonicalSkillId: undefined, - forkOf: undefined, - lastReviewedAt: now, - updatedAt: now, - }) - await ctx.db.insert('auditLogs', { - actorUserId: user._id, - action: 'skill.duplicate.clear', - targetType: 'skill', - targetId: skill._id, - metadata: { canonicalSlug: null }, - createdAt: now, - }) - return - } - - const canonical = await ctx.db - .query('skills') - .withIndex('by_slug', (q) => q.eq('slug', canonicalSlug)) - .unique() - if (!canonical) throw new Error('Canonical skill not found') - if (canonical._id === skill._id) throw new Error('Cannot duplicate a skill onto itself') - - const canonicalVersion = canonical.latestVersionId - ? await ctx.db.get(canonical.latestVersionId) - : null - - await ctx.db.patch(skill._id, { - canonicalSkillId: canonical._id, - forkOf: { - skillId: canonical._id, - kind: 'duplicate', - version: canonicalVersion?.version, - at: now, - }, - lastReviewedAt: now, - updatedAt: now, - }) - - await ctx.db.insert('auditLogs', { - actorUserId: user._id, - action: 'skill.duplicate.set', - targetType: 'skill', - targetId: skill._id, - metadata: { canonicalSlug }, - createdAt: now, - }) - }, -}) - export const setOfficialBadge = mutation({ args: { skillId: v.id('skills'), official: v.boolean() }, handler: async (ctx, args) => { @@ -1105,15 +1240,14 @@ export const setOfficialBadge = mutation({ const now = Date.now() if (args.official) { - await upsertSkillBadge(ctx, skill._id, 'official', user._id, now) + await upsertSkillBadge(ctx, skill, 'official', user._id, now) } else { - await removeSkillBadge(ctx, skill._id, 'official') + await removeSkillBadge(ctx, skill, 'official') } - await ctx.db.patch(skill._id, { - lastReviewedAt: now, - updatedAt: now, - }) + await ctx.db.patch(skill._id, { updatedAt: now }) + await upsertResourceForSkill(ctx, skill, { updatedAt: now }) + await upsertSkillModeration(ctx, skill._id, { reviewedAt: now }) await ctx.db.insert('auditLogs', { actorUserId: user._id, @@ -1136,15 +1270,14 @@ export const setDeprecatedBadge = mutation({ const now = Date.now() if (args.deprecated) { - await upsertSkillBadge(ctx, skill._id, 'deprecated', user._id, now) + await upsertSkillBadge(ctx, skill, 'deprecated', user._id, now) } else { - await removeSkillBadge(ctx, skill._id, 'deprecated') + await removeSkillBadge(ctx, skill, 'deprecated') } - await ctx.db.patch(skill._id, { - lastReviewedAt: now, - updatedAt: now, - }) + await ctx.db.patch(skill._id, { updatedAt: now }) + await upsertResourceForSkill(ctx, skill, { updatedAt: now }) + await upsertSkillModeration(ctx, skill._id, { reviewedAt: now }) await ctx.db.insert('auditLogs', { actorUserId: user._id, @@ -1222,12 +1355,31 @@ export const hardDelete = mutation({ await ctx.db.delete(star._id) } - const badges = await ctx.db - .query('skillBadges') + const resourceId = skill.resourceId + if (resourceId) { + const badges = await ctx.db + .query('resourceBadges') + .withIndex('by_resource', (q) => q.eq('resourceId', resourceId)) + .collect() + for (const badge of badges) { + await ctx.db.delete(badge._id) + } + } + + const moderation = await ctx.db + .query('skillModeration') .withIndex('by_skill', (q) => q.eq('skillId', skill._id)) .collect() - for (const badge of badges) { - await ctx.db.delete(badge._id) + for (const entry of moderation) { + await ctx.db.delete(entry._id) + } + + const reportStats = await ctx.db + .query('skillReportStats') + .withIndex('by_skill', (q) => q.eq('skillId', skill._id)) + .collect() + for (const entry of reportStats) { + await ctx.db.delete(entry._id) } const dailyStats = await ctx.db @@ -1273,17 +1425,18 @@ export const hardDelete = mutation({ const relatedSkills = await ctx.db.query('skills').collect() for (const related of relatedSkills) { if (related._id === skill._id) continue - if (related.canonicalSkillId === skill._id || related.forkOf?.skillId === skill._id) { + if (related.forkOf?.skillId === skill._id) { await ctx.db.patch(related._id, { - canonicalSkillId: - related.canonicalSkillId === skill._id ? undefined : related.canonicalSkillId, - forkOf: related.forkOf?.skillId === skill._id ? undefined : related.forkOf, + forkOf: undefined, updatedAt: Date.now(), }) } } await ctx.db.delete(skill._id) + if (skill.resourceId) { + await ctx.db.delete(skill.resourceId) + } await ctx.db.insert('auditLogs', { actorUserId: user._id, @@ -1347,11 +1500,10 @@ export const insertVersion = internalMutation({ const forkOfSlug = args.forkOf?.slug.trim().toLowerCase() || '' const forkOfVersion = args.forkOf?.version?.trim() || undefined - let canonicalSkillId: Id<'skills'> | undefined let forkOf: | { skillId: Id<'skills'> - kind: 'fork' | 'duplicate' + kind: 'fork' version?: string at: number } @@ -1363,23 +1515,12 @@ export const insertVersion = internalMutation({ .withIndex('by_slug', (q) => q.eq('slug', forkOfSlug)) .unique() if (!upstream || upstream.softDeletedAt) throw new Error('Upstream skill not found') - canonicalSkillId = upstream.canonicalSkillId ?? upstream._id forkOf = { skillId: upstream._id, kind: 'fork', version: forkOfVersion, at: now, } - } else { - const match = await findCanonicalSkillForFingerprint(ctx, args.fingerprint) - if (match) { - canonicalSkillId = match.canonicalSkillId ?? match._id - forkOf = { - skillId: match._id, - kind: 'duplicate', - at: now, - } - } } const summary = getFrontmatterValue(args.parsed.frontmatter, 'description') @@ -1389,26 +1530,41 @@ export const insertVersion = internalMutation({ parsed: args.parsed, files: args.files, }) + const resourceId = await ctx.db.insert('resources', { + type: 'skill', + slug: args.slug, + displayName: args.displayName, + summary: summaryValue, + ownerUserId: userId, + ownerHandle: user.handle ?? user._id, + softDeletedAt: undefined, + statsDownloads: 0, + statsStars: 0, + statsInstallsCurrent: 0, + statsInstallsAllTime: 0, + stats: { + downloads: 0, + installsCurrent: 0, + installsAllTime: 0, + stars: 0, + versions: 0, + comments: 0, + }, + createdAt: now, + updatedAt: now, + }) const skillId = await ctx.db.insert('skills', { + resourceId, slug: args.slug, displayName: args.displayName, summary: summaryValue, ownerUserId: userId, - canonicalSkillId, forkOf, latestVersionId: undefined, tags: {}, softDeletedAt: undefined, - badges: { - redactionApproved: undefined, - highlighted: undefined, - official: undefined, - deprecated: undefined, - }, moderationStatus: 'active', moderationFlags: moderationFlags.length ? moderationFlags : undefined, - reportCount: 0, - lastReportedAt: undefined, statsDownloads: 0, statsStars: 0, statsInstallsCurrent: 0, @@ -1465,19 +1621,31 @@ export const insertVersion = internalMutation({ files: args.files, }) + const nextStats = { ...skill.stats, versions: skill.stats.versions + 1 } await ctx.db.patch(skill._id, { displayName: args.displayName, summary: nextSummary ?? undefined, latestVersionId: versionId, tags: nextTags, - stats: { ...skill.stats, versions: skill.stats.versions + 1 }, + stats: nextStats, softDeletedAt: undefined, moderationStatus: skill.moderationStatus ?? 'active', moderationFlags: moderationFlags.length ? moderationFlags : undefined, updatedAt: now, }) + await upsertResourceForSkill(ctx, skill, { + displayName: args.displayName, + summary: nextSummary ?? undefined, + softDeletedAt: undefined, + stats: nextStats, + statsDownloads: skill.statsDownloads, + statsStars: skill.statsStars, + statsInstallsCurrent: skill.statsInstallsCurrent, + statsInstallsAllTime: skill.statsInstallsAllTime, + updatedAt: now, + }) - const badgeMap = await getSkillBadgeMap(ctx, skill._id) + const badgeMap = await getBadgesForSkill(ctx, skill) const isApproved = Boolean(badgeMap.redactionApproved) const embeddingId = await ctx.db.insert('skillEmbeddings', { @@ -1543,10 +1711,16 @@ export const setSkillSoftDeletedInternal = internalMutation({ await ctx.db.patch(skill._id, { softDeletedAt: args.deleted ? now : undefined, moderationStatus: args.deleted ? 'hidden' : 'active', + updatedAt: now, + }) + await upsertResourceForSkill(ctx, skill, { + softDeletedAt: args.deleted ? now : undefined, + updatedAt: now, + }) + await upsertSkillModeration(ctx, skill._id, { + reviewedAt: now, hiddenAt: args.deleted ? now : undefined, hiddenBy: args.deleted ? args.userId : undefined, - lastReviewedAt: now, - updatedAt: now, }) const embeddings = await ctx.db @@ -1571,6 +1745,28 @@ export const setSkillSoftDeletedInternal = internalMutation({ createdAt: now, }) + if (args.deleted) { + const [resource, owner] = await Promise.all([ + skill.resourceId ? ctx.db.get(skill.resourceId) : null, + ctx.db.get(skill.ownerUserId), + ]) + const ownerHandles = [ + resource?.ownerHandle, + owner?.handle, + owner?.displayName, + owner?.name, + owner?._id, + ] + .filter((value): value is string => Boolean(value)) + .map((value) => String(value)) + if (ownerHandles.length > 0) { + void ctx.scheduler.runAfter(0, internal.githubBackupsNode.deleteSkillBackupInternal, { + slug: skill.slug, + ownerHandles, + }) + } + } + return { ok: true as const } }, }) @@ -1587,20 +1783,3 @@ function clampInt(value: number, min: number, max: number) { return Math.min(max, Math.max(min, rounded)) } -async function findCanonicalSkillForFingerprint( - ctx: { db: MutationCtx['db'] }, - fingerprint: string, -) { - const matches = await ctx.db - .query('skillVersionFingerprints') - .withIndex('by_fingerprint', (q) => q.eq('fingerprint', fingerprint)) - .take(25) - - for (const entry of matches) { - const skill = await ctx.db.get(entry.skillId) - if (!skill || skill.softDeletedAt) continue - return skill - } - - return null -} diff --git a/convex/soulComments.ts b/convex/soulComments.ts index ed25d6c..8e3d0ba 100644 --- a/convex/soulComments.ts +++ b/convex/soulComments.ts @@ -3,6 +3,7 @@ import type { Doc } from './_generated/dataModel' import { mutation, query } from './_generated/server' import { assertModerator, requireUser } from './lib/access' import { type PublicUser, toPublicUser } from './lib/public' +import { upsertResourceForSoul } from './lib/resource' export const listBySoul = query({ args: { soulId: v.id('souls'), limit: v.optional(v.number()) }, @@ -43,9 +44,22 @@ export const add = mutation({ deletedBy: undefined, }) + const now = Date.now() + const nextStats = { ...soul.stats, comments: soul.stats.comments + 1 } await ctx.db.patch(soul._id, { - stats: { ...soul.stats, comments: soul.stats.comments + 1 }, - updatedAt: Date.now(), + stats: nextStats, + updatedAt: now, + }) + await upsertResourceForSoul(ctx, soul, { + statsDownloads: nextStats.downloads, + statsStars: nextStats.stars, + stats: { + downloads: nextStats.downloads, + stars: nextStats.stars, + versions: nextStats.versions, + comments: nextStats.comments, + }, + updatedAt: now, }) }, }) @@ -70,9 +84,22 @@ export const remove = mutation({ const soul = await ctx.db.get(comment.soulId) if (soul) { + const now = Date.now() + const nextStats = { ...soul.stats, comments: Math.max(0, soul.stats.comments - 1) } await ctx.db.patch(soul._id, { - stats: { ...soul.stats, comments: Math.max(0, soul.stats.comments - 1) }, - updatedAt: Date.now(), + stats: nextStats, + updatedAt: now, + }) + await upsertResourceForSoul(ctx, soul, { + statsDownloads: nextStats.downloads, + statsStars: nextStats.stars, + stats: { + downloads: nextStats.downloads, + stars: nextStats.stars, + versions: nextStats.versions, + comments: nextStats.comments, + }, + updatedAt: now, }) } diff --git a/convex/soulDownloads.ts b/convex/soulDownloads.ts index 2bb6f38..d53373c 100644 --- a/convex/soulDownloads.ts +++ b/convex/soulDownloads.ts @@ -1,14 +1,28 @@ import { v } from 'convex/values' import { mutation } from './_generated/server' +import { upsertResourceForSoul } from './lib/resource' export const increment = mutation({ args: { soulId: v.id('souls') }, handler: async (ctx, args) => { const soul = await ctx.db.get(args.soulId) if (!soul) return + const now = Date.now() + const nextStats = { ...soul.stats, downloads: soul.stats.downloads + 1 } await ctx.db.patch(soul._id, { - stats: { ...soul.stats, downloads: soul.stats.downloads + 1 }, - updatedAt: Date.now(), + stats: nextStats, + updatedAt: now, + }) + await upsertResourceForSoul(ctx, soul, { + statsDownloads: nextStats.downloads, + statsStars: nextStats.stars, + stats: { + downloads: nextStats.downloads, + stars: nextStats.stars, + versions: nextStats.versions, + comments: nextStats.comments, + }, + updatedAt: now, }) }, }) diff --git a/convex/soulStars.ts b/convex/soulStars.ts index adfc57f..f9cee44 100644 --- a/convex/soulStars.ts +++ b/convex/soulStars.ts @@ -2,6 +2,7 @@ import { v } from 'convex/values' import { mutation, query } from './_generated/server' import { requireUser } from './lib/access' import { toPublicSoul } from './lib/public' +import { upsertResourceForSoul } from './lib/resource' export const isStarred = query({ args: { soulId: v.id('souls') }, @@ -29,9 +30,22 @@ export const toggle = mutation({ if (existing) { await ctx.db.delete(existing._id) + const now = Date.now() + const nextStats = { ...soul.stats, stars: Math.max(0, soul.stats.stars - 1) } await ctx.db.patch(soul._id, { - stats: { ...soul.stats, stars: Math.max(0, soul.stats.stars - 1) }, - updatedAt: Date.now(), + stats: nextStats, + updatedAt: now, + }) + await upsertResourceForSoul(ctx, soul, { + statsDownloads: nextStats.downloads, + statsStars: nextStats.stars, + stats: { + downloads: nextStats.downloads, + stars: nextStats.stars, + versions: nextStats.versions, + comments: nextStats.comments, + }, + updatedAt: now, }) return { starred: false } } @@ -41,9 +55,22 @@ export const toggle = mutation({ userId, createdAt: Date.now(), }) + const now = Date.now() + const nextStats = { ...soul.stats, stars: soul.stats.stars + 1 } await ctx.db.patch(soul._id, { - stats: { ...soul.stats, stars: soul.stats.stars + 1 }, - updatedAt: Date.now(), + stats: nextStats, + updatedAt: now, + }) + await upsertResourceForSoul(ctx, soul, { + statsDownloads: nextStats.downloads, + statsStars: nextStats.stars, + stats: { + downloads: nextStats.downloads, + stars: nextStats.stars, + versions: nextStats.versions, + comments: nextStats.comments, + }, + updatedAt: now, }) return { starred: true } }, diff --git a/convex/souls.ts b/convex/souls.ts index e0e3ebd..605b2e2 100644 --- a/convex/souls.ts +++ b/convex/souls.ts @@ -1,9 +1,12 @@ import { ConvexError, v } from 'convex/values' import { internal } from './_generated/api' import type { Doc, Id } from './_generated/dataModel' +import type { QueryCtx } from './_generated/server' import { action, internalMutation, internalQuery, mutation, query } from './_generated/server' import { assertModerator, requireUser, requireUserFromAction } from './lib/access' +import { getResourceBadgeMap, getResourceBadgeMaps } from './lib/badges' import { toPublicSoul, toPublicUser } from './lib/public' +import { upsertResourceForSoul } from './lib/resource' import { getFrontmatterValue, hashSkillFiles } from './lib/skills' import { generateSoulChangelogPreview } from './lib/soulChangelog' import { fetchText, type PublishResult, publishSoulVersionForUser } from './lib/soulPublish' @@ -17,6 +20,65 @@ type FileTextResult = { path: string; text: string; size: number; sha256: string const MAX_DIFF_FILE_BYTES = 200 * 1024 const MAX_LIST_LIMIT = 50 +function mergeResourceIntoSoul(soul: Doc<'souls'>, resource: Doc<'resources'>) { + return { + ...soul, + slug: resource.slug, + displayName: resource.displayName, + summary: resource.summary, + ownerUserId: resource.ownerUserId, + softDeletedAt: resource.softDeletedAt, + stats: resource.stats, + createdAt: resource.createdAt, + updatedAt: resource.updatedAt, + } +} + +async function buildPublicSoulEntriesFromResources( + ctx: QueryCtx, + resources: Doc<'resources'>[], +) { + const validEntries = await Promise.all( + resources.map(async (resource) => { + const soul = await ctx.db + .query('souls') + .withIndex('by_resource', (q) => q.eq('resourceId', resource._id)) + .unique() + if (!soul || soul.softDeletedAt || resource.softDeletedAt) return null + return { soul, resource } + }), + ) + + const entries = validEntries.filter( + (entry): entry is { soul: Doc<'souls'>; resource: Doc<'resources'> } => Boolean(entry), + ) + + const badgeMapByResourceId = await getResourceBadgeMaps( + ctx, + entries.map((entry) => entry.resource._id), + ) + + const hydrated = await Promise.all( + entries.map(async (entry) => { + const latestVersion = entry.soul.latestVersionId + ? await ctx.db.get(entry.soul.latestVersionId) + : null + const badges = badgeMapByResourceId.get(entry.resource._id) ?? {} + const publicSoul = toPublicSoul({ + ...mergeResourceIntoSoul(entry.soul, entry.resource), + badges, + }) + if (!publicSoul) return null + return { soul: publicSoul, latestVersion } + }), + ) + + return hydrated.filter( + (entry): entry is { soul: NonNullable>; latestVersion: Doc<'soulVersions'> | null } => + Boolean(entry), + ) +} + export const getBySlug = query({ args: { slug: v.string() }, handler: async (ctx, args) => { @@ -27,12 +89,18 @@ export const getBySlug = query({ .take(2) const soul = matches[0] ?? null if (!soul || soul.softDeletedAt) return null - const latestVersion = soul.latestVersionId ? await ctx.db.get(soul.latestVersionId) : null - const owner = toPublicUser(await ctx.db.get(soul.ownerUserId)) - const publicSoul = toPublicSoul(soul) + const [latestVersion, owner, resource, badges] = await Promise.all([ + soul.latestVersionId ? ctx.db.get(soul.latestVersionId) : null, + ctx.db.get(soul.ownerUserId), + soul.resourceId ? ctx.db.get(soul.resourceId) : null, + soul.resourceId ? getResourceBadgeMap(ctx, soul.resourceId) : Promise.resolve({}), + ]) + const publicSoul = toPublicSoul( + resource ? { ...mergeResourceIntoSoul(soul, resource), badges } : { ...soul, badges }, + ) if (!publicSoul) return null - return { soul: publicSoul, latestVersion, owner } + return { soul: publicSoul, latestVersion, owner: toPublicUser(owner) } }, }) @@ -57,26 +125,23 @@ export const list = query({ const limit = args.limit ?? 24 const ownerUserId = args.ownerUserId if (ownerUserId) { - const entries = await ctx.db - .query('souls') - .withIndex('by_owner', (q) => q.eq('ownerUserId', ownerUserId)) + const resources = await ctx.db + .query('resources') + .withIndex('by_type_owner_updated', (q) => + q.eq('type', 'soul').eq('ownerUserId', ownerUserId), + ) .order('desc') - .take(limit * 5) - return entries - .filter((soul) => !soul.softDeletedAt) - .slice(0, limit) - .map((soul) => toPublicSoul(soul)) - .filter((soul): soul is NonNullable => Boolean(soul)) + .take(limit * 3) + const entries = await buildPublicSoulEntriesFromResources(ctx, resources) + return entries.map((entry) => entry.soul).slice(0, limit) } - const entries = await ctx.db - .query('souls') + const resources = await ctx.db + .query('resources') + .withIndex('by_type_updated', (q) => q.eq('type', 'soul')) .order('desc') - .take(limit * 5) - return entries - .filter((soul) => !soul.softDeletedAt) - .slice(0, limit) - .map((soul) => toPublicSoul(soul)) - .filter((soul): soul is NonNullable => Boolean(soul)) + .take(limit * 3) + const entries = await buildPublicSoulEntriesFromResources(ctx, resources) + return entries.map((entry) => entry.soul).slice(0, limit) }, }) @@ -88,23 +153,14 @@ export const listPublicPage = query({ handler: async (ctx, args) => { const limit = clampInt(args.limit ?? 24, 1, MAX_LIST_LIMIT) const { page, isDone, continueCursor } = await ctx.db - .query('souls') - .withIndex('by_updated', (q) => q) + .query('resources') + .withIndex('by_type_active_updated', (q) => + q.eq('type', 'soul').eq('softDeletedAt', undefined), + ) .order('desc') .paginate({ cursor: args.cursor ?? null, numItems: limit }) - const items: Array<{ - soul: NonNullable> - latestVersion: Doc<'soulVersions'> | null - }> = [] - - for (const soul of page) { - if (soul.softDeletedAt) continue - const latestVersion = soul.latestVersionId ? await ctx.db.get(soul.latestVersionId) : null - const publicSoul = toPublicSoul(soul) - if (!publicSoul) continue - items.push({ soul: publicSoul, latestVersion }) - } + const items = await buildPublicSoulEntriesFromResources(ctx, page) return { items, nextCursor: isDone ? null : continueCursor } }, @@ -334,11 +390,13 @@ export const updateTags = mutation({ } const latestEntry = args.tags.find((entry) => entry.tag === 'latest') + const now = Date.now() await ctx.db.patch(soul._id, { tags: nextTags, latestVersionId: latestEntry ? latestEntry.versionId : soul.latestVersionId, - updatedAt: Date.now(), + updatedAt: now, }) + await upsertResourceForSoul(ctx, soul, { updatedAt: now }) if (latestEntry) { const embeddings = await ctx.db @@ -402,7 +460,29 @@ export const insertVersion = internalMutation({ const now = Date.now() if (!soul) { const summary = args.summary ?? getFrontmatterValue(args.parsed.frontmatter, 'description') + const resourceId = await ctx.db.insert('resources', { + type: 'soul', + slug: args.slug, + displayName: args.displayName, + summary: summary ?? undefined, + ownerUserId: userId, + ownerHandle: user.handle ?? user._id, + softDeletedAt: undefined, + statsDownloads: 0, + statsStars: 0, + statsInstallsCurrent: undefined, + statsInstallsAllTime: undefined, + stats: { + downloads: 0, + stars: 0, + versions: 0, + comments: 0, + }, + createdAt: now, + updatedAt: now, + }) const soulId = await ctx.db.insert('souls', { + resourceId, slug: args.slug, displayName: args.displayName, summary: summary ?? undefined, @@ -453,14 +533,30 @@ export const insertVersion = internalMutation({ const latestBefore = soul.latestVersionId + const nextStats = { ...soul.stats, versions: soul.stats.versions + 1 } + const nextSummary = + args.summary ?? getFrontmatterValue(args.parsed.frontmatter, 'description') ?? soul.summary await ctx.db.patch(soul._id, { displayName: args.displayName, - summary: - args.summary ?? getFrontmatterValue(args.parsed.frontmatter, 'description') ?? soul.summary, + summary: nextSummary, latestVersionId: versionId, tags: nextTags, - stats: { ...soul.stats, versions: soul.stats.versions + 1 }, + stats: nextStats, + softDeletedAt: undefined, + updatedAt: now, + }) + await upsertResourceForSoul(ctx, soul, { + displayName: args.displayName, + summary: nextSummary ?? undefined, softDeletedAt: undefined, + statsDownloads: nextStats.downloads, + statsStars: nextStats.stars, + stats: { + downloads: nextStats.downloads, + stars: nextStats.stars, + versions: nextStats.versions, + comments: nextStats.comments, + }, updatedAt: now, }) @@ -530,6 +626,10 @@ export const setSoulSoftDeletedInternal = internalMutation({ softDeletedAt: args.deleted ? now : undefined, updatedAt: now, }) + await upsertResourceForSoul(ctx, soul, { + softDeletedAt: args.deleted ? now : undefined, + updatedAt: now, + }) const embeddings = await ctx.db .query('soulEmbeddings') diff --git a/convex/stars.ts b/convex/stars.ts index 03d2d86..149a1d5 100644 --- a/convex/stars.ts +++ b/convex/stars.ts @@ -1,6 +1,7 @@ import { v } from 'convex/values' import { internalMutation, mutation, query } from './_generated/server' import { requireUser } from './lib/access' +import { isSkillPublic } from './lib/moderation' import { toPublicSkill } from './lib/public' import { insertStatEvent } from './skillStatEvents' @@ -58,6 +59,7 @@ export const listByUser = query({ const skills: NonNullable>[] = [] for (const star of stars) { const skill = await ctx.db.get(star.skillId) + if (!skill || !isSkillPublic(skill)) continue const publicSkill = toPublicSkill(skill) if (!publicSkill) continue skills.push(publicSkill) diff --git a/convex/statsMaintenance.ts b/convex/statsMaintenance.ts index 35f8206..6463505 100644 --- a/convex/statsMaintenance.ts +++ b/convex/statsMaintenance.ts @@ -3,6 +3,7 @@ import { internal } from './_generated/api' import type { Doc } from './_generated/dataModel' import type { ActionCtx } from './_generated/server' import { internalAction, internalMutation, internalQuery } from './_generated/server' +import { upsertResourceForSkill } from './lib/resource' const DEFAULT_BATCH_SIZE = 200 const MAX_BATCH_SIZE = 1000 @@ -27,6 +28,13 @@ export const backfillSkillStatFieldsInternal = internalMutation({ const next = buildSkillStatPatch(skill) if (!next) continue await ctx.db.patch(skill._id, next) + await upsertResourceForSkill(ctx, skill, { + stats: skill.stats, + statsDownloads: next.statsDownloads, + statsStars: next.statsStars, + statsInstallsCurrent: next.statsInstallsCurrent, + statsInstallsAllTime: next.statsInstallsAllTime, + }) patched += 1 } diff --git a/convex/users.ts b/convex/users.ts index 5c53001..d9dd232 100644 --- a/convex/users.ts +++ b/convex/users.ts @@ -2,7 +2,7 @@ import { getAuthUserId } from '@convex-dev/auth/server' import { v } from 'convex/values' import { internal } from './_generated/api' import { internalQuery, mutation, query } from './_generated/server' -import { assertAdmin, requireUser } from './lib/access' +import { assertAdmin, assertModerator, requireUser } from './lib/access' import { toPublicUser } from './lib/public' const DEFAULT_ROLE = 'user' @@ -18,11 +18,31 @@ export const getByIdInternal = internalQuery({ handler: async (ctx, args) => ctx.db.get(args.userId), }) +export const getByHandleInternal = internalQuery({ + args: { handle: v.string() }, + handler: async (ctx, args) => { + return ctx.db + .query('users') + .withIndex('handle', (q) => q.eq('handle', args.handle)) + .unique() + }, +}) + +const AUTH_BYPASS = process.env.AUTH_BYPASS === 'true' + export const me = query({ args: {}, handler: async (ctx) => { const userId = await getAuthUserId(ctx) - if (!userId) return null + if (!userId) { + if (!AUTH_BYPASS) return null + const user = await ctx.db + .query('users') + .withIndex('handle', (q) => q.eq('handle', 'local')) + .unique() + if (!user || user.deletedAt) return null + return user + } const user = await ctx.db.get(userId) if (!user || user.deletedAt) return null return user @@ -101,6 +121,20 @@ export const getByHandle = query({ }, }) +export const lookupByHandle = query({ + args: { handle: v.string() }, + handler: async (ctx, args) => { + const { user } = await requireUser(ctx) + assertModerator(user) + const handle = args.handle.trim() + if (!handle) return null + return ctx.db + .query('users') + .withIndex('handle', (q) => q.eq('handle', handle)) + .unique() + }, +}) + export const setRole = mutation({ args: { userId: v.id('users'), diff --git a/docs/architecture.md b/docs/architecture.md index 0eabeb5..7642f6b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -12,7 +12,7 @@ read_when: - Web app: TanStack Start (React) under `src/`. - Backend: Convex under `convex/` (DB, storage, actions, HTTP routes). - CLI: `packages/clawdhub/` (published as `clawhub`, legacy `clawdhub`). -- Shared schemas/routes: `packages/schema/` (`clawhub-schema`). +- Shared schemas/routes: `packages/schema/` (`molthub-schema`). ## Data + storage diff --git a/e2e/clawdhub.e2e.test.ts b/e2e/clawdhub.e2e.test.ts index d5ed667..1dcaa39 100644 --- a/e2e/clawdhub.e2e.test.ts +++ b/e2e/clawdhub.e2e.test.ts @@ -9,7 +9,7 @@ import { ApiV1SearchResponseSchema, ApiV1WhoamiResponseSchema, parseArk, -} from 'clawhub-schema' +} from 'molthub-schema' import { unzipSync } from 'fflate' import { Agent, setGlobalDispatcher } from 'undici' import { describe, expect, it } from 'vitest' diff --git a/package.json b/package.json index 2fd483a..9dbb891 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "clawhub", + "name": "molthub", "private": true, "type": "module", "workspaces": [ @@ -21,8 +21,9 @@ "convex:deploy": "bunx convex deploy --typecheck=disable --yes", "lint": "bun run lint:biome && bun run lint:oxlint", "lint:biome": "biome check .", - "lint:oxlint": "oxlint --type-aware --tsconfig ./tsconfig.oxlint.json ./src ./convex ./packages/clawdhub/src ./packages/schema/src", - "format": "biome format --write ." + "lint:oxlint": "oxlint --type-aware --tsconfig ./tsconfig.oxlint.json ./src ./convex ./packages/molthub/src ./packages/schema/src", + "format": "biome format --write .", + "seed:local": "bunx convex run devSeed:seedNixSkillsPublic '{\"reset\":true}'" }, "dependencies": { "@auth/core": "^0.37.4", @@ -31,8 +32,18 @@ "@fontsource/ibm-plex-mono": "^5.2.7", "@fontsource/manrope": "^5.2.8", "@monaco-editor/react": "^4.7.0", + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.3", + "@radix-ui/react-navigation-menu": "^1.2.8", + "@radix-ui/react-scroll-area": "^1.2.4", + "@radix-ui/react-select": "^2.1.12", + "@radix-ui/react-separator": "^1.1.3", + "@radix-ui/react-switch": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.6", "@radix-ui/react-toggle-group": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.4", "@resvg/resvg-wasm": "^2.6.2", "@tailwindcss/vite": "^4.1.18", "@tanstack/react-devtools": "^0.9.2", @@ -41,7 +52,8 @@ "@tanstack/react-start": "^1.152.0", "@tanstack/router-plugin": "^1.151.6", "@vercel/analytics": "^1.6.1", - "clawhub-schema": "workspace:*", + "molthub-schema": "workspace:*", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "convex": "^1.31.6", "convex-helpers": "^0.1.111", diff --git a/packages/schema/README.md b/packages/schema/README.md index c8966fa..9a716ea 100644 --- a/packages/schema/README.md +++ b/packages/schema/README.md @@ -1,3 +1,3 @@ -# clawhub-schema +# molthub-schema Shared runtime schemas (ArkType) for OpenClaw. diff --git a/packages/schema/package.json b/packages/schema/package.json index 7bc5843..edab553 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -1,5 +1,5 @@ { - "name": "clawhub-schema", + "name": "molthub-schema", "version": "0.0.2", "private": true, "type": "module", diff --git a/packages/schema/src/schemas.test.ts b/packages/schema/src/schemas.test.ts index 018d419..bdf0520 100644 --- a/packages/schema/src/schemas.test.ts +++ b/packages/schema/src/schemas.test.ts @@ -10,7 +10,7 @@ import { WellKnownConfigSchema, } from './schemas' -describe('clawhub-schema', () => { +describe('molthub-schema', () => { it('parses lockfile records', () => { const lock = parseArk( LockfileSchema, diff --git a/packages/schema/src/textFiles.test.ts b/packages/schema/src/textFiles.test.ts index dfb9f82..772eeb5 100644 --- a/packages/schema/src/textFiles.test.ts +++ b/packages/schema/src/textFiles.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest' import * as schema from '.' import { isTextContentType, TEXT_FILE_EXTENSION_SET } from './textFiles' -describe('clawhub-schema textFiles', () => { +describe('molthub-schema textFiles', () => { it('exports text-file extension set', () => { expect(TEXT_FILE_EXTENSION_SET.has('md')).toBe(true) expect(TEXT_FILE_EXTENSION_SET.has('exe')).toBe(false) diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..762c90f --- /dev/null +++ b/plan.md @@ -0,0 +1,66 @@ +# UI Rebuild Plan (Shadcn/Base) — Ordered Sequence + +## Implementation Notes (do this throughout) +- No temporary files, placeholder components, or transitional UI. Replace in place. +- Apply redirects immediately when switching routes (no staging period). +- Use `rg` for searches and read files before editing. +- Keep changes scoped to UI; backend changes only for UI cleanup/dedupe. + +## 1) Foundations: Shared Resource Adapter +- Define `ResourceType = 'skill' | 'soul' | 'extension'`. +- Create shared helpers: + - `getResourceLabel()` + - `getResourceLink()` + - `getResourceOwner()` + - `getResourceBadge()` +- Add routing helper: `toCanonicalResourcePath(type, owner, slug)`. + +## 2) Shadcn Theme + Tokens (keep current palette + fonts) +- Use this exact shadcn setup (maia + stone + hugeicons): + ``` + bunx --bun shadcn@latest create --preset "https://ui.shadcn.com/init?base=base&style=maia&baseColor=stone&theme=stone&iconLibrary=hugeicons&font=inter&menuAccent=subtle&menuColor=default&radius=default&template=start" --template start + ``` +- Port existing color palette into shadcn CSS variables. +- Preserve current font stack (Bricolage Grotesque, Manrope, IBM Plex Mono). +- Replace custom CSS with shadcn utilities + component styles. + +## 3) Global Layout +- Rebuild `Header` + `Footer` with shadcn: + - `NavigationMenu`, `DropdownMenu`, `Button`, `Avatar`, `Sheet`. +- Add shared `PageShell` + `SectionHeader`. + +## 4) Core Pages (Skills path first) +- `/` Home → rebuild hero + sections with shared cards. +- `/skills` → rebuild toolbar + list/grid with shared resource components. +- `/$owner/$slug` → rebuild detail view using `ResourceDetailShell`. + +## 5) Souls + Extensions +- Rebuild souls on shared components. +- Extensions ready to drop in with same adapter. + +## 6) Upload / Import / Settings / Dashboard +- `/upload`: shadcn form layout + dropzone card. +- `/dashboard`: shared resource dashboard cards. +- `/settings`: shadcn form fields + token UI. + +## 7) Moderation Overhaul → `/moderation` +- Replace `/management` with `/moderation`. +- Tabs: Queue, Reports, Duplicates, Recent, Users. +- Cards modeled after Modrinth (queue + reports). +- Optional right-side detail drawer for actions. +- No `/management` redirect; remove the route entirely. + +## 8) Type Rewrite Roadmap (future-ready) +### Phase A — Add canonical routes +``` +/skills/:owner/:slug +/souls/:owner/:slug +/extensions/:owner/:slug +``` + +### Phase B — Redirect legacy (yolo) +- Immediately redirect `/$owner/$slug` → canonical. +- Update all internal links to use canonical helper. + +### Phase C — Deprecate legacy +- Remove the legacy route after redirect is stable (no soft-keep). diff --git a/server/og/fetchSoulOgMeta.ts b/server/og/fetchSoulOgMeta.ts index 520b6a7..c5cd0da 100644 --- a/server/og/fetchSoulOgMeta.ts +++ b/server/og/fetchSoulOgMeta.ts @@ -2,6 +2,7 @@ export type SoulOgMeta = { displayName: string | null summary: string | null owner: string | null + ownerId: string | null version: string | null } @@ -12,13 +13,14 @@ export async function fetchSoulOgMeta(slug: string, apiBase: string): Promise { const title = titleFromQuery || meta?.displayName || slug const description = descriptionFromQuery || meta?.summary || '' - const ownerLabel = owner ? `@${owner}` : 'clawhub' + const ownerLabel = owner ? `@${owner}` : 'molthub' const versionLabel = version ? `v${version}` : 'latest' - const footer = owner ? `clawhub.ai/${owner}/${slug}` : `clawhub.ai/skills/${slug}` + const footer = `molthub.com/skills/${owner || 'unknown'}/${slug}` const cacheKey = version ? 'public, max-age=31536000, immutable' : 'public, max-age=3600' setHeader(event, 'Cache-Control', cacheKey) diff --git a/server/routes/og/soul.png.ts b/server/routes/og/soul.png.ts index 8ea32f9..8d3c2c7 100644 --- a/server/routes/og/soul.png.ts +++ b/server/routes/og/soul.png.ts @@ -47,8 +47,8 @@ async function ensureWasm() { } function buildFooter(slug: string, owner: string | null) { - if (owner) return `@${owner}/${slug}` - return `souls/${slug}` + if (owner) return `souls/${owner}/${slug}` + return `souls/unknown/${slug}` } export default defineEventHandler(async (event) => { @@ -70,13 +70,14 @@ export default defineEventHandler(async (event) => { : null const owner = ownerFromQuery || meta?.owner || '' + const ownerId = meta?.ownerId || '' const version = versionFromQuery || meta?.version || '' const title = titleFromQuery || meta?.displayName || slug const description = descriptionFromQuery || meta?.summary || '' const ownerLabel = owner ? `@${owner}` : 'SoulHub' const versionLabel = version ? `v${version}` : 'latest' - const footer = buildFooter(slug, owner || null) + const footer = buildFooter(slug, owner || ownerId || null) const cacheKey = version ? 'public, max-age=31536000, immutable' : 'public, max-age=3600' setHeader(event, 'Cache-Control', cacheKey) diff --git a/src/__tests__/skill-detail-page.test.tsx b/src/__tests__/skill-detail-page.test.tsx index b13363b..f4639d3 100644 --- a/src/__tests__/skill-detail-page.test.tsx +++ b/src/__tests__/skill-detail-page.test.tsx @@ -78,7 +78,7 @@ describe('SkillDetailPage', () => { expect(navigateMock).toHaveBeenCalled() }) expect(navigateMock).toHaveBeenCalledWith({ - to: '/$owner/$slug', + to: '/skills/$owner/$slug', params: { owner: 'steipete', slug: 'weather' }, replace: true, }) diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 44725b2..986e83a 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -1,27 +1,33 @@ +import { Separator } from './ui/separator' import { getSiteName } from '../lib/site' export function Footer() { const siteName = getSiteName() return ( - diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 39101d3..918b4b4 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -4,10 +4,12 @@ import { Menu, Monitor, Moon, Sun } from 'lucide-react' import { useMemo, useRef } from 'react' import { gravatarUrl } from '../lib/gravatar' import { isModerator } from '../lib/roles' -import { getOpenClawSiteUrl, getSiteMode, getSiteName } from '../lib/site' +import { getMoltHubSiteUrl, getSiteMode, getSiteName } from '../lib/site' import { applyTheme, useThemeMode } from '../lib/theme' import { startThemeTransition } from '../lib/theme-transition' import { useAuthStatus } from '../lib/useAuthStatus' +import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar' +import { Button } from './ui/button' import { DropdownMenu, DropdownMenuContent, @@ -15,22 +17,30 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from './ui/dropdown-menu' +import { + NavigationMenu, + NavigationMenuItem, + NavigationMenuLink, + NavigationMenuList, +} from './ui/navigation-menu' +import { Separator } from './ui/separator' +import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from './ui/sheet' import { ToggleGroup, ToggleGroupItem } from './ui/toggle-group' export default function Header() { - const { isAuthenticated, isLoading, me } = useAuthStatus() + const { isAuthenticated, isLoading, me, bypassEnabled } = useAuthStatus() const { signIn, signOut } = useAuthActions() const { mode, setMode } = useThemeMode() const toggleRef = useRef(null) const siteMode = getSiteMode() const siteName = useMemo(() => getSiteName(siteMode), [siteMode]) const isSoulMode = siteMode === 'souls' - const clawdHubUrl = getOpenClawSiteUrl() + const moltHubUrl = getMoltHubSiteUrl() const avatar = me?.image ?? (me?.email ? gravatarUrl(me.email) : undefined) const handle = me?.handle ?? me?.displayName ?? 'user' const initial = (me?.displayName ?? me?.name ?? handle).charAt(0).toUpperCase() - const isStaff = isModerator(me) + const isStaff = isModerator(me) || bypassEnabled const setTheme = (next: 'system' | 'light' | 'dark') => { startThemeTransition({ @@ -45,191 +55,102 @@ export default function Header() { }) } + const navLinks = [ + ...(isSoulMode + ? ([{ href: moltHubUrl, label: 'MoltHub' }] as const) + : ([] as const)), + { + label: isSoulMode ? 'Souls' : 'Skills', + to: isSoulMode ? '/souls' : '/skills', + search: isSoulMode + ? { + q: undefined, + sort: undefined, + dir: undefined, + view: undefined, + focus: undefined, + } + : { + q: undefined, + sort: undefined, + dir: undefined, + highlighted: undefined, + view: undefined, + focus: undefined, + }, + }, + ...(isSoulMode ? ([] as const) : ([{ label: 'Extensions', to: '/extensions' }] as const)), + { + label: 'Upload', + to: '/upload', + search: { updateSlug: undefined }, + }, + ...(isSoulMode + ? ([] as const) + : ([{ label: 'Import', to: '/import' }] as const)), + { + label: 'Search', + to: isSoulMode ? '/souls' : '/skills', + search: isSoulMode + ? { + q: undefined, + sort: undefined, + dir: undefined, + view: undefined, + focus: 'search', + } + : { + q: undefined, + sort: undefined, + dir: undefined, + highlighted: undefined, + view: undefined, + focus: 'search', + }, + }, + ...(me ? ([{ label: 'Stars', to: '/stars' }] as const) : ([] as const)), + ...(isStaff ? ([{ label: 'Moderation', to: '/moderation' }] as const) : ([] as const)), + ] + return ( -
-
+
+
- - + + - {siteName} + {siteName} - -
-
- - - - - - {isSoulMode ? ( - - OpenClaw - - ) : null} - - {isSoulMode ? ( - - Souls - - ) : ( - - Skills + + + + {navLinks.map((link) => ( + + {'href' in link ? ( + + {link.label} + + ) : ( + + + {link.label} - )} - - - - Upload - - - {isSoulMode ? null : ( - - Import - + )} - - - Search - - - {me ? ( - - Stars - - ) : null} - {isStaff ? ( - - - Management - - - ) : null} - - setTheme('system')}> - - setTheme('light')}> - - setTheme('dark')}> - - - -
-
+ + ))} + + + +
+
- {isAuthenticated && me ? ( + + {isAuthenticated && (me || bypassEnabled) ? ( - + @@ -274,20 +197,60 @@ export default function Header() { Settings - void signOut()}>Sign out + {bypassEnabled ? null : ( + void signOut()}>Sign out + )} ) : ( - + Sign in with GitHub + )} + + + + + + + + Navigation + +
+ {navLinks.map((link) => ( +
+ {'href' in link ? ( + + {link.label} + + ) : ( + + {link.label} + + )} +
+ ))} + +
+ + + +
+
+
+
diff --git a/src/components/InstallSwitcher.tsx b/src/components/InstallSwitcher.tsx index 5909ac3..7411482 100644 --- a/src/components/InstallSwitcher.tsx +++ b/src/components/InstallSwitcher.tsx @@ -1,4 +1,6 @@ import { useMemo, useState } from 'react' +import { cn } from '../lib/utils' +import { Button } from './ui/button' type PackageManager = 'npm' | 'pnpm' | 'bun' @@ -27,27 +29,29 @@ export function InstallSwitcher({ exampleSlug = 'sonoscli' }: InstallSwitcherPro }, [exampleSlug, pm]) return ( -
-
-
Install any skill folder in one shot:
-
+
+
+
Install any skill folder in one shot:
+
{PACKAGE_MANAGERS.map((entry) => ( - + ))}
-
{command}
+
+ {command} +
) } diff --git a/src/components/PageShell.tsx b/src/components/PageShell.tsx new file mode 100644 index 0000000..cb8c66a --- /dev/null +++ b/src/components/PageShell.tsx @@ -0,0 +1,22 @@ +import type { ElementType, HTMLAttributes, ReactNode } from 'react' +import { cn } from '../lib/utils' + +type PageShellProps = { + as?: T + className?: string + children: ReactNode +} & Omit, 'as' | 'className'> + +export function PageShell({ + as, + className, + children, + ...props +}: PageShellProps) { + const Comp = (as ?? 'main') as ElementType + return ( + + {children} + + ) +} diff --git a/src/components/ResourceCard.tsx b/src/components/ResourceCard.tsx new file mode 100644 index 0000000..7e77676 --- /dev/null +++ b/src/components/ResourceCard.tsx @@ -0,0 +1,59 @@ +import { Link } from '@tanstack/react-router' +import type { ReactNode } from 'react' +import type { PublicResource, PublicSkill, PublicSoul } from '../lib/publicUser' +import { getResourceBadge, getResourceLink, type ResourceType } from '../lib/resources' +import { Badge } from './ui/badge' +import { Card } from './ui/card' +import { cn } from '../lib/utils' + +type ResourceCardProps = { + type: ResourceType + resource: PublicSkill | PublicSoul | PublicResource + ownerHandle?: string | null + summaryFallback: string + meta: ReactNode + badges?: string[] + chip?: string + href?: string + className?: string +} + +export function ResourceCard({ + type, + resource, + ownerHandle, + summaryFallback, + meta, + badges, + chip, + href, + className, +}: ResourceCardProps) { + const link = + href ?? getResourceLink(type, resource, resource.slug, ownerHandle ?? null) + const resolvedBadges = badges ?? getResourceBadge(type, resource) + + return ( + + + {resolvedBadges.length || chip ? ( +
+ {resolvedBadges.map((label) => ( + + {label} + + ))} + {chip ? {chip} : null} +
+ ) : null} +
+

{resource.displayName}

+

+ {resource.summary ?? summaryFallback} +

+
+
{meta}
+
+ + ) +} diff --git a/src/components/ResourceDetailShell.tsx b/src/components/ResourceDetailShell.tsx new file mode 100644 index 0000000..e5938cd --- /dev/null +++ b/src/components/ResourceDetailShell.tsx @@ -0,0 +1,52 @@ +import type { ReactNode } from 'react' +import { Badge } from './ui/badge' +import { Card } from './ui/card' +import { cn } from '../lib/utils' + +type ResourceDetailShellProps = { + title: string + subtitle?: string + badges?: string[] + stats?: ReactNode + ownerLine?: ReactNode + actions?: ReactNode + note?: ReactNode + className?: string +} + +export function ResourceDetailShell({ + title, + subtitle, + badges = [], + stats, + ownerLine, + actions, + note, + className, +}: ResourceDetailShellProps) { + return ( + +
+
+
+

{title}

+ {subtitle ?

{subtitle}

: null} +
+ {note ?
{note}
: null} + {stats ?
{stats}
: null} + {ownerLine ?
{ownerLine}
: null} + {badges.length ? ( +
+ {badges.map((badge) => ( + + {badge} + + ))} +
+ ) : null} +
+ {actions ?
{actions}
: null} +
+
+ ) +} diff --git a/src/components/ResourceListRow.tsx b/src/components/ResourceListRow.tsx new file mode 100644 index 0000000..5f2790d --- /dev/null +++ b/src/components/ResourceListRow.tsx @@ -0,0 +1,53 @@ +import { Link } from '@tanstack/react-router' +import type { ReactNode } from 'react' +import type { PublicResource, PublicSkill, PublicSoul } from '../lib/publicUser' +import { getResourceBadge, getResourceLink, type ResourceType } from '../lib/resources' +import { Badge } from './ui/badge' + +type ResourceListRowProps = { + type: ResourceType + resource: PublicSkill | PublicSoul | PublicResource + ownerHandle?: string | null + summaryFallback: string + meta?: ReactNode + badges?: string[] + chip?: string + href?: string +} + +export function ResourceListRow({ + type, + resource, + ownerHandle, + summaryFallback, + meta, + badges, + chip, + href, +}: ResourceListRowProps) { + const link = + href ?? getResourceLink(type, resource, resource.slug, ownerHandle ?? null) + const resolvedBadges = badges ?? getResourceBadge(type, resource) + + return ( + +
+ {resource.displayName} + /{resource.slug} + {resolvedBadges.map((badge) => ( + + {badge} + + ))} + {chip ? {chip} : null} +
+

+ {resource.summary ?? summaryFallback} +

+ {meta ?
{meta}
: null} + + ) +} diff --git a/src/components/SectionHeader.tsx b/src/components/SectionHeader.tsx new file mode 100644 index 0000000..c08a3fe --- /dev/null +++ b/src/components/SectionHeader.tsx @@ -0,0 +1,21 @@ +import type { ReactNode } from 'react' +import { cn } from '../lib/utils' + +type SectionHeaderProps = { + title: string + description?: string + actions?: ReactNode + className?: string +} + +export function SectionHeader({ title, description, actions, className }: SectionHeaderProps) { + return ( +
+
+

{title}

+ {description ?

{description}

: null} +
+ {actions ?
{actions}
: null} +
+ ) +} diff --git a/src/components/SkillCard.tsx b/src/components/SkillCard.tsx index 381f234..2a8857c 100644 --- a/src/components/SkillCard.tsx +++ b/src/components/SkillCard.tsx @@ -1,6 +1,7 @@ -import { Link } from '@tanstack/react-router' import type { ReactNode } from 'react' import type { PublicSkill } from '../lib/publicUser' +import { getSkillBadges } from '../lib/badges' +import { ResourceCard } from './ResourceCard' type SkillCardProps = { skill: PublicSkill @@ -9,28 +10,30 @@ type SkillCardProps = { summaryFallback: string meta: ReactNode href?: string + ownerHandle?: string | null } -export function SkillCard({ skill, badge, chip, summaryFallback, meta, href }: SkillCardProps) { - const owner = encodeURIComponent(String(skill.ownerUserId)) - const link = href ?? `/${owner}/${skill.slug}` - const badges = Array.isArray(badge) ? badge : badge ? [badge] : [] +export function SkillCard({ + skill, + badge, + chip, + summaryFallback, + meta, + href, + ownerHandle, +}: SkillCardProps) { + const badges = Array.isArray(badge) ? badge : badge ? [badge] : getSkillBadges(skill) return ( - - {badges.length || chip ? ( -
- {badges.map((label) => ( -
- {label} -
- ))} - {chip ?
{chip}
: null} -
- ) : null} -

{skill.displayName}

-

{skill.summary ?? summaryFallback}

-
{meta}
- + ) } diff --git a/src/components/SkillDetailPage.tsx b/src/components/SkillDetailPage.tsx index d6b54c5..85dda1a 100644 --- a/src/components/SkillDetailPage.tsx +++ b/src/components/SkillDetailPage.tsx @@ -1,7 +1,7 @@ import { Link, useNavigate } from '@tanstack/react-router' -import type { ClawdisSkillMetadata, SkillInstallSpec } from 'clawhub-schema' import { useAction, useMutation, useQuery } from 'convex/react' -import { useEffect, useMemo, useState } from 'react' +import type { MoltbotSkillMetadata, SkillInstallSpec } from 'molthub-schema' +import { useEffect, useMemo, useRef, useState } from 'react' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' import { api } from '../../convex/_generated/api' @@ -10,7 +10,23 @@ import { getSkillBadges } from '../lib/badges' import type { PublicSkill, PublicUser } from '../lib/publicUser' import { canManageSkill, isModerator } from '../lib/roles' import { useAuthStatus } from '../lib/useAuthStatus' +import { toCanonicalResourcePath } from '../lib/resources' +import { PageShell } from './PageShell' +import { ResourceDetailShell } from './ResourceDetailShell' import { SkillDiffCard } from './SkillDiffCard' +import { Badge } from './ui/badge' +import { Button, buttonVariants } from './ui/button' +import { Card } from './ui/card' +import { Input } from './ui/input' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from './ui/select' +import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs' +import { Textarea } from './ui/textarea' type SkillDetailPageProps = { slug: string @@ -23,15 +39,11 @@ type SkillBySlugResult = { latestVersion: Doc<'skillVersions'> | null owner: PublicUser | null forkOf: { - kind: 'fork' | 'duplicate' + kind: 'fork' version: string | null skill: { slug: string; displayName: string } owner: { handle: string | null; userId: Id<'users'> | null } } | null - canonical: { - skill: { slug: string; displayName: string } - owner: { handle: string | null; userId: Id<'users'> | null } - } | null } | null type SkillFile = Doc<'skillVersions'>['files'][number] @@ -90,25 +102,18 @@ export function SkillDetailPage({ (typeof canonicalOwner === 'string' && canonicalOwner && canonicalOwner !== ownerParam)), ) - const forkOf = result?.forkOf ?? null - const canonical = result?.canonical ?? null - const forkOfLabel = forkOf?.kind === 'duplicate' ? 'duplicate of' : 'fork of' + const forkOf = result?.forkOf?.kind === 'fork' ? result?.forkOf : null + const forkOfLabel = 'fork of' const forkOfOwnerHandle = forkOf?.owner?.handle ?? null const forkOfOwnerId = forkOf?.owner?.userId ?? null - const canonicalOwnerHandle = canonical?.owner?.handle ?? null - const canonicalOwnerId = canonical?.owner?.userId ?? null const forkOfHref = forkOf?.skill?.slug ? buildSkillHref(forkOfOwnerHandle, forkOfOwnerId, forkOf.skill.slug) : null - const canonicalHref = - canonical?.skill?.slug && canonical.skill.slug !== forkOf?.skill?.slug - ? buildSkillHref(canonicalOwnerHandle, canonicalOwnerId, canonical.skill.slug) - : null useEffect(() => { if (!wantsCanonicalRedirect || !ownerParam) return void navigate({ - to: '/$owner/$slug', + to: '/skills/$owner/$slug', params: { owner: ownerParam, slug }, replace: true, }) @@ -117,26 +122,26 @@ export function SkillDetailPage({ const versionById = new Map, Doc<'skillVersions'>>( (diffVersions ?? versions ?? []).map((version) => [version._id, version]), ) - const clawdis = (latestVersion?.parsed as { clawdis?: ClawdisSkillMetadata } | undefined)?.clawdis - const osLabels = useMemo(() => formatOsList(clawdis?.os), [clawdis?.os]) - const requirements = clawdis?.requires - const installSpecs = clawdis?.install ?? [] - const nixPlugin = clawdis?.nix?.plugin - const nixSystems = clawdis?.nix?.systems ?? [] + const moltbot = (latestVersion?.parsed as { moltbot?: MoltbotSkillMetadata } | undefined)?.moltbot + const osLabels = useMemo(() => formatOsList(moltbot?.os), [moltbot?.os]) + const requirements = moltbot?.requires + const installSpecs = moltbot?.install ?? [] + const nixPlugin = moltbot?.nix?.plugin + const nixSystems = moltbot?.nix?.systems ?? [] const nixSnippet = nixPlugin ? formatNixInstallSnippet(nixPlugin) : null - const configRequirements = clawdis?.config + const configRequirements = moltbot?.config const configExample = configRequirements?.example ? formatConfigSnippet(configRequirements.example) : null - const cliHelp = clawdis?.cliHelp + const cliHelp = moltbot?.cliHelp const hasRuntimeRequirements = Boolean( - clawdis?.emoji || + moltbot?.emoji || osLabels.length || requirements?.bins?.length || requirements?.anyBins?.length || requirements?.env?.length || requirements?.config?.length || - clawdis?.primaryEnv, + moltbot?.primaryEnv, ) const hasInstallSpecs = installSpecs.length > 0 const hasPluginBundle = Boolean(nixSnippet || configRequirements || cliHelp) @@ -146,12 +151,19 @@ export function SkillDetailPage({ }, [readme]) const latestFiles: SkillFile[] = latestVersion?.files ?? [] + const latestVersionId = latestVersion?._id + const getReadmeRef = useRef(getReadme) + useEffect(() => { - if (!latestVersion) return + getReadmeRef.current = getReadme + }, [getReadme]) + + useEffect(() => { + if (!latestVersionId) return setReadme(null) setReadmeError(null) let cancelled = false - void getReadme({ versionId: latestVersion._id }) + void getReadmeRef.current({ versionId: latestVersionId }) .then((data) => { if (cancelled) return setReadme(data.text) @@ -164,202 +176,190 @@ export function SkillDetailPage({ return () => { cancelled = true } - }, [latestVersion, getReadme]) + }, [latestVersionId]) useEffect(() => { - if (!tagVersionId && latestVersion) { - setTagVersionId(latestVersion._id) - } - }, [latestVersion, tagVersionId]) + if (!latestVersionId) return + setTagVersionId((current) => (current ? current : latestVersionId)) + }, [latestVersionId]) if (isLoadingSkill || wantsCanonicalRedirect) { return ( -
-
-
Loading skill…
-
+
+ + Loading skill… +
) } if (result === null || !skill) { return ( -
-
Skill not found.
+
+ + Skill not found. +
) } const tagEntries = Object.entries(skill.tags ?? {}) as Array<[string, Id<'skillVersions'>]> + const tagVersionValue = tagVersionId ? String(tagVersionId) : '' return ( -
-
-
-
-
-
-
-

- {skill.displayName} -

- {nixPlugin ? Plugin bundle (nix) : null} -
-

{skill.summary ?? 'No summary provided.'}

- - {nixPlugin ? ( -
- Bundles the skill pack, CLI binary, and config requirements in one Nix install. -
- ) : null} -
- ⭐ {skill.stats.stars} · ⤓ {skill.stats.downloads} · ⤒{' '} - {skill.stats.installsCurrent ?? 0} current · {skill.stats.installsAllTime ?? 0}{' '} - all-time -
- {owner?.handle ? ( - - ) : null} - {forkOf && forkOfHref ? ( -
- {forkOfLabel}{' '} - - {forkOfOwnerHandle ? `@${forkOfOwnerHandle}/` : ''} - {forkOf.skill.slug} - - {forkOf.version ? ` (based on ${forkOf.version})` : null} -
- ) : null} - {canonicalHref ? ( - - ) : null} - {getSkillBadges(skill).map((badge) => ( -
- {badge} -
- ))} -
- {isAuthenticated ? ( - - ) : null} - {isAuthenticated ? ( - - ) : null} - {isStaff ? ( - - Manage - - ) : null} -
-
-
-
- Current version - v{latestVersion?.version ?? '—'} +
+ + + ⭐ {skill.stats.stars} stars · ⤓ {skill.stats.downloads} downloads · ⤒{' '} + {skill.stats.installsCurrent ?? 0} current installs · {skill.stats.installsAllTime ?? 0}{' '} + total installs + + } + ownerLine={ +
+ {owner?.handle ? ( + - {!nixPlugin ? ( - - Download zip + ) : null} + {forkOf && forkOfHref ? ( +
+ {forkOfLabel}{' '} + + {forkOfOwnerHandle ? `@${forkOfOwnerHandle}/` : ''} + {forkOf.skill.slug} - ) : null} + {forkOf.version ? ` (based on ${forkOf.version})` : null} +
+ ) : null} +
+ } + actions={ +
+
+
Current version
+ v{latestVersion?.version ?? '—'}
+ {!nixPlugin ? ( + + Download zip + + ) : null} + {isAuthenticated ? ( + + ) : null} + {isAuthenticated ? ( + + ) : null} + {isStaff ? ( + + Moderation + + ) : null}
- {hasPluginBundle ? ( -
-
-
Plugin bundle (nix)
-
Skill pack · CLI binary · Config
-
-
- SKILL.md - CLI - Config -
- {configRequirements ? ( -
-
Config requirements
-
- {configRequirements.requiredEnv?.length ? ( -
- Required env - {configRequirements.requiredEnv.join(', ')} -
- ) : null} - {configRequirements.stateDirs?.length ? ( -
- State dirs - {configRequirements.stateDirs.join(', ')} -
- ) : null} -
+ } + /> + + {hasPluginBundle ? ( + +
+

Plugin bundle (nix)

+

Skill pack · CLI binary · Config

+
+
+ SKILL.md + CLI + Config +
+ {configRequirements ? ( +
+

Config requirements

+ {configRequirements.requiredEnv?.length ? ( +
+ Required env ·{' '} + {configRequirements.requiredEnv.join(', ')}
) : null} - {cliHelp ? ( -
- CLI help (from plugin) -
{cliHelp}
-
+ {configRequirements.stateDirs?.length ? ( +
+ State dirs ·{' '} + {configRequirements.stateDirs.join(', ')} +
) : null}
) : null} -
-
+ {cliHelp ? ( +
+ CLI help (from plugin) +
{cliHelp}
+
+ ) : null} + + ) : null} + + +
{tagEntries.length === 0 ? ( - - No tags yet. - + No tags yet. ) : ( tagEntries.map(([tag, versionId]) => ( - + {tag} - + v{versionById.get(versionId)?.version ?? versionId} - + )) )}
+ {canManage ? (
{ @@ -367,254 +367,199 @@ export function SkillDetailPage({ if (!tagName.trim() || !tagVersionId) return void updateTags({ skillId: skill._id, - tags: [{ tag: tagName.trim(), versionId: tagVersionId }], - }) + tags: [ + { + tag: tagName.trim(), + versionId: tagVersionId, + }, + ], + }).then(() => setTagName('')) }} - className="tag-form" + className="flex flex-col gap-3 md:flex-row md:items-end" > - setTagName(event.target.value)} - placeholder="latest" - /> - - -
- ) : null} - {hasRuntimeRequirements || hasInstallSpecs ? ( -
-
- {hasRuntimeRequirements ? ( -
-

- Runtime requirements -

-
- {clawdis?.emoji ?
{clawdis.emoji} Clawdis
: null} - {osLabels.length ? ( -
- OS - {osLabels.join(' · ')} -
- ) : null} - {requirements?.bins?.length ? ( -
- Bins - {requirements.bins.join(', ')} -
- ) : null} - {requirements?.anyBins?.length ? ( -
- Any bin - {requirements.anyBins.join(', ')} -
- ) : null} - {requirements?.env?.length ? ( -
- Env - {requirements.env.join(', ')} -
- ) : null} - {requirements?.config?.length ? ( -
- Config - {requirements.config.join(', ')} -
- ) : null} - {clawdis?.primaryEnv ? ( -
- Primary env - {clawdis.primaryEnv} -
- ) : null} -
-
- ) : null} - {hasInstallSpecs ? ( -
-

- Install -

-
- {installSpecs.map((spec, index) => { - const command = formatInstallCommand(spec) - return ( -
-
- {spec.label ?? formatInstallLabel(spec)} - {spec.bins?.length ? ( -
- Bins: {spec.bins.join(', ')} -
- ) : null} - {command ? {command} : null} -
-
- ) - })} -
-
- ) : null} +
+ + setTagName(event.target.value)} />
-
+
+ + +
+ + ) : null} -
+
+ + {(hasRuntimeRequirements || hasInstallSpecs) && ( +
+ {hasRuntimeRequirements ? ( + +

Runtime

+
+ {moltbot?.emoji ? {moltbot.emoji} Moltbot : null} + {osLabels.length ? ( + OS: {osLabels.join(', ')} + ) : null} + {requirements?.bins?.length ? ( + Bins: {requirements.bins.join(', ')} + ) : null} + {requirements?.anyBins?.length ? ( + Any bins: {requirements.anyBins.join(', ')} + ) : null} + {requirements?.env?.length ? ( + Env: {requirements.env.join(', ')} + ) : null} + {requirements?.config?.length ? ( + Config: {requirements.config.join(', ')} + ) : null} + {moltbot?.primaryEnv ? Primary env: {moltbot.primaryEnv} : null} +
+
+ ) : null} + + {hasInstallSpecs ? ( + +

Install

+
+ {installSpecs.map((spec, index) => { + const label = formatInstallLabel(spec) + const command = formatInstallCommand(spec) + return ( +
+ {label} + {command ?
{command}
: null} +
+ ) + })} +
+
+ ) : null} +
+ )} + {nixSnippet ? ( -
-

- Install via Nix -

-

- {nixSystems.length ? `Systems: ${nixSystems.join(', ')}` : 'nix-clawdbot'} + +

Nix install

+

+ Available systems: {nixSystems.length ? nixSystems.join(', ') : 'Unspecified'}.

-
+            
               {nixSnippet}
             
-
+ ) : null} + {configExample ? ( -
-

- Config example -

-

- Starter config for this plugin bundle. -

-
+          
+            

Config example

+
               {configExample}
             
-
+ ) : null} -
-
- - - -
- {activeTab === 'files' ? ( -
-
-

- SKILL.md -

-
- {readmeContent ? ( - {readmeContent} - ) : readmeError ? ( -
Failed to load SKILL.md: {readmeError}
- ) : ( -
Loading…
- )} -
+ + setActiveTab(value as typeof activeTab)}> + + README + Compare + Versions + + + +

SKILL.md

+
+ {readmeContent ? ( + {readmeContent} + ) : readmeError ? ( +
+ Failed to load SKILL.md: {readmeError} +
+ ) : ( +
Loading SKILL.md…
+ )}
-
-
-

- Files -

- - {latestFiles.length} total - -
-
- {latestFiles.length === 0 ? ( -
No files available.
- ) : ( - latestFiles.map((file) => ( -
- {file.path} - {formatBytes(file.size)} -
- )) - )} -
+ + +
+

Files

+ + {latestFiles.length} file{latestFiles.length === 1 ? '' : 's'} +
-
- ) : null} - {activeTab === 'compare' && skill ? ( -
- -
- ) : null} - {activeTab === 'versions' ? ( -
-
-

- Versions -

-

- {nixPlugin - ? 'Review release history and changelog.' - : 'Download older releases or scan the changelog.'} -

+
+ {latestFiles.length === 0 ? ( +
No files available.
+ ) : ( + latestFiles.map((file) => ( +
+ {file.path} + {formatBytes(file.size)} +
+ )) + )}
-
-
- {(versions ?? []).map((version) => ( -
-
-
- v{version.version} · {new Date(version.createdAt).toLocaleDateString()} - {version.changelogSource === 'auto' ? ( - · auto - ) : null} -
-
- {version.changelog} -
-
- {!nixPlugin ? ( - + + + + + } versions={diffVersions ?? []} /> + + + + +

Versions

+

Release history for this skill.

+
+ {(versions ?? []).map((version) => ( +
+
+ v{version.version} · {new Date(version.createdAt).toLocaleDateString()} + {version.changelogSource === 'auto' ? ( + · auto ) : null}
- ))} -
+
+ {version.changelog} +
+ +
+ ))}
-
- ) : null} -
-
-

- Comments -

+ + + + + +

Comments

{isAuthenticated ? (
{ @@ -624,54 +569,55 @@ export function SkillDetailPage({ setComment(''), ) }} - className="comment-form" + className="space-y-3" > -