mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 08:52:21 +00:00
Compare commits
37
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffa25f47da | ||
|
|
f866dc05d1 | ||
|
|
69436fd79b | ||
|
|
f185ca6f55 | ||
|
|
f3fc8d62b6 | ||
|
|
71f94a774f | ||
|
|
07349e6107 | ||
|
|
5ef00e8c6a | ||
|
|
c5e5e657dd | ||
|
|
69e1e5c507 | ||
|
|
481f1b9188 | ||
|
|
d4f5832554 | ||
|
|
ca3275ba92 | ||
|
|
d577add8a0 | ||
|
|
90bd065f7e | ||
|
|
219f05b160 | ||
|
|
c8091ee8a8 | ||
|
|
63f367e1d6 | ||
|
|
e701d7b713 | ||
|
|
79bd1f1d6c | ||
|
|
ac51cc0236 | ||
|
|
b9c23dc00e | ||
|
|
f4e96995bc | ||
|
|
33165db598 | ||
|
|
4c10e4847b | ||
|
|
8d68b55333 | ||
|
|
251de1f540 | ||
|
|
123f60fa93 | ||
|
|
643faf71c8 | ||
|
|
a2c46fbb5d | ||
|
|
f51e0a087d | ||
|
|
d9108b0948 | ||
|
|
d7a017e1c3 | ||
|
|
fffdf82540 | ||
|
|
a16e624766 | ||
|
|
decce1d35c | ||
|
|
468832af3f |
@@ -40,5 +40,6 @@
|
||||
- OAuth: GitHub OAuth App credentials required for login.
|
||||
|
||||
## Convex Ops (Gotchas)
|
||||
- New Convex functions must be pushed before `convex run`: use `bunx convex dev --once` (dev) or `bunx convex deploy --prod` (prod).
|
||||
- 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 <name>` / `--prod`.
|
||||
|
||||
+30
-5
@@ -1,5 +1,30 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
### Changed
|
||||
|
||||
### Fixed
|
||||
|
||||
## 0.4.0 - 2026-01-30
|
||||
|
||||
### Added
|
||||
- Web: show published skills on user profiles (thanks @njoylab, #20).
|
||||
- CLI: include OpenClaw + Moltbot fallback skill roots for sync scans.
|
||||
- CLI: support OpenClaw configuration files (`OPENCLAW_CONFIG_PATH` / `OPENCLAW_STATE_DIR`).
|
||||
|
||||
### Changed
|
||||
- Brand: rebrand to OpenClaw and publish CLI as `clawhub` (legacy `clawdhub` supported).
|
||||
- Domain: default site/registry now `https://clawhub.ai`; `.well-known/clawhub.json` preferred.
|
||||
- Theme: persist theme under `clawhub-theme` (legacy key still read).
|
||||
|
||||
### Fixed
|
||||
- Registry: drop missing skills during search hydration (thanks @aaronn, #28).
|
||||
- CLI: use path-based skill metadata lookup for updates (thanks @daveonkels, #22).
|
||||
- Search: keep highlighted-only filtering and clamp vector candidates to Convex limits (thanks @aaronn, #30).
|
||||
|
||||
## 0.3.0 - 2026-01-19
|
||||
|
||||
### Added
|
||||
@@ -26,7 +51,7 @@
|
||||
- Registry: make SoulHub auto-seed idempotent and non-user-owned.
|
||||
- Registry: keep GitHub backup state + publish backups intact (thanks @joshp123, #1).
|
||||
- CLI/Registry: restore fork lineage on sync + clamp bulk list queries (thanks @joshp123, #1).
|
||||
- CLI: default workdir falls back to Clawdbot workspace (override with `--workdir` / `CLAWDHUB_WORKDIR`).
|
||||
- CLI: default workdir falls back to Clawdbot workspace (override with `--workdir` / `CLAWHUB_WORKDIR`).
|
||||
|
||||
## 0.0.6 - 2026-01-07
|
||||
|
||||
@@ -42,7 +67,7 @@
|
||||
## 0.0.5 - 2026-01-06
|
||||
|
||||
### Added
|
||||
- Telemetry: track installs via `clawdhub sync` (logged-in only), per root, with 120-day staleness.
|
||||
- Telemetry: track installs via `clawhub sync` (logged-in only), per root, with 120-day staleness.
|
||||
- Skills: show current + all-time installs; sort by installs.
|
||||
- Profile: private "Installed" tab with JSON export + delete telemetry controls.
|
||||
- Docs: add `docs/telemetry.md` (what we track + how to opt out).
|
||||
@@ -50,7 +75,7 @@
|
||||
- Web: dashboard for managing your published skills (thanks @dbhurley!).
|
||||
|
||||
### Changed
|
||||
- CLI: telemetry opt-out via `CLAWDHUB_DISABLE_TELEMETRY=1`.
|
||||
- CLI: telemetry opt-out via `CLAWHUB_DISABLE_TELEMETRY=1`.
|
||||
- Web: move theme picker into mobile menu.
|
||||
|
||||
### Fixed
|
||||
@@ -98,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 `clawdhub-schema` publish).
|
||||
- Repo: mark `clawdhub-schema` as private to prevent publishing.
|
||||
- CLI package: bundle schema internally (no external `clawhub-schema` publish).
|
||||
- Repo: mark `clawhub-schema` as private to prevent publishing.
|
||||
|
||||
## 0.0.2 - 2026-01-04
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
# ClawdHub
|
||||
# OpenClaw
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/clawdbot/clawdhub/actions/workflows/ci.yml?branch=main"><img src="https://img.shields.io/github/actions/workflow/status/clawdbot/clawdhub/ci.yml?branch=main&style=for-the-badge" alt="CI status"></a>
|
||||
<a href="https://github.com/openclaw/clawhub/actions/workflows/ci.yml?branch=main"><img src="https://img.shields.io/github/actions/workflow/status/openclaw/clawhub/ci.yml?branch=main&style=for-the-badge" alt="CI status"></a>
|
||||
<a href="https://discord.gg/clawd"><img src="https://img.shields.io/discord/1456350064065904867?label=Discord&logo=discord&logoColor=white&color=5865F2&style=for-the-badge" alt="Discord"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge" alt="MIT License"></a>
|
||||
</p>
|
||||
|
||||
ClawdHub is the **public skill registry for Clawdbot**: publish, version, and search text-based agent skills (a `SKILL.md` plus supporting files).
|
||||
OpenClaw is the **public skill registry for Clawdbot**: publish, version, and search text-based agent skills (a `SKILL.md` plus supporting files).
|
||||
It’s designed for fast browsing + a CLI-friendly API, with moderation hooks and vector search.
|
||||
|
||||
onlycrabs.ai is the **SOUL.md registry**: publish and share system lore the same way you publish skills.
|
||||
|
||||
Live: `https://clawdhub.com`
|
||||
Live: `https://clawhub.ai`
|
||||
onlycrabs.ai: `https://onlycrabs.ai`
|
||||
|
||||
## What you can do
|
||||
@@ -27,7 +27,7 @@ onlycrabs.ai: `https://onlycrabs.ai`
|
||||
|
||||
- Entry point is host-based: `onlycrabs.ai`.
|
||||
- On the onlycrabs.ai host, the home page and nav default to souls.
|
||||
- On ClawdHub, souls live under `/souls`.
|
||||
- On OpenClaw, souls live under `/souls`.
|
||||
- Soul bundles only accept `SOUL.md` for now (no extra files).
|
||||
|
||||
## How it works (high level)
|
||||
@@ -35,15 +35,15 @@ 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` (`clawdhub-schema`).
|
||||
- API schema + routes: `packages/schema` (`clawhub-schema`).
|
||||
|
||||
## Telemetry
|
||||
|
||||
ClawdHub tracks minimal **install telemetry** (to compute install counts) when you run `clawdhub sync` while logged in.
|
||||
OpenClaw tracks minimal **install telemetry** (to compute install counts) when you run `clawhub sync` while logged in.
|
||||
Disable via:
|
||||
|
||||
```bash
|
||||
export CLAWDHUB_DISABLE_TELEMETRY=1
|
||||
export CLAWHUB_DISABLE_TELEMETRY=1
|
||||
```
|
||||
|
||||
Details: `docs/telemetry.md`.
|
||||
@@ -95,7 +95,7 @@ This writes `JWT_PRIVATE_KEY` + `JWKS` to the deployment and prints values for y
|
||||
|
||||
## Nix plugins (nixmode skills)
|
||||
|
||||
ClawdHub can store a nix-clawdbot plugin pointer in SKILL frontmatter so the registry knows which
|
||||
OpenClaw can store a nix-clawdbot plugin pointer in SKILL frontmatter so the registry knows which
|
||||
Nix package bundle to install. A nix plugin is different from a regular skill pack: it bundles the
|
||||
skill pack, the CLI binary, and its config flags/requirements together.
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.3.13/schema.json",
|
||||
"files": {
|
||||
"includes": [
|
||||
"**",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "clawdhub",
|
||||
"name": "clawhub",
|
||||
"dependencies": {
|
||||
"@auth/core": "^0.37.4",
|
||||
"@convex-dev/auth": "^0.0.90",
|
||||
@@ -21,9 +21,10 @@
|
||||
"@tanstack/react-start": "^1.152.0",
|
||||
"@tanstack/router-plugin": "^1.151.6",
|
||||
"@vercel/analytics": "^1.6.1",
|
||||
"clawdhub-schema": "workspace:*",
|
||||
"clawhub-schema": "workspace:*",
|
||||
"clsx": "^2.1.1",
|
||||
"convex": "^1.31.5",
|
||||
"convex": "^1.31.6",
|
||||
"convex-helpers": "^0.1.111",
|
||||
"fflate": "^0.8.2",
|
||||
"h3": "2.0.1-rc.8",
|
||||
"lucide-react": "^0.562.0",
|
||||
@@ -52,6 +53,7 @@
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"@vitest/coverage-v8": "^4.0.17",
|
||||
"jsdom": "^27.4.0",
|
||||
"only-allow": "^1.2.1",
|
||||
"oxlint": "^1.39.0",
|
||||
"oxlint-tsgolint": "^0.11.1",
|
||||
"typescript": "^5.9.3",
|
||||
@@ -60,9 +62,10 @@
|
||||
},
|
||||
},
|
||||
"packages/clawdhub": {
|
||||
"name": "clawdhub",
|
||||
"version": "0.3.0",
|
||||
"name": "clawhub",
|
||||
"version": "0.4.0",
|
||||
"bin": {
|
||||
"clawhub": "bin/clawdhub.js",
|
||||
"clawdhub": "bin/clawdhub.js",
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -84,7 +87,7 @@
|
||||
},
|
||||
},
|
||||
"packages/schema": {
|
||||
"name": "clawdhub-schema",
|
||||
"name": "clawhub-schema",
|
||||
"version": "0.0.2",
|
||||
"dependencies": {
|
||||
"arktype": "^2.1.29",
|
||||
@@ -95,7 +98,7 @@
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@acemir/cssom": ["@acemir/cssom@0.9.30", "", {}, "sha512-9CnlMCI0LmCIq0olalQqdWrJHPzm0/tw3gzOA9zJSgvFX7Xau3D24mAGa4BtwxwY69nsuJW6kQqqCzf/mEcQgg=="],
|
||||
"@acemir/cssom": ["@acemir/cssom@0.9.31", "", {}, "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA=="],
|
||||
|
||||
"@ark/schema": ["@ark/schema@0.56.0", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA=="],
|
||||
|
||||
@@ -109,23 +112,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.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/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/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="],
|
||||
"@babel/compat-data": ["@babel/compat-data@7.28.6", "", {}, "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg=="],
|
||||
|
||||
"@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/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/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/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/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-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-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
||||
|
||||
"@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-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-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-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-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
|
||||
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
|
||||
@@ -133,45 +136,45 @@
|
||||
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||
|
||||
"@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
|
||||
"@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/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||
"@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="],
|
||||
|
||||
"@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-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-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-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-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.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
|
||||
"@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
|
||||
|
||||
"@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/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/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/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/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=="],
|
||||
"@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=="],
|
||||
|
||||
"@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="],
|
||||
|
||||
"@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/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/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA=="],
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0OCwP0/BoKzyJHnFdaTk/i7hIP9JHH9oJJq6hrSCPmJPo8JWcJhprK4gQlhFzrwdTBAW4Bjt/RmCf3ZZe59gwQ=="],
|
||||
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg=="],
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-AGr8OoemT/ejynbIu56qeil2+F2WLkIjn2d8jGK1JkchxnMUhYOfnqc9sVzcRxpG9Ycvw4weQ5sprRvtb7Yhcw=="],
|
||||
|
||||
"@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": ["@biomejs/cli-linux-arm64@2.3.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-xvOiFkrDNu607MPMBUQ6huHmBG1PZLOrqhtK6pXJW3GjfVqJg0Z/qpTdhXfcqWdSZHcT+Nct2fOgewZvytESkw=="],
|
||||
|
||||
"@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-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-TUdDCSY+Eo/EHjhJz7P2GnWwfqet+lFxBZzGHldrvULr59AgahamLs/N85SC4+bdF86EhqDuuw9rYLvLFWWlXA=="],
|
||||
|
||||
"@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": ["@biomejs/cli-linux-x64@2.3.13", "", { "os": "linux", "cpu": "x64" }, "sha512-s+YsZlgiXNq8XkgHs6xdvKDFOj/bwTEevqEY6rC2I3cBHbxXYU1LOZstH3Ffw9hE5tE1sqT7U23C00MzkXztMw=="],
|
||||
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw=="],
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.13", "", { "os": "linux", "cpu": "x64" }, "sha512-0bdwFVSbbM//Sds6OjtnmQGp4eUjOTt6kHvR/1P0ieR9GcTUAlPNvPC3DiavTqq302W34Ae2T6u5VVNGuQtGlQ=="],
|
||||
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw=="],
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-QweDxY89fq0VvrxME+wS/BXKmqMrOTZlN9SqQ79kQSIc3FrEwvW/PvUegQF6XIVaekncDykB5dzPqjbwSKs9DA=="],
|
||||
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.11", "", { "os": "win32", "cpu": "x64" }, "sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg=="],
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.13", "", { "os": "win32", "cpu": "x64" }, "sha512-trDw2ogdM2lyav9WFQsdsfdVy1dvZALymRpgmWsvSez0BJzBjulhOT/t+wyKeh3pZWvwP3VMs1SoOKwO3wecMQ=="],
|
||||
|
||||
"@clack/core": ["@clack/core@0.5.0", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow=="],
|
||||
|
||||
@@ -187,13 +190,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.22", "", {}, "sha512-qBcx6zYlhleiFfdtzkRgwNC7VVoAwfK76Vmsw5t+PbvtdknO9StgRk7ROvq9so1iqbdW4uLIDAsXRsTfUrIoOw=="],
|
||||
"@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.0.26", "", {}, "sha512-6boXK0KkzT5u5xOgF6TKB+CLq9SOpEGmkZw0g5n9/7yg85wab3UzSxB8TxhLJ31L4SGJ6BCFRw/iftTha1CJXA=="],
|
||||
|
||||
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="],
|
||||
|
||||
"@emnapi/core": ["@emnapi/core@1.8.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-ryJnSmj4UhrGLZZPJ6PKVb4wNPAIkW6iyLy+0TRwazd3L1u0wzMe8RfqevAh2HbcSkoeLiSYnOVDOys4JSGYyg=="],
|
||||
"@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/runtime": ["@emnapi/runtime@1.8.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Z82FDl1ByxqPEPrAYYeTQVlx2FSHPe1qwX465c+96IRS3fTdSYRoJcRxg3g2fEG5I69z1dSEWQlNRRr0/677mg=="],
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||
|
||||
@@ -249,13 +252,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.8.0", "", { "peerDependencies": { "@exodus/crypto": "^1.0.0-rc.4" }, "optionalPeers": ["@exodus/crypto"] }, "sha512-8JPn18Bcp8Uo1T82gR8lh2guEOa5KKU/IEKvvdp0sgmi7coPBWf1Doi1EXsGZb2ehc8ym/StJCjffYV+ne7sXQ=="],
|
||||
"@exodus/bytes": ["@exodus/bytes@1.10.0", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-tf8YdcbirXdPnJ+Nd4UN1EXnz+IP2DI45YVEr3vvzcVTOyrApkmIB4zvOQVd3XPr7RXnfBtAx+PXImXOIU0Ajg=="],
|
||||
|
||||
"@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
|
||||
"@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="],
|
||||
|
||||
"@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/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/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/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/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
|
||||
|
||||
@@ -297,97 +300,117 @@
|
||||
|
||||
"@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="],
|
||||
|
||||
"@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-android-arm-eabi": ["@oxc-minify/binding-android-arm-eabi@0.110.0", "", { "os": "android", "cpu": "arm" }, "sha512-43fMTO8/5bMlqfOiNSZNKUzIqeLIYuB9Hr1Ohyf58B1wU11S2dPGibTXOGNaWsfgHy99eeZ1bSgeIHy/fEYqbw=="],
|
||||
|
||||
"@oxc-minify/binding-darwin-arm64": ["@oxc-minify/binding-darwin-arm64@0.96.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-i0LkJAUXb4BeBFrJQbMKQPoxf8+cFEffDyLSb7NEzzKuPcH8qrVsnEItoOzeAdYam8Sr6qCHVwmBNEQzl7PWpw=="],
|
||||
"@oxc-minify/binding-android-arm64": ["@oxc-minify/binding-android-arm64@0.110.0", "", { "os": "android", "cpu": "arm64" }, "sha512-5oQrnn9eK/ccOp80PTrNj0Vq893NPNNRryjGpOIVsYNgWFuoGCfpnKg68oEFcN8bArizYAqw4nvgHljEnar69w=="],
|
||||
|
||||
"@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-darwin-arm64": ["@oxc-minify/binding-darwin-arm64@0.110.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dqBDgTG9tF2z2lrZp9E8wU+Godz1i8gCGSei2eFKS2hRploBOD5dmOLp1j4IMornkPvSQmbwB3uSjPq7fjx4EA=="],
|
||||
|
||||
"@oxc-minify/binding-freebsd-x64": ["@oxc-minify/binding-freebsd-x64@0.96.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-3//5DNx+xUjVBMLLk2sl6hfe4fwfENJtjVQUBXjxzwPuv8xgZUqASG4cRG3WqG5Qe8dV6SbCI4EgKQFjO4KCZA=="],
|
||||
"@oxc-minify/binding-darwin-x64": ["@oxc-minify/binding-darwin-x64@0.110.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U0AqabqaooDOpYmeeOye8wClv8PSScELXgOfYqyqgrwH9J9KrpCE1jL8Rlqgz68QbL4mPw3V6sKiiHssI4CLeQ=="],
|
||||
|
||||
"@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-freebsd-x64": ["@oxc-minify/binding-freebsd-x64@0.110.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-H0w8o/Wo1072WSdLfhwwrpFpwZnPpjQODlHuRYkTfsSSSJbTxQtjJd4uxk7YJsRv5RQp69y0I7zvdH6f8Xueyw=="],
|
||||
|
||||
"@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-arm-gnueabihf": ["@oxc-minify/binding-linux-arm-gnueabihf@0.110.0", "", { "os": "linux", "cpu": "arm" }, "sha512-qd6sW0AvEVYZhbVVMGtmKZw3b1zDYGIW+54Uh42moWRAj6i4Jhk/LGr6r9YNZpOINeuvZfkFuEeDD/jbu7xPUA=="],
|
||||
|
||||
"@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-linux-arm-musleabihf": ["@oxc-minify/binding-linux-arm-musleabihf@0.110.0", "", { "os": "linux", "cpu": "arm" }, "sha512-7WXP0aXMrWSn0ScppUBi3jf68ebfBG0eri8kxLmBOVSBj6jw1repzkHMITJMBeLr5d0tT/51qFEptiAk2EP2iA=="],
|
||||
|
||||
"@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-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-riscv64-gnu": ["@oxc-minify/binding-linux-riscv64-gnu@0.96.0", "", { "os": "linux", "cpu": "none" }, "sha512-3paajIuzGnukHwSI3YBjYVqbd72pZd8NJxaayaNFR0AByIm8rmIT5RqFXbq8j2uhtpmNdZRXiu0em1zOmIScWA=="],
|
||||
"@oxc-minify/binding-linux-arm64-musl": ["@oxc-minify/binding-linux-arm64-musl@0.110.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-53GjCVY8kvymk9P6qNDh6zyblcehF5QHstq9QgCjv13ONGRnSHjeds0PxIwiihD7h295bxsWs84DN39syLPH4Q=="],
|
||||
|
||||
"@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-linux-ppc64-gnu": ["@oxc-minify/binding-linux-ppc64-gnu@0.110.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-li8XcN81dxbJDMBESnTgGhoiAQ+CNIdM0QGscZ4duVPjCry1RpX+5FJySFbGqG3pk4s9ZzlL/vtQtbRzZIZOzg=="],
|
||||
|
||||
"@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-linux-riscv64-gnu": ["@oxc-minify/binding-linux-riscv64-gnu@0.110.0", "", { "os": "linux", "cpu": "none" }, "sha512-SweKfsnLKShu6UFV8mwuj1d1wmlNoL/FlAxPUzwjEBgwiT2HQkY24KnjBH+TIA+//1O83kzmWKvvs4OuEhdIEQ=="],
|
||||
|
||||
"@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-minify/binding-linux-riscv64-musl": ["@oxc-minify/binding-linux-riscv64-musl@0.110.0", "", { "os": "linux", "cpu": "none" }, "sha512-oH8G4aFMP8XyTsEpdANC5PQyHgSeGlopHZuW1rpyYcaErg5YaK0vXjQ4EM5HVvPm+feBV24JjxgakTnZoF3aOQ=="],
|
||||
|
||||
"@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-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-win32-arm64-msvc": ["@oxc-minify/binding-win32-arm64-msvc@0.96.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-4L4DlHUT47qMWQuTyUghpncR3NZHWtxvd0G1KgSjVgXf+cXzFdWQCWZZtCU0yrmOoVCNUf4S04IFCJyAe+Ie7A=="],
|
||||
"@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-win32-x64-msvc": ["@oxc-minify/binding-win32-x64-msvc@0.96.0", "", { "os": "win32", "cpu": "x64" }, "sha512-T2ijfqZLpV2bgGGocXV4SXTuMoouqN0asYTIm+7jVOLvT5XgDogf3ZvCmiEnSWmxl21+r5wHcs8voU2iUROXAg=="],
|
||||
"@oxc-minify/binding-linux-x64-musl": ["@oxc-minify/binding-linux-x64-musl@0.110.0", "", { "os": "linux", "cpu": "x64" }, "sha512-QqzvALuOTtSckI8x467R4GNArzYDb/yEh6aNzLoeaY1O7vfT7SPDwlOEcchaTznutpeS9Dy8gUS/AfqtUHaufw=="],
|
||||
|
||||
"@oxc-transform/binding-android-arm64": ["@oxc-transform/binding-android-arm64@0.96.0", "", { "os": "android", "cpu": "arm64" }, "sha512-wOm+ZsqFvyZ7B9RefUMsj0zcXw77Z2pXA51nbSQyPXqr+g0/pDGxriZWP8Sdpz/e4AEaKPA9DvrwyOZxu7GRDQ=="],
|
||||
"@oxc-minify/binding-openharmony-arm64": ["@oxc-minify/binding-openharmony-arm64@0.110.0", "", { "os": "none", "cpu": "arm64" }, "sha512-gAMssLs2Q3+uhLZxanh1DF+27Kaug3cf4PXb9AB7XK81DR+LVcKySXaoGYoOs20Co0fFSphd6rRzKge2qDK3dA=="],
|
||||
|
||||
"@oxc-transform/binding-darwin-arm64": ["@oxc-transform/binding-darwin-arm64@0.96.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-td1sbcvzsyuoNRiNdIRodPXRtFFwxzPpC/6/yIUtRRhKn30XQcizxupIvQQVpJWWchxkphbBDh6UN+u+2CJ8Zw=="],
|
||||
"@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-transform/binding-darwin-x64": ["@oxc-transform/binding-darwin-x64@0.96.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-xgqxnqhPYH2NYkgbqtnCJfhbXvxIf/pnhF/ig5UBK8PYpCEWIP/cfLpQRQ9DcQnRfuxi7RMIF6LdmB1AiS6Fkg=="],
|
||||
"@oxc-minify/binding-win32-arm64-msvc": ["@oxc-minify/binding-win32-arm64-msvc@0.110.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-ZPx+0Tj4dqn41ecyoGotlvekQKy6JxJCixn9Rw7h/dafZ3eDuBcEVh3c2ZoldXXsyMIt5ywI8IWzFZsjNedd5Q=="],
|
||||
|
||||
"@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-minify/binding-win32-ia32-msvc": ["@oxc-minify/binding-win32-ia32-msvc@0.110.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-H0Oyd3RWBfpEyvJIrFK94RYiY7KKSQl11Ym7LMDwLEagelIAfRCkt1amHZhFa/S3ZRoaOJFXzEw4YKeSsjVFsg=="],
|
||||
|
||||
"@oxc-transform/binding-linux-arm-gnueabihf": ["@oxc-transform/binding-linux-arm-gnueabihf@0.96.0", "", { "os": "linux", "cpu": "arm" }, "sha512-9MJBs0SWODsqyzO3eAnacXgJ/sZu1xqinjEwBzkcZ3tQI8nKhMADOzu2NzbVWDWujeoC8DESXaO08tujvUru+Q=="],
|
||||
"@oxc-minify/binding-win32-x64-msvc": ["@oxc-minify/binding-win32-x64-msvc@0.110.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Hr3nK90+qXKJ2kepXwFIcNfQQIOBecB4FFCyaMMypthoEEhVP08heRynj4eSXZ8NL9hLjs3fQzH8PJXfpznRnQ=="],
|
||||
|
||||
"@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-android-arm-eabi": ["@oxc-transform/binding-android-arm-eabi@0.110.0", "", { "os": "android", "cpu": "arm" }, "sha512-sE9dxvqqAax1YYJ3t7j+h5ZSI9jl6dYuDfngl6ieZUrIy5P89/8JKVgAzgp8o3wQSo7ndpJvYsi1K4ZqrmbP7w=="],
|
||||
|
||||
"@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-android-arm64": ["@oxc-transform/binding-android-arm64@0.110.0", "", { "os": "android", "cpu": "arm64" }, "sha512-nqtbP4aMCtsCZ6qpHlHaQoWVHSBtlKzwaAgwEOvR+9DWqHjk31BHvpGiDXlMeed6CVNpl3lCbWgygb3RcSjcfw=="],
|
||||
|
||||
"@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-darwin-arm64": ["@oxc-transform/binding-darwin-arm64@0.110.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oeSeHnL4Z4cMXtc8V0/rwoVn0dgwlS9q0j6LcHn9dIhtFEdp3W0iSBF8YmMQA+E7sILeLDjsHmHE4Kp0sOScXw=="],
|
||||
|
||||
"@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-darwin-x64": ["@oxc-transform/binding-darwin-x64@0.110.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-nL9K5x7OuZydobAGPylsEW9d4APs2qEkIBLMgQPA+kY8dtVD3IR87QsTbs4l4DBQYyun/+ay6qVCDlxqxdX2Jg=="],
|
||||
|
||||
"@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-freebsd-x64": ["@oxc-transform/binding-freebsd-x64@0.110.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-GS29zXXirDQhZEUq8xKJ1azAWMuUy3Ih3W5Bc5ddk12LRthO5wRLFcKIyeHpAXCoXymQ+LmxbMtbPf84GPxouw=="],
|
||||
|
||||
"@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-arm-gnueabihf": ["@oxc-transform/binding-linux-arm-gnueabihf@0.110.0", "", { "os": "linux", "cpu": "arm" }, "sha512-glzDHak8ISyZJemCUi7RCvzNSl+MQ1ly9RceT2qRufhUsvNZ4C/2QLJ1HJwd2N6E88bO4laYn+RofdRzNnGGEA=="],
|
||||
|
||||
"@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-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-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-linux-arm64-gnu": ["@oxc-transform/binding-linux-arm64-gnu@0.110.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-IRh21Ub/g4bkHoErZ0AUWMlWfoZaS0A6EaOVtbcY70RSYIMlrsbjiFwJCzM+b/1DD1rXbH5tsGcH7GweTbfRqg=="],
|
||||
|
||||
"@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-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-win32-x64-msvc": ["@oxc-transform/binding-win32-x64-msvc@0.96.0", "", { "os": "win32", "cpu": "x64" }, "sha512-0fI0P0W7bSO/GCP/N5dkmtB9vBqCA4ggo1WmXTnxNJVmFFOtcA1vYm1I9jl8fxo+sucW2WnlpnI4fjKdo3JKxA=="],
|
||||
"@oxc-transform/binding-linux-ppc64-gnu": ["@oxc-transform/binding-linux-ppc64-gnu@0.110.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Y3/Tnnz1GvDpmv8FXBIKtdZPsdZklOEPdrL6NHrN5i2u54BOkybFaDSptgWF53wOrJlTrcmAVSE6fRKK9XCM2Q=="],
|
||||
|
||||
"@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/darwin-arm64@0.11.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UJIOFeJZpFTJIGS+bMdFXcvjslvnXBEouMvzynfQD7RTazcFIRLbokYgEbhrN2P6B352Ut1TUtvR0CLAp/9QfA=="],
|
||||
"@oxc-transform/binding-linux-riscv64-gnu": ["@oxc-transform/binding-linux-riscv64-gnu@0.110.0", "", { "os": "linux", "cpu": "none" }, "sha512-Y0E35iA9/v9jlkNcP6tMJ+ZFOS0rLsWDqG6rU9z+X2R3fBFJBO9UARIK6ngx8upxk81y1TFR2CmBFhupfYdH6Q=="],
|
||||
|
||||
"@oxlint-tsgolint/darwin-x64": ["@oxlint-tsgolint/darwin-x64@0.11.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-68O8YvexIm+ISZKl2vBFII1dMfLrteDyPcuCIecDuiBIj2tV0KYq13zpSCMz4dvJUWJW6RmOOGZKrkkvOAy6uQ=="],
|
||||
"@oxc-transform/binding-linux-riscv64-musl": ["@oxc-transform/binding-linux-riscv64-musl@0.110.0", "", { "os": "linux", "cpu": "none" }, "sha512-JOUSYFfHjBUs7xp2FHmZHb8eTYD/oEu0NklS6JgUauqnoXZHiTLPLVW2o2uVCqldnabYHcomuwI2iqVFYJNhTw=="],
|
||||
|
||||
"@oxlint-tsgolint/linux-arm64": ["@oxlint-tsgolint/linux-arm64@0.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-hXBInrFxPNbPPbPQYozo8YpSsFFYdtHBWRUiLMxul71vTy1CdSA7H5Qq2KbrKomr/ASmhvIDVAQZxh9hIJNHMA=="],
|
||||
"@oxc-transform/binding-linux-s390x-gnu": ["@oxc-transform/binding-linux-s390x-gnu@0.110.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-7blgoXF9D3Ngzb7eun23pNrHJpoV/TtE6LObwlZ3Nmb4oZ6Z+yMvBVaoW68NarbmvNGfZ95zrOjgm6cVETLYBA=="],
|
||||
|
||||
"@oxlint-tsgolint/linux-x64": ["@oxlint-tsgolint/linux-x64@0.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-aMaGctlwrJhaIQPOdVJR+AGHZGPm4D1pJ457l0SqZt4dLXAhuUt2ene6cUUGF+864R7bDyFVGZqbZHODYpENyA=="],
|
||||
"@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=="],
|
||||
|
||||
"@oxlint-tsgolint/win32-arm64": ["@oxlint-tsgolint/win32-arm64@0.11.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-ipOs6kKo8fz5n5LSHvcbyZFmEpEIsh2m7+B03RW3jGjBEPMiXb4PfKNuxnusFYTtJM9WaR3bCVm5UxeJTA8r3w=="],
|
||||
"@oxc-transform/binding-linux-x64-musl": ["@oxc-transform/binding-linux-x64-musl@0.110.0", "", { "os": "linux", "cpu": "x64" }, "sha512-fkjr5qE632ULmNgvFXWDR/8668WxERz3tU7TQFp6JebPBneColitjSkdx6VKNVXEoMmQnOvBIGeP5tUNT384oA=="],
|
||||
|
||||
"@oxlint-tsgolint/win32-x64": ["@oxlint-tsgolint/win32-x64@0.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-m2apsAXg6qU3ulQG45W/qshyEpOjoL+uaQyXJG5dBoDoa66XPtCaSkBlKltD0EwGu0aoB8lM4I5I3OzQ6raNhw=="],
|
||||
"@oxc-transform/binding-openharmony-arm64": ["@oxc-transform/binding-openharmony-arm64@0.110.0", "", { "os": "none", "cpu": "arm64" }, "sha512-HWH9Zj+lMrdSTqFRCZsvDWMz7OnMjbdGsm3xURXWfRZpuaz0bVvyuZNDQXc4FyyhRDsemICaJbU1bgeIpUJDGw=="],
|
||||
|
||||
"@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@1.39.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lT3hNhIa02xCujI6YGgjmYGg3Ht/X9ag5ipUVETaMpx5Rd4BbTNWUPif1WN1YZHxt3KLCIqaAe7zVhatv83HOQ=="],
|
||||
"@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=="],
|
||||
|
||||
"@oxlint/darwin-x64": ["@oxlint/darwin-x64@1.39.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-UT+rfTWd+Yr7iJeSLd/7nF8X4gTYssKh+n77hxl6Oilp3NnG1CKRHxZDy3o3lIBnwgzJkdyUAiYWO1bTMXQ1lA=="],
|
||||
"@oxc-transform/binding-win32-arm64-msvc": ["@oxc-transform/binding-win32-arm64-msvc@0.110.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-9VTwpXCZs7xkV+mKhQ62dVk7KLnLXtEUxNS2T4nLz3iMl1IJbA4h5oltK0JoobtiUAnbkV53QmMVGW8+Nh3bDQ=="],
|
||||
|
||||
"@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@1.39.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qocBkvS2V6rH0t9AT3DfQunMnj3xkM7srs5/Ycj2j5ZqMoaWd/FxHNVJDFP++35roKSvsRJoS0mtA8/77jqm6Q=="],
|
||||
"@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/linux-arm64-musl": ["@oxlint/linux-arm64-musl@1.39.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-arZzAc1PPcz9epvGBBCMHICeyQloKtHX3eoOe62B3Dskn7gf6Q14wnDHr1r9Vp4vtcBATNq6HlKV14smdlC/qA=="],
|
||||
"@oxc-transform/binding-win32-x64-msvc": ["@oxc-transform/binding-win32-x64-msvc@0.110.0", "", { "os": "win32", "cpu": "x64" }, "sha512-QROrowwlrApI1fEScMknGWKM6GTM/Z2xwMnDqvSaEmzNazBsDUlE08Jasw610hFEsYAVU2K5sp/YaCa9ORdP4A=="],
|
||||
|
||||
"@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@1.39.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZVt5qsECpuNprdWxAPpDBwoixr1VTcZ4qAEQA2l/wmFyVPDYFD3oBY/SWACNnWBddMrswjTg9O8ALxYWoEpmXw=="],
|
||||
"@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/darwin-arm64@0.11.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-FU4e+w09D+2rkCVdL7I7zMuQOJ2tuapVhBGPGY66VAct2FUwFDVmgU+rNJ2hHIdc9uHg24v+FD8PcfFYpask8Q=="],
|
||||
|
||||
"@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@1.39.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pB0hlGyKPbxr9NMIV783lD6cWL3MpaqnZRM9MWni4yBdHPTKyFNYdg5hGD0Bwg+UP4S2rOevq/+OO9x9Bi7E6g=="],
|
||||
"@oxlint-tsgolint/darwin-x64": ["@oxlint-tsgolint/darwin-x64@0.11.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-7sm1d920HfFsC3hIP7SJVm11WhYufA8qnLQQVk7odTpSzVUAT1jtG8LdfFigzgb38zHszQbsqJ7OjAgIW/OgmA=="],
|
||||
|
||||
"@oxlint/win32-arm64": ["@oxlint/win32-arm64@1.39.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gg2SFaJohI9+tIQVKXlPw3FsPQFi/eCSWiCgwPtPn5uzQxHRTeQEZKuluz1fuzR5U70TXubb2liZi4Dgl8LJQA=="],
|
||||
"@oxlint-tsgolint/linux-arm64": ["@oxlint-tsgolint/linux-arm64@0.11.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-eoJfdmHcpG9k8fufb8yL3rC3HC6QELoTEfs56lmGaRIHHmd1aj4MWDbGCqdRqPEp7oC5fVvFxi7wDkA1MDf99Q=="],
|
||||
|
||||
"@oxlint/win32-x64": ["@oxlint/win32-x64@1.39.0", "", { "os": "win32", "cpu": "x64" }, "sha512-sbi25lfj74hH+6qQtb7s1wEvd1j8OQbTaH8v3xTcDjrwm579Cyh0HBv1YSZ2+gsnVwfVDiCTL1D0JsNqYXszVA=="],
|
||||
"@oxlint-tsgolint/linux-x64": ["@oxlint-tsgolint/linux-x64@0.11.3", "", { "os": "linux", "cpu": "x64" }, "sha512-t7jGK0vBApuAGvOnCPTxsdX+1e9nMdvqU3zHCJWQ7yUDaJxki0bCy4zbKfUgVo8ePeVRgIKWwqLFBOVTXQ5AMQ=="],
|
||||
|
||||
"@oxlint-tsgolint/win32-arm64": ["@oxlint-tsgolint/win32-arm64@0.11.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-6ellG0zcWnj2b6Mr7fl19x+nlFIWGWoKCBlYnqNZ4CaziRYGpYx7PLwHhPJq331w7zzRRSnYqhyTrVluYjZADQ=="],
|
||||
|
||||
"@oxlint-tsgolint/win32-x64": ["@oxlint-tsgolint/win32-x64@0.11.3", "", { "os": "win32", "cpu": "x64" }, "sha512-rzvfaRJPK9eRYVWMXCt8JtvOsVFAsqScgsFhnXzsipU6W1Te0g+b4q068o7hZ3NRTjJxNgFJj8ayOkZ6NbX0tA=="],
|
||||
|
||||
"@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@1.42.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ui5CdAcDsXPQwZQEXOOSWsilJWhgj9jqHCvYBm2tDE8zfwZZuF9q58+hGKH1x5y0SV4sRlyobB2Quq6uU6EgeA=="],
|
||||
|
||||
"@oxlint/darwin-x64": ["@oxlint/darwin-x64@1.42.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-wo0M/hcpHRv7vFje99zHHqheOhVEwUOKjOgBKyi0M99xcLizv04kcSm1rTd6HSCeZgOtiJYZRVAlKhQOQw2byQ=="],
|
||||
|
||||
"@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@1.42.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j4QzfCM8ks+OyM+KKYWDiBEQsm5RCW50H1Wz16wUyoFsobJ+X5qqcJxq6HvkE07m8euYmZelyB0WqsiDoz1v8g=="],
|
||||
|
||||
"@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@1.42.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-g5b1Uw7zo6yw4Ymzyd1etKzAY7xAaGA3scwB8tAp3QzuY7CYdfTwlhiLKSAKbd7T/JBgxOXAGNcLDorJyVTXcg=="],
|
||||
|
||||
"@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@1.42.0", "", { "os": "linux", "cpu": "x64" }, "sha512-HnD99GD9qAbpV4q9iQil7mXZUJFpoBdDavfcC2CgGLPlawfcV5COzQPNwOgvPVkr7C0cBx6uNCq3S6r9IIiEIg=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@panva/hkdf": ["@panva/hkdf@1.2.1", "", {}, "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw=="],
|
||||
|
||||
"@playwright/test": ["@playwright/test@1.57.0", "", { "dependencies": { "playwright": "1.57.0" }, "bin": { "playwright": "cli.js" } }, "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA=="],
|
||||
"@playwright/test": ["@playwright/test@1.58.0", "", { "dependencies": { "playwright": "1.58.0" }, "bin": { "playwright": "cli.js" } }, "sha512-fWza+Lpbj6SkQKCrU6si4iu+fD2dD3gxNHFhUPxsfXBPhnv3rRSQVd0NtBUT9Z/RhF/boCBcuUaMUSTRTopjZg=="],
|
||||
|
||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
||||
|
||||
@@ -449,49 +472,55 @@
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.54.0", "", { "os": "android", "cpu": "arm" }, "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng=="],
|
||||
"@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.54.0", "", { "os": "android", "cpu": "arm64" }, "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw=="],
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.0", "", { "os": "android", "cpu": "arm64" }, "sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.54.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw=="],
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.54.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A=="],
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.54.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA=="],
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.54.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ=="],
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.54.0", "", { "os": "linux", "cpu": "arm" }, "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ=="],
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.0", "", { "os": "linux", "cpu": "arm" }, "sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q=="],
|
||||
|
||||
"@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-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA=="],
|
||||
|
||||
"@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-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.54.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg=="],
|
||||
"@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-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw=="],
|
||||
"@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-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.54.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA=="],
|
||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.0", "", { "os": "linux", "cpu": "none" }, "sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ=="],
|
||||
|
||||
"@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-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A=="],
|
||||
"@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-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.54.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ=="],
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.0", "", { "os": "linux", "cpu": "none" }, "sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ=="],
|
||||
|
||||
"@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-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.0", "", { "os": "linux", "cpu": "none" }, "sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.54.0", "", { "os": "linux", "cpu": "x64" }, "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw=="],
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.54.0", "", { "os": "none", "cpu": "arm64" }, "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg=="],
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.0", "", { "os": "linux", "cpu": "x64" }, "sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.54.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw=="],
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.0", "", { "os": "linux", "cpu": "x64" }, "sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.54.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ=="],
|
||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.54.0", "", { "os": "win32", "cpu": "x64" }, "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ=="],
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.0", "", { "os": "none", "cpu": "arm64" }, "sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.54.0", "", { "os": "win32", "cpu": "x64" }, "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg=="],
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
@@ -537,7 +566,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.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": ["@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-client": ["@tanstack/devtools-client@0.0.5", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.0" } }, "sha512-hsNDE3iu4frt9cC2ppn1mNRnLKo2uc1/1hXAyY9z4UYb+o40M2clFAhiFoo4HngjfGJDV3x18KVVIq7W4Un+zA=="],
|
||||
|
||||
@@ -549,49 +578,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.151.1", "", {}, "sha512-Z/eymNBuUGHYIea7nNX3xR5feqx418ChlwWOKklVpCVzEQ5Q3kNTUw+WK4HYUKxF+1uXFN01Dbuhhl7SmW1LJA=="],
|
||||
"@tanstack/history": ["@tanstack/history@1.154.14", "", {}, "sha512-xyIfof8eHBuub1CkBnbKNKQXeRZC4dClhmzePHVOEel4G7lk/dW+TQ16da7CFdeNLv6u6Owf5VoBQxoo6DFTSA=="],
|
||||
|
||||
"@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-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-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": ["@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-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-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-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": ["@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-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-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-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-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-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.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-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-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-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-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-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-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-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-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/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/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-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-fn-stubs": ["@tanstack/start-fn-stubs@1.151.3", "", {}, "sha512-/zWBnfsOwact936Bn0CxigudU1QRZdiNTsK7ME/LMXXA66XsDxkryX5+5FeGwU5ETNPfLAx6pRUet1mtUKnLCg=="],
|
||||
"@tanstack/start-fn-stubs": ["@tanstack/start-fn-stubs@1.154.7", "", {}, "sha512-D69B78L6pcFN5X5PHaydv7CScQcKLzJeEYqs7jpuyyqGQHSUIZUjS955j+Sir8cHhuDIovCe2LmsYHeZfWf3dQ=="],
|
||||
|
||||
"@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-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-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-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-storage-context": ["@tanstack/start-storage-context@1.151.6", "", { "dependencies": { "@tanstack/router-core": "1.151.6" } }, "sha512-MvTcT40qnqatIpKjWSfMRxFzTkprGBxhX2c+em58iZLEsGksitMUWbprknD6AIUqjHty8V3LuhULks/o6tSugQ=="],
|
||||
"@tanstack/start-storage-context": ["@tanstack/start-storage-context@1.157.16", "", { "dependencies": { "@tanstack/router-core": "1.157.16" } }, "sha512-56izE0oihAw2YRwYUEds2H+uO5dyT2CahXCgWX62+l+FHou09M9mSep68n1lBKPdphC2ZU3cPV7wnvgeraJWHg=="],
|
||||
|
||||
"@tanstack/store": ["@tanstack/store@0.8.0", "", {}, "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ=="],
|
||||
|
||||
"@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.145.4", "", {}, "sha512-CI75JrfqSluhdGwLssgVeQBaCphgfkMQpi8MCY3UJX1hoGzXa8kHYJcUuIFMOLs1q7zqHy++EVVtMK03osR5wQ=="],
|
||||
"@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.154.7", "", {}, "sha512-cHHDnewHozgjpI+MIVp9tcib6lYEQK5MyUr0ChHpHFGBl8Xei55rohFK0I0ve/GKoHeioaK42Smd8OixPp6CTg=="],
|
||||
|
||||
"@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.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=="],
|
||||
"@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=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
@@ -621,9 +650,9 @@
|
||||
|
||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||
|
||||
"@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="],
|
||||
"@types/node": ["@types/node@25.1.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.8", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg=="],
|
||||
"@types/react": ["@types/react@19.2.10", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw=="],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||
|
||||
@@ -639,21 +668,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.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/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/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/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/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/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/pretty-format": ["@vitest/pretty-format@4.0.17", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-Ah3VAYmjcEdHg6+MwFE17qyLqBHZ+ni2ScKCiW2XrlSBV4H3Z7vYfPfz7CWQ33gyu76oc0Ai36+kgLU3rfF4nw=="],
|
||||
"@vitest/pretty-format": ["@vitest/pretty-format@4.0.18", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw=="],
|
||||
|
||||
"@vitest/runner": ["@vitest/runner@4.0.17", "", { "dependencies": { "@vitest/utils": "4.0.17", "pathe": "^2.0.3" } }, "sha512-JmuQyf8aMWoo/LmNFppdpkfRVHJcsgzkbCA+/Bk7VfNH7RE6Ut2qxegeyx2j3ojtJtKIbIGy3h+KxGfYfk28YQ=="],
|
||||
"@vitest/runner": ["@vitest/runner@4.0.18", "", { "dependencies": { "@vitest/utils": "4.0.18", "pathe": "^2.0.3" } }, "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw=="],
|
||||
|
||||
"@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/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/spy": ["@vitest/spy@4.0.17", "", {}, "sha512-I1bQo8QaP6tZlTomQNWKJE6ym4SHf3oLS7ceNjozxxgzavRAgZDc06T7kD8gb9bXKEgcLNt00Z+kZO6KaJ62Ew=="],
|
||||
"@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="],
|
||||
|
||||
"@vitest/utils": ["@vitest/utils@4.0.17", "", { "dependencies": { "@vitest/pretty-format": "4.0.17", "tinyrainbow": "^3.0.3" } }, "sha512-RG6iy+IzQpa9SB8HAFHJ9Y+pTzI+h8553MrciN9eC6TFBErqrQaTas4vG+MVj8S4uKk8uTT2p0vgZPnTdxd96w=="],
|
||||
"@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
@@ -683,11 +712,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.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=="],
|
||||
"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=="],
|
||||
|
||||
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="],
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="],
|
||||
|
||||
"bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
|
||||
|
||||
@@ -699,7 +728,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.30001762", "", {}, "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw=="],
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001766", "", {}, "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA=="],
|
||||
|
||||
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
||||
|
||||
@@ -715,19 +744,19 @@
|
||||
|
||||
"character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
|
||||
|
||||
"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": ["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-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=="],
|
||||
|
||||
"clawdhub": ["clawdhub@workspace:packages/clawdhub"],
|
||||
"clawhub": ["clawhub@workspace:packages/clawdhub"],
|
||||
|
||||
"clawdhub-schema": ["clawdhub-schema@workspace:packages/schema"],
|
||||
"clawhub-schema": ["clawhub-schema@workspace:packages/schema"],
|
||||
|
||||
"cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
|
||||
|
||||
"cli-spinners": ["cli-spinners@3.3.0", "", {}, "sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ=="],
|
||||
"cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
@@ -739,13 +768,15 @@
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"convex": ["convex@1.31.5", "", { "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-E1IuJKFwMCHDToNGukBPs6c7RFaarR3t8chLF9n98TM5/Tgmj8lM6l7sKM1aJ3VwqGaB4wbeUAPY8osbCOXBhQ=="],
|
||||
"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-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=="],
|
||||
|
||||
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||
|
||||
"cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="],
|
||||
|
||||
"crossws": ["crossws@0.4.1", "", { "peerDependencies": { "srvx": ">=0.7.1" }, "optionalPeers": ["srvx"] }, "sha512-E7WKBcHVhAVrY6JYD5kteNqVq1GSZxqGrdSiwXR9at+XHi43HJoCQKXcCczR5LBnBquFZPsB3o7HklulKoBU5w=="],
|
||||
"crossws": ["crossws@0.4.4", "", { "peerDependencies": { "srvx": ">=0.7.1" }, "optionalPeers": ["srvx"] }, "sha512-w6c4OdpRNnudVmcgr7brb/+/HmYjMQvYToO/oTrprTwxRUiom3LYWU1PMWuD006okbUWpII1Ea9/+kwpUfmyRg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -753,11 +784,11 @@
|
||||
|
||||
"css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="],
|
||||
|
||||
"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=="],
|
||||
"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=="],
|
||||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"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=="],
|
||||
"data-urls": ["data-urls@6.0.1", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^15.1.0" } }, "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -765,7 +796,7 @@
|
||||
|
||||
"decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
|
||||
|
||||
"decode-named-character-reference": ["decode-named-character-reference@1.2.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q=="],
|
||||
"decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
|
||||
|
||||
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||
|
||||
@@ -775,7 +806,7 @@
|
||||
|
||||
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
||||
|
||||
"diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="],
|
||||
"diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
|
||||
|
||||
"dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
|
||||
|
||||
@@ -789,7 +820,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.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="],
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.282", "", {}, "sha512-FCPkJtpst28UmFzd903iU7PdeVTfY0KAeJy+Lk0GLZRwgwYHn/irRcaCbQQOmr5Vytc/7rcavsYLvTM8RiHYhQ=="],
|
||||
|
||||
"encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="],
|
||||
|
||||
@@ -843,7 +874,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.7", "", { "dependencies": { "rou3": "^0.7.12", "srvx": "^0.10.0" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-qbrRu1OLXmUYnysWOCVrYhtC/m8ZuXu/zCbo3U/KyphJxbPFiC76jHYwVrmEcss9uNAHO5BoUguQ46yEpgI2PA=="],
|
||||
"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=="],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
@@ -857,7 +888,7 @@
|
||||
|
||||
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
|
||||
|
||||
"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=="],
|
||||
"htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="],
|
||||
|
||||
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
|
||||
|
||||
@@ -895,7 +926,7 @@
|
||||
|
||||
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
|
||||
|
||||
"isbot": ["isbot@5.1.32", "", {}, "sha512-VNfjM73zz2IBZmdShMfAUg10prm6t7HFUQmNAEOAVS4YH92ZrZcvkMcGX6cIgBJAzWDzPent/EeAtYEHNPNPBQ=="],
|
||||
"isbot": ["isbot@5.1.34", "", {}, "sha512-aCMIBSKd/XPRYdiCQTLC8QHH4YT8B3JUADu+7COgYIZPvkeoMcUHMRjZLM9/7V8fCj+l7FSREc1lOPNjzogo/A=="],
|
||||
|
||||
"istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="],
|
||||
|
||||
@@ -949,7 +980,7 @@
|
||||
|
||||
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
||||
|
||||
"lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="],
|
||||
"lru-cache": ["lru-cache@11.2.5", "", {}, "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw=="],
|
||||
|
||||
"lucia": ["lucia@3.2.2", "", { "dependencies": { "@oslojs/crypto": "^1.0.1", "@oslojs/encoding": "^1.1.0" } }, "sha512-P1FlFBGCMPMXu+EGdVD9W4Mjm0DqsusmKgO7Xc33mI5X1bklmsQb0hfzPhXomQr9waWIBDsiOjvr1e6BTaUqpA=="],
|
||||
|
||||
@@ -1065,9 +1096,9 @@
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"nf3": ["nf3@0.1.12", "", {}, "sha512-qbMXT7RTGh74MYWPeqTIED8nDW70NXOULVHpdWcdZ7IVHVnAsMV9fNugSNnvooipDc1FMOzpis7T9nXJEbJhvQ=="],
|
||||
"nf3": ["nf3@0.3.7", "", {}, "sha512-wL73kyZbBoeTWlvQWQ0gQDZnqp+aNlUN5YIqsc3fv5V/06LAlwrwt+G7TpugFLJIai0AhrmnKJ2kgW0xprj+yQ=="],
|
||||
|
||||
"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=="],
|
||||
"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=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
|
||||
|
||||
@@ -1085,15 +1116,17 @@
|
||||
|
||||
"onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
||||
|
||||
"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=="],
|
||||
"only-allow": ["only-allow@1.2.2", "", { "dependencies": { "which-pm-runs": "1.1.0" }, "bin": { "only-allow": "bin.js" } }, "sha512-uxyNYDsCh5YIJ780G7hC5OHjVUr9reHsbZNMM80L9tZlTpb3hUzb36KXgW4ZUGtJKQnGA3xegmWg1BxhWV0jJA=="],
|
||||
|
||||
"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=="],
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
"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-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=="],
|
||||
|
||||
"p-retry": ["p-retry@7.1.1", "", { "dependencies": { "is-network-error": "^1.1.0" } }, "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w=="],
|
||||
|
||||
@@ -1113,9 +1146,9 @@
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
||||
"playwright": ["playwright@1.57.0", "", { "dependencies": { "playwright-core": "1.57.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw=="],
|
||||
"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-core": ["playwright-core@1.57.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ=="],
|
||||
"playwright-core": ["playwright-core@1.58.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -1123,7 +1156,7 @@
|
||||
|
||||
"preact-render-to-string": ["preact-render-to-string@6.5.11", "", { "peerDependencies": { "preact": ">=10" } }, "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw=="],
|
||||
|
||||
"prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="],
|
||||
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -1131,9 +1164,9 @@
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="],
|
||||
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
|
||||
|
||||
"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-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
|
||||
|
||||
"react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
|
||||
|
||||
@@ -1165,7 +1198,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.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=="],
|
||||
"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=="],
|
||||
|
||||
"rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
|
||||
|
||||
@@ -1177,9 +1210,9 @@
|
||||
|
||||
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"seroval": ["seroval@1.4.2", "", {}, "sha512-N3HEHRCZYn3cQbsC4B5ldj9j+tHdf4JZoYPlcI4rRYu0Xy4qN8MQf1Z08EibzB0WpgRG5BGK08FTrmM66eSzKQ=="],
|
||||
"seroval": ["seroval@1.5.0", "", {}, "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw=="],
|
||||
|
||||
"seroval-plugins": ["seroval-plugins@1.4.2", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-X7p4MEDTi+60o2sXZ4bnDBhgsUYDSkQEvzYZuJyFqWg9jcoPsHts5nrg5O956py2wyt28lUrBxk0M0/wU8URpA=="],
|
||||
"seroval-plugins": ["seroval-plugins@1.5.0", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA=="],
|
||||
|
||||
"server-only": ["server-only@0.0.1", "", {}, "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA=="],
|
||||
|
||||
@@ -1191,7 +1224,7 @@
|
||||
|
||||
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
||||
|
||||
"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=="],
|
||||
"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=="],
|
||||
|
||||
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
||||
|
||||
@@ -1199,7 +1232,7 @@
|
||||
|
||||
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
||||
|
||||
"srvx": ["srvx@0.10.0", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-NqIsR+wQCfkvvwczBh8J8uM4wTZx41K2lLSEp/3oMp917ODVVMtW5Me4epCmQ3gH8D+0b+/t4xxkUKutyhimTA=="],
|
||||
"srvx": ["srvx@0.10.1", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-A//xtfak4eESMWWydSRFUVvCTQbSwivnGCEf8YGPe2eHU0+Z6znfUTCPF0a7oV3sObSOcrXHlL6Bs9vVctfXdg=="],
|
||||
|
||||
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
||||
|
||||
@@ -1209,7 +1242,7 @@
|
||||
|
||||
"stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="],
|
||||
|
||||
"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=="],
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -1263,9 +1296,9 @@
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="],
|
||||
"ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
|
||||
|
||||
"undici": ["undici@7.16.0", "", {}, "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g=="],
|
||||
"undici": ["undici@7.19.2", "", {}, "sha512-4VQSpGEGsWzk0VYxyB/wVX/Q7qf9t5znLRgs0dzszr9w9Fej/8RVNQ+S20vdXSAyra/bJ7ZQfGv6ZMj7UEbzSg=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
@@ -1279,13 +1312,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.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": ["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-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.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=="],
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -1301,11 +1334,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.4", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3", "vite": "*" } }, "sha512-iIsEJ+ek5KqRTK17pmxtgIxXtqr3qDdE6OxrP9mVeGhVDNXRJTKN/l9oMbujTQNzMLe6XZ8qmpztfbkPu2TiFQ=="],
|
||||
"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=="],
|
||||
|
||||
"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.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=="],
|
||||
"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=="],
|
||||
|
||||
"w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
|
||||
|
||||
@@ -1319,9 +1352,11 @@
|
||||
|
||||
"whatwg-url": ["whatwg-url@15.1.0", "", { "dependencies": { "tr46": "^6.0.0", "webidl-conversions": "^8.0.0" } }, "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g=="],
|
||||
|
||||
"which-pm-runs": ["which-pm-runs@1.1.0", "", {}, "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA=="],
|
||||
|
||||
"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.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||
"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=="],
|
||||
|
||||
"xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="],
|
||||
|
||||
@@ -1345,9 +1380,9 @@
|
||||
|
||||
"@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.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-ryJnSmj4UhrGLZZPJ6PKVb4wNPAIkW6iyLy+0TRwazd3L1u0wzMe8RfqevAh2HbcSkoeLiSYnOVDOys4JSGYyg=="],
|
||||
"@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=="],
|
||||
|
||||
"@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/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
@@ -1357,6 +1392,8 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -1367,11 +1404,13 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
"htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||
|
||||
"nitro/srvx": ["srvx@0.9.8", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-RZaxTKJEE/14HYn8COLuUOJAt0U55N9l1Xf6jj+T0GoA01EUH1Xz5JtSUOI+EHn+AEgPCVn7gk6jHJffrr06fQ=="],
|
||||
"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=="],
|
||||
|
||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
@@ -1385,10 +1424,6 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bun
|
||||
import { existsSync } from 'node:fs'
|
||||
import { stat } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const distCliUrl = new URL('./packages/clawdhub/dist/cli.js', import.meta.url)
|
||||
const distCliPath = fileURLToPath(distCliUrl)
|
||||
const srcRootPath = fileURLToPath(new URL('./packages/clawdhub/src/', import.meta.url))
|
||||
|
||||
const shouldBuild = await (async () => {
|
||||
if (!existsSync(distCliPath)) return true
|
||||
try {
|
||||
const dist = await stat(distCliPath)
|
||||
const latestSrcMtime = await getLatestMtime(srcRootPath)
|
||||
return latestSrcMtime > dist.mtimeMs
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
})()
|
||||
|
||||
if (shouldBuild) {
|
||||
const proc = Bun.spawn(['bunx', 'tsc', '-p', 'packages/clawdhub/tsconfig.json'], {
|
||||
stdin: 'inherit',
|
||||
stdout: 'inherit',
|
||||
stderr: 'inherit',
|
||||
})
|
||||
const code = await proc.exited
|
||||
if (code !== 0) process.exit(code)
|
||||
}
|
||||
|
||||
await import(distCliUrl.href)
|
||||
|
||||
async function getLatestMtime(root: string) {
|
||||
let latest = 0
|
||||
const glob = new Bun.Glob('**/*.ts')
|
||||
for await (const rel of glob.scan({ cwd: root, onlyFiles: true })) {
|
||||
const path = `${root}${root.endsWith('/') ? '' : '/'}${rel}`
|
||||
try {
|
||||
const entry = await stat(path)
|
||||
latest = Math.max(latest, entry.mtimeMs)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return latest
|
||||
}
|
||||
Vendored
+10
@@ -12,6 +12,7 @@ import type * as auth from "../auth.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 githubBackups from "../githubBackups.js";
|
||||
import type * as githubBackupsNode from "../githubBackupsNode.js";
|
||||
@@ -24,12 +25,15 @@ import type * as httpApiV1 from "../httpApiV1.js";
|
||||
import type * as leaderboards from "../leaderboards.js";
|
||||
import type * as lib_access from "../lib/access.js";
|
||||
import type * as lib_apiTokenAuth from "../lib/apiTokenAuth.js";
|
||||
import type * as lib_badges from "../lib/badges.js";
|
||||
import type * as lib_changelog from "../lib/changelog.js";
|
||||
import type * as lib_embeddings from "../lib/embeddings.js";
|
||||
import type * as lib_githubBackup from "../lib/githubBackup.js";
|
||||
import type * as lib_githubImport from "../lib/githubImport.js";
|
||||
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_searchText from "../lib/searchText.js";
|
||||
import type * as lib_skillBackfill from "../lib/skillBackfill.js";
|
||||
import type * as lib_skillPublish from "../lib/skillPublish.js";
|
||||
@@ -44,6 +48,7 @@ import type * as rateLimits from "../rateLimits.js";
|
||||
import type * as search from "../search.js";
|
||||
import type * as seed from "../seed.js";
|
||||
import type * as seedSouls from "../seedSouls.js";
|
||||
import type * as skillStatEvents from "../skillStatEvents.js";
|
||||
import type * as skills from "../skills.js";
|
||||
import type * as soulComments from "../soulComments.js";
|
||||
import type * as soulDownloads from "../soulDownloads.js";
|
||||
@@ -68,6 +73,7 @@ declare const fullApi: ApiFromModules<{
|
||||
comments: typeof comments;
|
||||
crons: typeof crons;
|
||||
devSeed: typeof devSeed;
|
||||
devSeedExtra: typeof devSeedExtra;
|
||||
downloads: typeof downloads;
|
||||
githubBackups: typeof githubBackups;
|
||||
githubBackupsNode: typeof githubBackupsNode;
|
||||
@@ -80,12 +86,15 @@ declare const fullApi: ApiFromModules<{
|
||||
leaderboards: typeof leaderboards;
|
||||
"lib/access": typeof lib_access;
|
||||
"lib/apiTokenAuth": typeof lib_apiTokenAuth;
|
||||
"lib/badges": typeof lib_badges;
|
||||
"lib/changelog": typeof lib_changelog;
|
||||
"lib/embeddings": typeof lib_embeddings;
|
||||
"lib/githubBackup": typeof lib_githubBackup;
|
||||
"lib/githubImport": typeof lib_githubImport;
|
||||
"lib/githubSoulBackup": typeof lib_githubSoulBackup;
|
||||
"lib/leaderboards": typeof lib_leaderboards;
|
||||
"lib/moderation": typeof lib_moderation;
|
||||
"lib/public": typeof lib_public;
|
||||
"lib/searchText": typeof lib_searchText;
|
||||
"lib/skillBackfill": typeof lib_skillBackfill;
|
||||
"lib/skillPublish": typeof lib_skillPublish;
|
||||
@@ -100,6 +109,7 @@ declare const fullApi: ApiFromModules<{
|
||||
search: typeof search;
|
||||
seed: typeof seed;
|
||||
seedSouls: typeof seedSouls;
|
||||
skillStatEvents: typeof skillStatEvents;
|
||||
skills: typeof skills;
|
||||
soulComments: typeof soulComments;
|
||||
soulDownloads: typeof soulDownloads;
|
||||
|
||||
+5
-4
@@ -1,7 +1,8 @@
|
||||
import { v } from 'convex/values'
|
||||
import type { Doc } from './_generated/dataModel'
|
||||
import { mutation, query } from './_generated/server'
|
||||
import { assertRole, requireUser } from './lib/access'
|
||||
import { assertModerator, requireUser } from './lib/access'
|
||||
import { type PublicUser, toPublicUser } from './lib/public'
|
||||
|
||||
export const listBySkill = query({
|
||||
args: { skillId: v.id('skills'), limit: v.optional(v.number()) },
|
||||
@@ -13,10 +14,10 @@ export const listBySkill = query({
|
||||
.order('desc')
|
||||
.take(limit)
|
||||
|
||||
const results: Array<{ comment: Doc<'comments'>; user: Doc<'users'> | null }> = []
|
||||
const results: Array<{ comment: Doc<'comments'>; user: PublicUser | null }> = []
|
||||
for (const comment of comments) {
|
||||
if (comment.softDeletedAt) continue
|
||||
const user = await ctx.db.get(comment.userId)
|
||||
const user = toPublicUser(await ctx.db.get(comment.userId))
|
||||
results.push({ comment, user })
|
||||
}
|
||||
return results
|
||||
@@ -59,7 +60,7 @@ export const remove = mutation({
|
||||
|
||||
const isOwner = comment.userId === user._id
|
||||
if (!isOwner) {
|
||||
assertRole(user, ['admin', 'moderator'])
|
||||
assertModerator(user)
|
||||
}
|
||||
|
||||
await ctx.db.patch(comment._id, {
|
||||
|
||||
@@ -24,4 +24,11 @@ crons.interval(
|
||||
{ batchSize: 200, maxBatches: 5 },
|
||||
)
|
||||
|
||||
crons.interval(
|
||||
'skill-stat-events',
|
||||
{ minutes: 15 },
|
||||
internal.skillStatEvents.processSkillStatEventsAction,
|
||||
{},
|
||||
)
|
||||
|
||||
export default crons
|
||||
|
||||
+63
-41
@@ -1,5 +1,6 @@
|
||||
import { v } from 'convex/values'
|
||||
import { internal } from './_generated/api'
|
||||
import type { ActionCtx } from './_generated/server'
|
||||
import { internalAction, internalMutation } from './_generated/server'
|
||||
import { EMBEDDING_DIMENSIONS } from './lib/embeddings'
|
||||
import { parseClawdisMetadata, parseFrontmatter } from './lib/skills'
|
||||
@@ -13,6 +14,17 @@ type SeedSkillSpec = {
|
||||
rawSkillMd: string
|
||||
}
|
||||
|
||||
type SeedActionArgs = {
|
||||
reset?: boolean
|
||||
}
|
||||
|
||||
type SeedActionResult = {
|
||||
ok: true
|
||||
results: Array<Record<string, unknown> & { slug: string }>
|
||||
}
|
||||
|
||||
type SeedMutationResult = Record<string, unknown>
|
||||
|
||||
const SEED_SKILLS: SeedSkillSpec[] = [
|
||||
{
|
||||
slug: 'padel',
|
||||
@@ -237,53 +249,19 @@ function injectMetadata(rawSkillMd: string, metadata: Record<string, unknown>) {
|
||||
)}${rawSkillMd.slice(frontmatterEnd)}`
|
||||
}
|
||||
|
||||
export const seedNixSkills = internalAction({
|
||||
args: {
|
||||
reset: v.optional(v.boolean()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const results = []
|
||||
|
||||
for (const spec of SEED_SKILLS) {
|
||||
const skillMd = injectMetadata(spec.rawSkillMd, spec.metadata)
|
||||
const frontmatter = parseFrontmatter(skillMd)
|
||||
const clawdis = parseClawdisMetadata(frontmatter)
|
||||
const storageId = await ctx.storage.store(new Blob([skillMd], { type: 'text/markdown' }))
|
||||
|
||||
const result = await ctx.runMutation(internal.devSeed.seedSkillMutation, {
|
||||
reset: args.reset,
|
||||
storageId,
|
||||
metadata: spec.metadata,
|
||||
frontmatter,
|
||||
clawdis,
|
||||
skillMd,
|
||||
slug: spec.slug,
|
||||
displayName: spec.displayName,
|
||||
summary: spec.summary,
|
||||
version: spec.version,
|
||||
})
|
||||
|
||||
results.push({ slug: spec.slug, ...result })
|
||||
}
|
||||
|
||||
return { ok: true, results }
|
||||
},
|
||||
})
|
||||
|
||||
export const seedPadelSkill = internalAction({
|
||||
args: {
|
||||
reset: v.optional(v.boolean()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const spec = SEED_SKILLS.find((entry) => entry.slug === 'padel')
|
||||
if (!spec) throw new Error('padel seed spec missing')
|
||||
async function seedNixSkillsHandler(
|
||||
ctx: ActionCtx,
|
||||
args: SeedActionArgs,
|
||||
): Promise<SeedActionResult> {
|
||||
const results: Array<Record<string, unknown> & { slug: string }> = []
|
||||
|
||||
for (const spec of SEED_SKILLS) {
|
||||
const skillMd = injectMetadata(spec.rawSkillMd, spec.metadata)
|
||||
const frontmatter = parseFrontmatter(skillMd)
|
||||
const clawdis = parseClawdisMetadata(frontmatter)
|
||||
const storageId = await ctx.storage.store(new Blob([skillMd], { type: 'text/markdown' }))
|
||||
|
||||
return ctx.runMutation(internal.devSeed.seedSkillMutation, {
|
||||
const result: SeedMutationResult = await ctx.runMutation(internal.devSeed.seedSkillMutation, {
|
||||
reset: args.reset,
|
||||
storageId,
|
||||
metadata: spec.metadata,
|
||||
@@ -295,7 +273,51 @@ export const seedPadelSkill = internalAction({
|
||||
summary: spec.summary,
|
||||
version: spec.version,
|
||||
})
|
||||
|
||||
results.push({ slug: spec.slug, ...result })
|
||||
}
|
||||
|
||||
return { ok: true, results }
|
||||
}
|
||||
|
||||
export const seedNixSkills: ReturnType<typeof internalAction> = internalAction({
|
||||
args: {
|
||||
reset: v.optional(v.boolean()),
|
||||
},
|
||||
handler: seedNixSkillsHandler,
|
||||
})
|
||||
|
||||
async function seedPadelSkillHandler(
|
||||
ctx: ActionCtx,
|
||||
args: SeedActionArgs,
|
||||
): Promise<SeedMutationResult> {
|
||||
const spec = SEED_SKILLS.find((entry) => entry.slug === 'padel')
|
||||
if (!spec) throw new Error('padel seed spec missing')
|
||||
|
||||
const skillMd = injectMetadata(spec.rawSkillMd, spec.metadata)
|
||||
const frontmatter = parseFrontmatter(skillMd)
|
||||
const clawdis = parseClawdisMetadata(frontmatter)
|
||||
const storageId = await ctx.storage.store(new Blob([skillMd], { type: 'text/markdown' }))
|
||||
|
||||
return (await ctx.runMutation(internal.devSeed.seedSkillMutation, {
|
||||
reset: args.reset,
|
||||
storageId,
|
||||
metadata: spec.metadata,
|
||||
frontmatter,
|
||||
clawdis,
|
||||
skillMd,
|
||||
slug: spec.slug,
|
||||
displayName: spec.displayName,
|
||||
summary: spec.summary,
|
||||
version: spec.version,
|
||||
})) as SeedMutationResult
|
||||
}
|
||||
|
||||
export const seedPadelSkill: ReturnType<typeof internalAction> = internalAction({
|
||||
args: {
|
||||
reset: v.optional(v.boolean()),
|
||||
},
|
||||
handler: seedPadelSkillHandler,
|
||||
})
|
||||
|
||||
export const seedSkillMutation = internalMutation({
|
||||
|
||||
@@ -0,0 +1,541 @@
|
||||
/**
|
||||
* Extra seed skills for pagination testing.
|
||||
*
|
||||
* This file contains 50 placeholder skills to test pagination behavior.
|
||||
* Run with: bunx convex run internal.devSeedExtra.seedExtraSkillsInternal
|
||||
* Or with reset: bunx convex run internal.devSeedExtra.seedExtraSkillsInternal '{"reset": true}'
|
||||
*/
|
||||
|
||||
import { v } from 'convex/values'
|
||||
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'
|
||||
|
||||
type SeedSkillSpec = {
|
||||
slug: string
|
||||
displayName: string
|
||||
summary: string
|
||||
version: string
|
||||
metadata: Record<string, unknown>
|
||||
rawSkillMd: string
|
||||
}
|
||||
|
||||
function makeSkill(
|
||||
slug: string,
|
||||
displayName: string,
|
||||
summary: string,
|
||||
envVars: string[] = [],
|
||||
commands: string[] = ['help', 'status', 'run'],
|
||||
): SeedSkillSpec {
|
||||
const cliHelp = `${slug} - ${summary}
|
||||
|
||||
Usage:
|
||||
${slug} [command]
|
||||
|
||||
Commands:
|
||||
${commands.map((cmd) => ` ${cmd.padEnd(12)} Run ${cmd} operation`).join('\n')}
|
||||
|
||||
Flags:
|
||||
-h, --help Show help
|
||||
--json Output as JSON
|
||||
`
|
||||
|
||||
const rawSkillMd = `---
|
||||
name: ${slug}
|
||||
description: ${summary}
|
||||
---
|
||||
|
||||
# ${displayName}
|
||||
|
||||
## CLI
|
||||
|
||||
\`\`\`bash
|
||||
${commands.map((cmd) => `${slug} ${cmd}`).join('\n')}
|
||||
\`\`\`
|
||||
|
||||
## Usage
|
||||
|
||||
Use this skill to ${summary.toLowerCase()}.
|
||||
`
|
||||
|
||||
return {
|
||||
slug,
|
||||
displayName,
|
||||
summary,
|
||||
version: '0.1.0',
|
||||
metadata: {
|
||||
clawdbot: {
|
||||
nix: {
|
||||
plugin: `github:example/${slug}`,
|
||||
systems: ['aarch64-darwin', 'x86_64-linux'],
|
||||
},
|
||||
config: {
|
||||
requiredEnv: envVars,
|
||||
},
|
||||
cliHelp,
|
||||
},
|
||||
},
|
||||
rawSkillMd,
|
||||
}
|
||||
}
|
||||
|
||||
// 50 placeholder skills for pagination testing
|
||||
const EXTRA_SEED_SKILLS: SeedSkillSpec[] = [
|
||||
// DevOps & Infrastructure (10)
|
||||
makeSkill(
|
||||
'kubectl-helper',
|
||||
'Kubectl Helper',
|
||||
'Simplified kubectl commands for common Kubernetes operations.',
|
||||
['KUBECONFIG'],
|
||||
['pods', 'logs', 'exec', 'describe', 'apply'],
|
||||
),
|
||||
makeSkill(
|
||||
'terraform-runner',
|
||||
'Terraform Runner',
|
||||
'Execute Terraform plans and applies with safety checks.',
|
||||
['TF_VAR_region', 'AWS_PROFILE'],
|
||||
['plan', 'apply', 'destroy', 'output', 'state'],
|
||||
),
|
||||
makeSkill(
|
||||
'ansible-exec',
|
||||
'Ansible Exec',
|
||||
'Run Ansible playbooks and ad-hoc commands.',
|
||||
['ANSIBLE_INVENTORY'],
|
||||
['playbook', 'adhoc', 'inventory', 'facts', 'vault'],
|
||||
),
|
||||
makeSkill(
|
||||
'docker-compose-mgr',
|
||||
'Docker Compose Manager',
|
||||
'Manage Docker Compose stacks and services.',
|
||||
['DOCKER_HOST'],
|
||||
['up', 'down', 'logs', 'ps', 'restart'],
|
||||
),
|
||||
makeSkill(
|
||||
'k9s-wrapper',
|
||||
'K9s Wrapper',
|
||||
'Interactive Kubernetes cluster management via K9s.',
|
||||
['KUBECONFIG'],
|
||||
['launch', 'contexts', 'namespaces', 'pods', 'logs'],
|
||||
),
|
||||
makeSkill(
|
||||
'helm-charts',
|
||||
'Helm Charts',
|
||||
'Manage Helm chart deployments and releases.',
|
||||
['KUBECONFIG', 'HELM_REPO'],
|
||||
['install', 'upgrade', 'rollback', 'list', 'search'],
|
||||
),
|
||||
makeSkill(
|
||||
'prometheus-alerts',
|
||||
'Prometheus Alerts',
|
||||
'Query Prometheus metrics and manage alerting rules.',
|
||||
['PROMETHEUS_URL'],
|
||||
['query', 'alerts', 'rules', 'targets', 'status'],
|
||||
),
|
||||
makeSkill(
|
||||
'grafana-dash',
|
||||
'Grafana Dashboards',
|
||||
'Create and manage Grafana dashboards programmatically.',
|
||||
['GRAFANA_URL', 'GRAFANA_API_KEY'],
|
||||
['list', 'export', 'import', 'create', 'delete'],
|
||||
),
|
||||
makeSkill(
|
||||
'nginx-config',
|
||||
'Nginx Config',
|
||||
'Generate and validate Nginx configuration files.',
|
||||
['NGINX_CONF_DIR'],
|
||||
['generate', 'validate', 'reload', 'test', 'sites'],
|
||||
),
|
||||
makeSkill(
|
||||
'jenkins-jobs',
|
||||
'Jenkins Jobs',
|
||||
'Manage Jenkins jobs and pipelines.',
|
||||
['JENKINS_URL', 'JENKINS_TOKEN'],
|
||||
['list', 'build', 'status', 'logs', 'config'],
|
||||
),
|
||||
|
||||
// Productivity (8)
|
||||
makeSkill(
|
||||
'todoist-sync',
|
||||
'Todoist Sync',
|
||||
'Sync and manage Todoist tasks from the command line.',
|
||||
['TODOIST_API_TOKEN'],
|
||||
['list', 'add', 'complete', 'projects', 'labels'],
|
||||
),
|
||||
makeSkill(
|
||||
'notion-backup',
|
||||
'Notion Backup',
|
||||
'Export and backup Notion workspaces.',
|
||||
['NOTION_TOKEN'],
|
||||
['export', 'backup', 'restore', 'pages', 'databases'],
|
||||
),
|
||||
makeSkill(
|
||||
'gcal-manager',
|
||||
'Google Calendar Manager',
|
||||
'Manage Google Calendar events and schedules.',
|
||||
['GOOGLE_CREDENTIALS_FILE'],
|
||||
['events', 'create', 'delete', 'calendars', 'reminders'],
|
||||
),
|
||||
makeSkill(
|
||||
'time-tracker',
|
||||
'Time Tracker',
|
||||
'Track time spent on projects and tasks.',
|
||||
['TIMETRACK_DB'],
|
||||
['start', 'stop', 'status', 'report', 'projects'],
|
||||
),
|
||||
makeSkill(
|
||||
'email-digest',
|
||||
'Email Digest',
|
||||
'Generate email digests and summaries.',
|
||||
['IMAP_SERVER', 'IMAP_USER'],
|
||||
['fetch', 'digest', 'search', 'folders', 'unread'],
|
||||
),
|
||||
makeSkill(
|
||||
'habit-tracker',
|
||||
'Habit Tracker',
|
||||
'Track daily habits and streaks.',
|
||||
['HABITS_DB'],
|
||||
['log', 'streak', 'stats', 'habits', 'remind'],
|
||||
),
|
||||
makeSkill(
|
||||
'bookmark-sync',
|
||||
'Bookmark Sync',
|
||||
'Sync bookmarks across browsers and devices.',
|
||||
['BOOKMARKS_DIR'],
|
||||
['sync', 'export', 'import', 'search', 'tags'],
|
||||
),
|
||||
makeSkill(
|
||||
'notes-export',
|
||||
'Notes Export',
|
||||
'Export notes to various formats.',
|
||||
['NOTES_DIR'],
|
||||
['export', 'convert', 'search', 'list', 'tags'],
|
||||
),
|
||||
|
||||
// Media & Entertainment (6)
|
||||
makeSkill(
|
||||
'spotify-ctl',
|
||||
'Spotify Control',
|
||||
'Control Spotify playback from the terminal.',
|
||||
['SPOTIFY_CLIENT_ID', 'SPOTIFY_CLIENT_SECRET'],
|
||||
['play', 'pause', 'next', 'prev', 'search'],
|
||||
),
|
||||
makeSkill(
|
||||
'plex-manager',
|
||||
'Plex Manager',
|
||||
'Manage Plex media libraries and playback.',
|
||||
['PLEX_URL', 'PLEX_TOKEN'],
|
||||
['libraries', 'scan', 'search', 'play', 'sessions'],
|
||||
),
|
||||
makeSkill(
|
||||
'ytdl-wrapper',
|
||||
'YouTube Downloader',
|
||||
'Download videos from YouTube and other platforms.',
|
||||
['YTDL_OUTPUT_DIR'],
|
||||
['download', 'info', 'playlist', 'audio', 'formats'],
|
||||
),
|
||||
makeSkill(
|
||||
'podcast-dl',
|
||||
'Podcast Downloader',
|
||||
'Download and manage podcast episodes.',
|
||||
['PODCAST_DIR'],
|
||||
['subscribe', 'download', 'list', 'play', 'search'],
|
||||
),
|
||||
makeSkill(
|
||||
'audiobook-player',
|
||||
'Audiobook Player',
|
||||
'Manage and play audiobook collections.',
|
||||
['AUDIOBOOK_DIR'],
|
||||
['play', 'pause', 'bookmark', 'list', 'progress'],
|
||||
),
|
||||
makeSkill(
|
||||
'music-lib',
|
||||
'Music Library',
|
||||
'Organize and query local music libraries.',
|
||||
['MUSIC_DIR'],
|
||||
['scan', 'search', 'play', 'playlist', 'stats'],
|
||||
),
|
||||
|
||||
// Smart Home (8)
|
||||
makeSkill(
|
||||
'hass-control',
|
||||
'Home Assistant Control',
|
||||
'Control Home Assistant entities and automations.',
|
||||
['HASS_URL', 'HASS_TOKEN'],
|
||||
['entities', 'services', 'automations', 'scenes', 'history'],
|
||||
),
|
||||
makeSkill(
|
||||
'zigbee-mqtt',
|
||||
'Zigbee2MQTT',
|
||||
'Manage Zigbee devices via MQTT.',
|
||||
['MQTT_BROKER', 'ZIGBEE_TOPIC'],
|
||||
['devices', 'pair', 'remove', 'rename', 'groups'],
|
||||
),
|
||||
makeSkill(
|
||||
'tasmota-ctl',
|
||||
'Tasmota Control',
|
||||
'Control Tasmota-flashed devices.',
|
||||
['TASMOTA_HOSTS'],
|
||||
['status', 'power', 'config', 'update', 'backup'],
|
||||
),
|
||||
makeSkill(
|
||||
'esphome-mgr',
|
||||
'ESPHome Manager',
|
||||
'Manage ESPHome device configurations.',
|
||||
['ESPHOME_DIR'],
|
||||
['compile', 'upload', 'logs', 'dashboard', 'config'],
|
||||
),
|
||||
makeSkill(
|
||||
'mqtt-broker',
|
||||
'MQTT Broker',
|
||||
'Interact with MQTT brokers for IoT messaging.',
|
||||
['MQTT_BROKER', 'MQTT_USER'],
|
||||
['pub', 'sub', 'topics', 'clients', 'stats'],
|
||||
),
|
||||
makeSkill(
|
||||
'hue-lights',
|
||||
'Philips Hue',
|
||||
'Control Philips Hue lights and scenes.',
|
||||
['HUE_BRIDGE_IP', 'HUE_API_KEY'],
|
||||
['lights', 'scenes', 'groups', 'schedules', 'sensors'],
|
||||
),
|
||||
makeSkill(
|
||||
'smart-thermo',
|
||||
'Smart Thermostat',
|
||||
'Control smart thermostats and HVAC systems.',
|
||||
['THERMOSTAT_API_KEY'],
|
||||
['status', 'set', 'schedule', 'history', 'zones'],
|
||||
),
|
||||
makeSkill(
|
||||
'cam-viewer',
|
||||
'Camera Viewer',
|
||||
'View and manage security camera feeds.',
|
||||
['CAMERA_URLS'],
|
||||
['list', 'snapshot', 'stream', 'record', 'events'],
|
||||
),
|
||||
|
||||
// Finance (5)
|
||||
makeSkill(
|
||||
'budget-track',
|
||||
'Budget Tracker',
|
||||
'Track budgets and spending across categories.',
|
||||
['BUDGET_DB'],
|
||||
['summary', 'add', 'categories', 'report', 'goals'],
|
||||
),
|
||||
makeSkill(
|
||||
'crypto-watch',
|
||||
'Crypto Watcher',
|
||||
'Monitor cryptocurrency prices and portfolios.',
|
||||
['CRYPTO_API_KEY'],
|
||||
['prices', 'portfolio', 'alerts', 'history', 'convert'],
|
||||
),
|
||||
makeSkill(
|
||||
'stock-alerts',
|
||||
'Stock Alerts',
|
||||
'Set up stock price alerts and notifications.',
|
||||
['STOCK_API_KEY'],
|
||||
['quote', 'watch', 'alerts', 'portfolio', 'news'],
|
||||
),
|
||||
makeSkill(
|
||||
'expense-cat',
|
||||
'Expense Categorizer',
|
||||
'Automatically categorize expenses.',
|
||||
['EXPENSE_DB'],
|
||||
['import', 'categorize', 'report', 'rules', 'export'],
|
||||
),
|
||||
makeSkill(
|
||||
'invoice-gen',
|
||||
'Invoice Generator',
|
||||
'Generate and manage invoices.',
|
||||
['INVOICE_DIR', 'COMPANY_INFO'],
|
||||
['create', 'list', 'send', 'paid', 'overdue'],
|
||||
),
|
||||
|
||||
// Communication (5)
|
||||
makeSkill(
|
||||
'slack-bot',
|
||||
'Slack Bot',
|
||||
'Interact with Slack channels and messages.',
|
||||
['SLACK_TOKEN'],
|
||||
['send', 'channels', 'users', 'search', 'files'],
|
||||
),
|
||||
makeSkill(
|
||||
'discord-mgr',
|
||||
'Discord Manager',
|
||||
'Manage Discord servers and messages.',
|
||||
['DISCORD_TOKEN'],
|
||||
['send', 'servers', 'channels', 'members', 'roles'],
|
||||
),
|
||||
makeSkill(
|
||||
'telegram-bot',
|
||||
'Telegram Bot',
|
||||
'Send and receive Telegram messages.',
|
||||
['TELEGRAM_BOT_TOKEN'],
|
||||
['send', 'receive', 'chats', 'files', 'inline'],
|
||||
),
|
||||
makeSkill(
|
||||
'matrix-cli',
|
||||
'Matrix CLI',
|
||||
'Interact with Matrix chat rooms.',
|
||||
['MATRIX_HOMESERVER', 'MATRIX_TOKEN'],
|
||||
['send', 'rooms', 'join', 'leave', 'sync'],
|
||||
),
|
||||
makeSkill(
|
||||
'irc-bridge',
|
||||
'IRC Bridge',
|
||||
'Bridge IRC channels to other platforms.',
|
||||
['IRC_SERVER', 'IRC_NICK'],
|
||||
['connect', 'join', 'send', 'channels', 'users'],
|
||||
),
|
||||
|
||||
// Data & Analytics (5)
|
||||
makeSkill(
|
||||
'pg-queries',
|
||||
'PostgreSQL Queries',
|
||||
'Execute PostgreSQL queries and manage databases.',
|
||||
['DATABASE_URL'],
|
||||
['query', 'tables', 'schema', 'backup', 'restore'],
|
||||
),
|
||||
makeSkill(
|
||||
'clickhouse-ql',
|
||||
'ClickHouse Queries',
|
||||
'Run ClickHouse analytics queries.',
|
||||
['CLICKHOUSE_URL'],
|
||||
['query', 'tables', 'insert', 'system', 'optimize'],
|
||||
),
|
||||
makeSkill(
|
||||
'redis-cli',
|
||||
'Redis CLI',
|
||||
'Interact with Redis cache and data structures.',
|
||||
['REDIS_URL'],
|
||||
['get', 'set', 'keys', 'info', 'flush'],
|
||||
),
|
||||
makeSkill(
|
||||
'elastic-search',
|
||||
'Elasticsearch',
|
||||
'Search and manage Elasticsearch indices.',
|
||||
['ELASTICSEARCH_URL'],
|
||||
['search', 'index', 'mapping', 'cluster', 'aliases'],
|
||||
),
|
||||
makeSkill(
|
||||
'mongo-shell',
|
||||
'MongoDB Shell',
|
||||
'Query and manage MongoDB collections.',
|
||||
['MONGODB_URI'],
|
||||
['find', 'insert', 'update', 'delete', 'aggregate'],
|
||||
),
|
||||
|
||||
// Security (3)
|
||||
makeSkill(
|
||||
'vault-secrets',
|
||||
'Vault Secrets',
|
||||
'Manage secrets in HashiCorp Vault.',
|
||||
['VAULT_ADDR', 'VAULT_TOKEN'],
|
||||
['read', 'write', 'list', 'delete', 'seal'],
|
||||
),
|
||||
makeSkill(
|
||||
'gpg-keys',
|
||||
'GPG Keys',
|
||||
'Manage GPG keys and encryption.',
|
||||
['GNUPGHOME'],
|
||||
['list', 'generate', 'export', 'import', 'encrypt'],
|
||||
),
|
||||
makeSkill(
|
||||
'ssh-rotate',
|
||||
'SSH Key Rotator',
|
||||
'Rotate and manage SSH keys.',
|
||||
['SSH_KEY_DIR'],
|
||||
['generate', 'rotate', 'deploy', 'list', 'revoke'],
|
||||
),
|
||||
]
|
||||
|
||||
function injectMetadata(rawSkillMd: string, metadata: Record<string, unknown>) {
|
||||
const frontmatterEnd = rawSkillMd.indexOf('\n---', 3)
|
||||
if (frontmatterEnd === -1) return rawSkillMd
|
||||
return `${rawSkillMd.slice(0, frontmatterEnd)}\nmetadata: ${JSON.stringify(
|
||||
metadata,
|
||||
)}${rawSkillMd.slice(frontmatterEnd)}`
|
||||
}
|
||||
|
||||
function randomStats() {
|
||||
return {
|
||||
downloads: Math.floor(Math.random() * 5000),
|
||||
stars: Math.floor(Math.random() * 500),
|
||||
installsCurrent: Math.floor(Math.random() * 200),
|
||||
installsAllTime: Math.floor(Math.random() * 1000),
|
||||
}
|
||||
}
|
||||
|
||||
export const applyRandomStats = internalMutation({
|
||||
args: {
|
||||
skillId: v.id('skills'),
|
||||
stats: v.object({
|
||||
downloads: v.number(),
|
||||
stars: v.number(),
|
||||
installsCurrent: v.number(),
|
||||
installsAllTime: v.number(),
|
||||
}),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
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,
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const seedExtraSkillsInternal = internalAction({
|
||||
args: {
|
||||
reset: v.optional(v.boolean()),
|
||||
},
|
||||
handler: async (ctx: ActionCtx, args) => {
|
||||
const results: Array<{ slug: string; ok: boolean; skipped?: boolean }> = []
|
||||
|
||||
for (const spec of EXTRA_SEED_SKILLS) {
|
||||
const skillMd = injectMetadata(spec.rawSkillMd, spec.metadata)
|
||||
const frontmatter = parseFrontmatter(skillMd)
|
||||
const clawdis = parseClawdisMetadata(frontmatter)
|
||||
const storageId = await ctx.storage.store(new Blob([skillMd], { type: 'text/markdown' }))
|
||||
|
||||
const result = (await ctx.runMutation(internal.devSeed.seedSkillMutation, {
|
||||
reset: args.reset,
|
||||
storageId,
|
||||
metadata: spec.metadata,
|
||||
frontmatter,
|
||||
clawdis,
|
||||
skillMd,
|
||||
slug: spec.slug,
|
||||
displayName: spec.displayName,
|
||||
summary: spec.summary,
|
||||
version: spec.version,
|
||||
})) as { ok: boolean; skipped?: boolean; skillId?: string }
|
||||
|
||||
// Apply random stats after creation (only if not skipped)
|
||||
if (result.skillId && !result.skipped) {
|
||||
const stats = randomStats()
|
||||
await ctx.runMutation(internal.devSeedExtra.applyRandomStats, {
|
||||
skillId: result.skillId as Id<'skills'>,
|
||||
stats,
|
||||
})
|
||||
}
|
||||
|
||||
results.push({ slug: spec.slug, ok: result.ok, skipped: result.skipped })
|
||||
}
|
||||
|
||||
const created = results.filter((r) => !r.skipped).length
|
||||
const skipped = results.filter((r) => r.skipped).length
|
||||
|
||||
return { ok: true, total: results.length, created, skipped }
|
||||
},
|
||||
})
|
||||
+4
-7
@@ -2,7 +2,7 @@ import { v } from 'convex/values'
|
||||
import { zipSync } from 'fflate'
|
||||
import { api } from './_generated/api'
|
||||
import { httpAction, mutation } from './_generated/server'
|
||||
import { applySkillStatDeltas, bumpDailySkillStats } from './lib/skillStats'
|
||||
import { insertStatEvent } from './skillStatEvents'
|
||||
|
||||
export const downloadZip = httpAction(async (ctx, request) => {
|
||||
const url = new URL(request.url)
|
||||
@@ -70,12 +70,9 @@ export const increment = mutation({
|
||||
handler: async (ctx, args) => {
|
||||
const skill = await ctx.db.get(args.skillId)
|
||||
if (!skill) return
|
||||
const now = Date.now()
|
||||
const patch = applySkillStatDeltas(skill, { downloads: 1 })
|
||||
await ctx.db.patch(skill._id, {
|
||||
...patch,
|
||||
updatedAt: now,
|
||||
await insertStatEvent(ctx, {
|
||||
skillId: skill._id,
|
||||
kind: 'download',
|
||||
})
|
||||
await bumpDailySkillStats(ctx, { skillId: skill._id, now, downloads: 1 })
|
||||
},
|
||||
})
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { ApiRoutes, LegacyApiRoutes } from 'clawdhub-schema'
|
||||
import { ApiRoutes, LegacyApiRoutes } from 'clawhub-schema'
|
||||
import { httpRouter } from 'convex/server'
|
||||
import { auth } from './auth'
|
||||
import { downloadZip } from './downloads'
|
||||
|
||||
@@ -11,7 +11,7 @@ vi.mock('./skills', () => ({
|
||||
|
||||
const { requireApiTokenUser } = await import('./lib/apiTokenAuth')
|
||||
const { publishVersionForUser } = await import('./skills')
|
||||
const { __handlers, cliSkillDeleteHttp, cliSkillUndeleteHttp } = await import('./httpApi')
|
||||
const { __handlers } = await import('./httpApi')
|
||||
const { hashSkillFiles } = await import('./lib/skills')
|
||||
|
||||
function makeCtx(partial: Record<string, unknown>) {
|
||||
@@ -33,7 +33,7 @@ describe('httpApi handlers', () => {
|
||||
expect(await response.json()).toEqual({ results: [] })
|
||||
})
|
||||
|
||||
it('searchSkillsHttp forwards args', async () => {
|
||||
it('searchSkillsHttp forwards args (approvedOnly alias)', async () => {
|
||||
const runAction = vi.fn().mockResolvedValue([
|
||||
{
|
||||
score: 1,
|
||||
@@ -48,14 +48,27 @@ describe('httpApi handlers', () => {
|
||||
expect(runAction).toHaveBeenCalledWith(expect.anything(), {
|
||||
query: 'test',
|
||||
limit: 5,
|
||||
approvedOnly: true,
|
||||
highlightedOnly: true,
|
||||
})
|
||||
expect(response.status).toBe(200)
|
||||
const json = await response.json()
|
||||
expect(json.results[0].slug).toBe('a')
|
||||
})
|
||||
|
||||
it('searchSkillsHttp omits approvedOnly when false', async () => {
|
||||
it('searchSkillsHttp forwards highlightedOnly', async () => {
|
||||
const runAction = vi.fn().mockResolvedValue([])
|
||||
await __handlers.searchSkillsHandler(
|
||||
makeCtx({ runAction }),
|
||||
new Request('https://example.com/api/search?q=test&highlightedOnly=true'),
|
||||
)
|
||||
expect(runAction).toHaveBeenCalledWith(expect.anything(), {
|
||||
query: 'test',
|
||||
limit: undefined,
|
||||
highlightedOnly: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('searchSkillsHttp omits highlightedOnly when approvedOnly is false', async () => {
|
||||
const runAction = vi.fn().mockResolvedValue([])
|
||||
await __handlers.searchSkillsHandler(
|
||||
makeCtx({ runAction }),
|
||||
@@ -64,7 +77,7 @@ describe('httpApi handlers', () => {
|
||||
expect(runAction).toHaveBeenCalledWith(expect.anything(), {
|
||||
query: 'test',
|
||||
limit: undefined,
|
||||
approvedOnly: undefined,
|
||||
highlightedOnly: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -416,13 +429,14 @@ describe('httpApi handlers', () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
vi.mocked(requireApiTokenUser).mockResolvedValueOnce({ userId: 'user1' } as never)
|
||||
const runMutation = vi.fn().mockResolvedValue({ ok: true })
|
||||
const response = await cliSkillUndeleteHttp(
|
||||
const response = await __handlers.cliSkillDeleteHandler(
|
||||
makeCtx({ runMutation }),
|
||||
new Request('https://x/api/cli/skill/undelete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ slug: 'demo' }),
|
||||
}),
|
||||
false,
|
||||
)
|
||||
expect(response.status).toBe(200)
|
||||
expect(runMutation).toHaveBeenCalledWith(expect.anything(), {
|
||||
@@ -437,13 +451,14 @@ describe('httpApi handlers', () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
vi.mocked(requireApiTokenUser).mockResolvedValueOnce({ userId: 'user1' } as never)
|
||||
const runMutation = vi.fn().mockResolvedValue({ ok: true })
|
||||
const response = await cliSkillDeleteHttp(
|
||||
const response = await __handlers.cliSkillDeleteHandler(
|
||||
makeCtx({ runMutation }),
|
||||
new Request('https://x/api/cli/skill/delete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ slug: 'demo' }),
|
||||
}),
|
||||
true,
|
||||
)
|
||||
expect(response.status).toBe(200)
|
||||
expect(runMutation).toHaveBeenCalledWith(expect.anything(), {
|
||||
|
||||
+3
-2
@@ -5,7 +5,7 @@ import {
|
||||
CliSkillDeleteRequestSchema,
|
||||
CliTelemetrySyncRequestSchema,
|
||||
parseArk,
|
||||
} from 'clawdhub-schema'
|
||||
} from 'clawhub-schema'
|
||||
import { api, internal } from './_generated/api'
|
||||
import type { Id } from './_generated/dataModel'
|
||||
import type { ActionCtx } from './_generated/server'
|
||||
@@ -44,13 +44,14 @@ async function searchSkillsHandler(ctx: ActionCtx, request: Request) {
|
||||
const query = url.searchParams.get('q')?.trim() ?? ''
|
||||
const limit = toOptionalNumber(url.searchParams.get('limit'))
|
||||
const approvedOnly = url.searchParams.get('approvedOnly') === 'true'
|
||||
const highlightedOnly = url.searchParams.get('highlightedOnly') === 'true' || approvedOnly
|
||||
|
||||
if (!query) return json({ results: [] })
|
||||
|
||||
const results = (await ctx.runAction(api.search.searchSkills, {
|
||||
query,
|
||||
limit,
|
||||
approvedOnly: approvedOnly || undefined,
|
||||
highlightedOnly: highlightedOnly || undefined,
|
||||
})) as SearchSkillEntry[]
|
||||
|
||||
return json({
|
||||
|
||||
+32
-6
@@ -1,4 +1,4 @@
|
||||
import { CliPublishRequestSchema, parseArk } from 'clawdhub-schema'
|
||||
import { CliPublishRequestSchema, parseArk } from 'clawhub-schema'
|
||||
import { api, internal } from './_generated/api'
|
||||
import type { Doc, Id } from './_generated/dataModel'
|
||||
import type { ActionCtx } from './_generated/server'
|
||||
@@ -44,6 +44,9 @@ type ListSkillsResult = {
|
||||
nextCursor: string | null
|
||||
}
|
||||
|
||||
type SkillFile = Doc<'skillVersions'>['files'][number]
|
||||
type SoulFile = Doc<'soulVersions'>['files'][number]
|
||||
|
||||
type GetBySlugResult = {
|
||||
skill: {
|
||||
_id: Id<'skills'>
|
||||
@@ -56,7 +59,7 @@ type GetBySlugResult = {
|
||||
updatedAt: number
|
||||
} | null
|
||||
latestVersion: Doc<'skillVersions'> | null
|
||||
owner: { handle?: string; displayName?: string; image?: string } | null
|
||||
owner: { _id: Id<'users'>; handle?: string; displayName?: string; image?: string } | null
|
||||
} | null
|
||||
|
||||
type ListVersionsResult = {
|
||||
@@ -264,6 +267,7 @@ async function skillsGetRouterV1Handler(ctx: ActionCtx, request: Request) {
|
||||
owner: result.owner
|
||||
? {
|
||||
handle: result.owner.handle ?? null,
|
||||
userId: result.owner._id,
|
||||
displayName: result.owner.displayName ?? null,
|
||||
image: result.owner.image ?? null,
|
||||
}
|
||||
@@ -318,7 +322,7 @@ async function skillsGetRouterV1Handler(ctx: ActionCtx, request: Request) {
|
||||
createdAt: version.createdAt,
|
||||
changelog: version.changelog,
|
||||
changelogSource: version.changelogSource ?? null,
|
||||
files: version.files.map((file) => ({
|
||||
files: version.files.map((file: SkillFile) => ({
|
||||
path: file.path,
|
||||
size: file.size,
|
||||
sha256: file.sha256,
|
||||
@@ -371,6 +375,9 @@ async function skillsGetRouterV1Handler(ctx: ActionCtx, request: Request) {
|
||||
if (!blob) return text('File missing in storage', 410, rate.headers)
|
||||
const textContent = await blob.text()
|
||||
|
||||
const isSvg =
|
||||
file.contentType?.toLowerCase().includes('svg') || file.path.toLowerCase().endsWith('.svg')
|
||||
|
||||
const headers = mergeHeaders(rate.headers, {
|
||||
'Content-Type': file.contentType
|
||||
? `${file.contentType}; charset=utf-8`
|
||||
@@ -379,6 +386,14 @@ async function skillsGetRouterV1Handler(ctx: ActionCtx, request: Request) {
|
||||
ETag: file.sha256,
|
||||
'X-Content-SHA256': file.sha256,
|
||||
'X-Content-Size': String(file.size),
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
'X-Frame-Options': 'DENY',
|
||||
// For any text response that a browser might try to render, lock it down.
|
||||
// In particular, this prevents SVG <foreignObject> script execution from
|
||||
// reading localStorage tokens on this origin.
|
||||
'Content-Security-Policy':
|
||||
"default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
|
||||
...(isSvg ? { 'Content-Disposition': 'attachment' } : {}),
|
||||
})
|
||||
return new Response(textContent, { status: 200, headers })
|
||||
}
|
||||
@@ -784,8 +799,8 @@ function parseListSort(value: string | null): SkillListSort {
|
||||
}
|
||||
|
||||
async function sha256Hex(bytes: Uint8Array) {
|
||||
const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength)
|
||||
const digest = await crypto.subtle.digest('SHA-256', buffer)
|
||||
const data = new Uint8Array(bytes)
|
||||
const digest = await crypto.subtle.digest('SHA-256', data)
|
||||
return toHex(new Uint8Array(digest))
|
||||
}
|
||||
|
||||
@@ -925,7 +940,7 @@ async function soulsGetRouterV1Handler(ctx: ActionCtx, request: Request) {
|
||||
createdAt: version.createdAt,
|
||||
changelog: version.changelog,
|
||||
changelogSource: version.changelogSource ?? null,
|
||||
files: version.files.map((file) => ({
|
||||
files: version.files.map((file: SoulFile) => ({
|
||||
path: file.path,
|
||||
size: file.size,
|
||||
sha256: file.sha256,
|
||||
@@ -980,6 +995,9 @@ async function soulsGetRouterV1Handler(ctx: ActionCtx, request: Request) {
|
||||
|
||||
void ctx.runMutation(api.soulDownloads.increment, { soulId: soulResult.soul._id })
|
||||
|
||||
const isSvg =
|
||||
file.contentType?.toLowerCase().includes('svg') || file.path.toLowerCase().endsWith('.svg')
|
||||
|
||||
const headers = mergeHeaders(rate.headers, {
|
||||
'Content-Type': file.contentType
|
||||
? `${file.contentType}; charset=utf-8`
|
||||
@@ -988,6 +1006,14 @@ async function soulsGetRouterV1Handler(ctx: ActionCtx, request: Request) {
|
||||
ETag: file.sha256,
|
||||
'X-Content-SHA256': file.sha256,
|
||||
'X-Content-Size': String(file.size),
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
'X-Frame-Options': 'DENY',
|
||||
// For any text response that a browser might try to render, lock it down.
|
||||
// In particular, this prevents SVG <foreignObject> script execution from
|
||||
// reading localStorage tokens on this origin.
|
||||
'Content-Security-Policy':
|
||||
"default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
|
||||
...(isSvg ? { 'Content-Disposition': 'attachment' } : {}),
|
||||
})
|
||||
return new Response(textContent, { status: 200, headers })
|
||||
}
|
||||
|
||||
+10
-2
@@ -1,5 +1,5 @@
|
||||
import { getAuthUserId } from '@convex-dev/auth/server'
|
||||
import { api } from '../_generated/api'
|
||||
import { internal } from '../_generated/api'
|
||||
import type { Doc } from '../_generated/dataModel'
|
||||
import type { ActionCtx, MutationCtx, QueryCtx } from '../_generated/server'
|
||||
|
||||
@@ -16,7 +16,7 @@ export async function requireUser(ctx: MutationCtx | QueryCtx) {
|
||||
export async function requireUserFromAction(ctx: ActionCtx) {
|
||||
const userId = await getAuthUserId(ctx)
|
||||
if (!userId) throw new Error('Unauthorized')
|
||||
const user = await ctx.runQuery(api.users.getById, { userId })
|
||||
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'> }
|
||||
}
|
||||
@@ -26,3 +26,11 @@ export function assertRole(user: Doc<'users'>, allowed: Role[]) {
|
||||
throw new Error('Forbidden')
|
||||
}
|
||||
}
|
||||
|
||||
export function assertAdmin(user: Doc<'users'>) {
|
||||
assertRole(user, ['admin'])
|
||||
}
|
||||
|
||||
export function assertModerator(user: Doc<'users'>) {
|
||||
assertRole(user, ['admin', 'moderator'])
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { Doc, Id } from '../_generated/dataModel'
|
||||
import type { QueryCtx } from '../_generated/server'
|
||||
|
||||
type BadgeKind = Doc<'skillBadges'>['kind']
|
||||
|
||||
export type SkillBadgeMap = Partial<Record<BadgeKind, { byUserId: Id<'users'>; at: number }>>
|
||||
|
||||
export type SkillBadgeSource = { badges?: SkillBadgeMap | null }
|
||||
|
||||
type BadgeCtx = Pick<QueryCtx, 'db'>
|
||||
|
||||
export function isSkillHighlighted(skill: SkillBadgeSource) {
|
||||
return Boolean(skill.badges?.highlighted)
|
||||
}
|
||||
|
||||
export function isSkillOfficial(skill: SkillBadgeSource) {
|
||||
return Boolean(skill.badges?.official)
|
||||
}
|
||||
|
||||
export function isSkillDeprecated(skill: SkillBadgeSource) {
|
||||
return Boolean(skill.badges?.deprecated)
|
||||
}
|
||||
|
||||
export function buildBadgeMap(records: Doc<'skillBadges'>[]): SkillBadgeMap {
|
||||
return records.reduce<SkillBadgeMap>((acc, record) => {
|
||||
acc[record.kind] = { byUserId: record.byUserId, at: record.at }
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
export async function getSkillBadgeMap(
|
||||
ctx: BadgeCtx,
|
||||
skillId: Id<'skills'>,
|
||||
): Promise<SkillBadgeMap> {
|
||||
const records = await ctx.db
|
||||
.query('skillBadges')
|
||||
.withIndex('by_skill', (q) => q.eq('skillId', skillId))
|
||||
.collect()
|
||||
return buildBadgeMap(records)
|
||||
}
|
||||
|
||||
export async function getSkillBadgeMaps(
|
||||
ctx: BadgeCtx,
|
||||
skillIds: Array<Id<'skills'>>,
|
||||
): Promise<Map<Id<'skills'>, SkillBadgeMap>> {
|
||||
const entries = await Promise.all(
|
||||
skillIds.map(async (skillId) => [skillId, await getSkillBadgeMap(ctx, skillId)] as const),
|
||||
)
|
||||
return new Map(entries)
|
||||
}
|
||||
@@ -8,7 +8,7 @@ const GITHUB_API = 'https://api.github.com'
|
||||
const DEFAULT_REPO = 'clawdbot/skills'
|
||||
const DEFAULT_ROOT = 'skills'
|
||||
const META_FILENAME = '_meta.json'
|
||||
const USER_AGENT = 'clawdhub/skills-backup'
|
||||
const USER_AGENT = 'clawhub/skills-backup'
|
||||
|
||||
type BackupFile = {
|
||||
path: string
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TEXT_FILE_EXTENSION_SET } from 'clawdhub-schema'
|
||||
import { TEXT_FILE_EXTENSION_SET } from 'clawhub-schema'
|
||||
import { zipSync } from 'fflate'
|
||||
import semver from 'semver'
|
||||
import { parseFrontmatter } from './skills'
|
||||
@@ -118,7 +118,7 @@ async function resolveRefCommit(parsed: GitHubImportUrl, ref: string, fetcher: t
|
||||
const response = await fetcher(apiUrl, {
|
||||
headers: {
|
||||
Accept: 'application/vnd.github+json',
|
||||
'User-Agent': 'clawdhub/github-import',
|
||||
'User-Agent': 'clawhub/github-import',
|
||||
},
|
||||
})
|
||||
if (!response.ok) throw new Error('GitHub ref not found')
|
||||
@@ -156,7 +156,7 @@ export async function fetchGitHubZipBytes(
|
||||
const maxZipBytes = limits?.maxZipBytes ?? 25 * 1024 * 1024
|
||||
const url = `https://${CODELOAD_HOST}/${resolved.owner}/${resolved.repo}/zip/${resolved.commit}`
|
||||
const response = await fetcher(url, {
|
||||
headers: { 'User-Agent': 'clawdhub/github-import' },
|
||||
headers: { 'User-Agent': 'clawhub/github-import' },
|
||||
})
|
||||
if (!response.ok) throw new Error('GitHub archive download failed')
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ const GITHUB_API = 'https://api.github.com'
|
||||
const DEFAULT_REPO = 'clawdbot/souls'
|
||||
const DEFAULT_ROOT = 'souls'
|
||||
const META_FILENAME = '_meta.json'
|
||||
const USER_AGENT = 'clawdhub/souls-backup'
|
||||
const USER_AGENT = 'clawhub/souls-backup'
|
||||
|
||||
type BackupFile = {
|
||||
path: string
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { Doc } from '../_generated/dataModel'
|
||||
|
||||
const FLAG_RULES: Array<{ flag: string; pattern: RegExp }> = [
|
||||
// Known-bad / known-suspicious identifiers.
|
||||
// NOTE: keep these narrowly scoped; use staff review to confirm removals.
|
||||
{
|
||||
flag: 'blocked.malware',
|
||||
pattern: /(keepcold131\/ClawdAuthenticatorTool|ClawdAuthenticatorTool)/i,
|
||||
},
|
||||
|
||||
{ flag: 'suspicious.keyword', pattern: /(malware|stealer|phish|phishing|keylogger)/i },
|
||||
{ flag: 'suspicious.secrets', pattern: /(api[-_ ]?key|token|password|private key|secret)/i },
|
||||
{ flag: 'suspicious.crypto', pattern: /(wallet|seed phrase|mnemonic|crypto)/i },
|
||||
{ flag: 'suspicious.webhook', pattern: /(discord\.gg|webhook|hooks\.slack)/i },
|
||||
{ flag: 'suspicious.script', pattern: /(curl[^\n]+\|\s*(sh|bash))/i },
|
||||
{ flag: 'suspicious.url_shortener', pattern: /(bit\.ly|tinyurl\.com|t\.co|goo\.gl|is\.gd)/i },
|
||||
]
|
||||
|
||||
export function deriveModerationFlags({
|
||||
skill,
|
||||
parsed,
|
||||
files,
|
||||
}: {
|
||||
skill: Pick<Doc<'skills'>, 'slug' | 'displayName' | 'summary'>
|
||||
parsed: Doc<'skillVersions'>['parsed']
|
||||
files: Doc<'skillVersions'>['files']
|
||||
}) {
|
||||
const text = [
|
||||
skill.slug,
|
||||
skill.displayName,
|
||||
skill.summary ?? '',
|
||||
JSON.stringify(parsed?.frontmatter ?? {}),
|
||||
JSON.stringify(parsed?.metadata ?? {}),
|
||||
JSON.stringify((parsed as { moltbot?: unknown } | undefined)?.moltbot ?? {}),
|
||||
...files.map((file) => file.path),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
|
||||
const flags = new Set<string>()
|
||||
|
||||
for (const rule of FLAG_RULES) {
|
||||
if (rule.pattern.test(text)) {
|
||||
flags.add(rule.flag)
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(flags)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import type { Doc } from '../_generated/dataModel'
|
||||
|
||||
export type PublicUser = Pick<
|
||||
Doc<'users'>,
|
||||
'_id' | '_creationTime' | 'handle' | 'name' | 'displayName' | 'image' | 'bio'
|
||||
>
|
||||
|
||||
export type PublicSkill = Pick<
|
||||
Doc<'skills'>,
|
||||
| '_id'
|
||||
| '_creationTime'
|
||||
| 'slug'
|
||||
| 'displayName'
|
||||
| 'summary'
|
||||
| 'ownerUserId'
|
||||
| 'canonicalSkillId'
|
||||
| 'forkOf'
|
||||
| 'latestVersionId'
|
||||
| 'tags'
|
||||
| 'badges'
|
||||
| 'stats'
|
||||
| 'createdAt'
|
||||
| 'updatedAt'
|
||||
>
|
||||
|
||||
export type PublicSoul = Pick<
|
||||
Doc<'souls'>,
|
||||
| '_id'
|
||||
| '_creationTime'
|
||||
| 'slug'
|
||||
| 'displayName'
|
||||
| 'summary'
|
||||
| 'ownerUserId'
|
||||
| 'latestVersionId'
|
||||
| 'tags'
|
||||
| 'stats'
|
||||
| 'createdAt'
|
||||
| 'updatedAt'
|
||||
>
|
||||
|
||||
export function toPublicUser(user: Doc<'users'> | null | undefined): PublicUser | null {
|
||||
if (!user || user.deletedAt) return null
|
||||
return {
|
||||
_id: user._id,
|
||||
_creationTime: user._creationTime,
|
||||
handle: user.handle,
|
||||
name: user.name,
|
||||
displayName: user.displayName,
|
||||
image: user.image,
|
||||
bio: user.bio,
|
||||
}
|
||||
}
|
||||
|
||||
export function toPublicSkill(skill: Doc<'skills'> | 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
|
||||
return {
|
||||
_id: skill._id,
|
||||
_creationTime: skill._creationTime,
|
||||
slug: skill.slug,
|
||||
displayName: skill.displayName,
|
||||
summary: skill.summary,
|
||||
ownerUserId: skill.ownerUserId,
|
||||
canonicalSkillId: skill.canonicalSkillId,
|
||||
forkOf: skill.forkOf,
|
||||
latestVersionId: skill.latestVersionId,
|
||||
tags: skill.tags,
|
||||
badges: skill.badges,
|
||||
stats: skill.stats,
|
||||
createdAt: skill.createdAt,
|
||||
updatedAt: skill.updatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
export function toPublicSoul(soul: Doc<'souls'> | null | undefined): PublicSoul | null {
|
||||
if (!soul || soul.softDeletedAt) return null
|
||||
return {
|
||||
_id: soul._id,
|
||||
_creationTime: soul._creationTime,
|
||||
slug: soul.slug,
|
||||
displayName: soul.displayName,
|
||||
summary: soul.summary,
|
||||
ownerUserId: soul.ownerUserId,
|
||||
latestVersionId: soul.latestVersionId,
|
||||
tags: soul.tags,
|
||||
stats: soul.stats,
|
||||
createdAt: soul.createdAt,
|
||||
updatedAt: soul.updatedAt,
|
||||
}
|
||||
}
|
||||
@@ -13,12 +13,26 @@ describe('searchText', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('matchesExactTokens requires all query tokens', () => {
|
||||
it('matchesExactTokens requires at least one query token to prefix-match', () => {
|
||||
const queryTokens = tokenize('Remind Me')
|
||||
expect(matchesExactTokens(queryTokens, ['Remind Me', '/remind-me', 'Short summary'])).toBe(true)
|
||||
// "Reminder" starts with "remind", so it matches with prefix matching
|
||||
expect(matchesExactTokens(queryTokens, ['Reminder tool', '/reminder', 'Short summary'])).toBe(
|
||||
false,
|
||||
true,
|
||||
)
|
||||
// Matches because "remind" token is present
|
||||
expect(matchesExactTokens(queryTokens, ['Remind tool', '/remind', 'Short summary'])).toBe(true)
|
||||
// No matching tokens at all
|
||||
expect(matchesExactTokens(queryTokens, ['Other tool', '/other', 'Short summary'])).toBe(false)
|
||||
})
|
||||
|
||||
it('matchesExactTokens supports prefix matching for partial queries', () => {
|
||||
// "go" should match "gohome" because "gohome" starts with "go"
|
||||
expect(matchesExactTokens(['go'], ['GoHome', '/gohome', 'Navigate home'])).toBe(true)
|
||||
// "pad" should match "padel"
|
||||
expect(matchesExactTokens(['pad'], ['Padel', '/padel', 'Tennis-like sport'])).toBe(true)
|
||||
// "xyz" should not match anything
|
||||
expect(matchesExactTokens(['xyz'], ['GoHome', '/gohome', 'Navigate home'])).toBe(false)
|
||||
})
|
||||
|
||||
it('matchesExactTokens ignores empty inputs', () => {
|
||||
|
||||
@@ -18,8 +18,10 @@ export function matchesExactTokens(
|
||||
if (!text) return false
|
||||
const textTokens = tokenize(text)
|
||||
if (textTokens.length === 0) return false
|
||||
const textSet = new Set(textTokens)
|
||||
return queryTokens.every((token) => textSet.has(token))
|
||||
// Require at least one token to prefix-match, allowing vector similarity to determine relevance
|
||||
return queryTokens.some((queryToken) =>
|
||||
textTokens.some((textToken) => textToken.includes(queryToken)),
|
||||
)
|
||||
}
|
||||
|
||||
export const __test = { normalize, tokenize, matchesExactTokens }
|
||||
|
||||
+22
-13
@@ -3,8 +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 { generateChangelogForPublish } from './changelog'
|
||||
import { generateEmbedding } from './embeddings'
|
||||
import type { PublicUser } from './public'
|
||||
import {
|
||||
buildEmbeddingText,
|
||||
getFrontmatterMetadata,
|
||||
@@ -75,16 +77,20 @@ export async function publishVersionForUser(
|
||||
if (sanitizedFiles.some((file) => !file.path)) {
|
||||
throw new ConvexError('Invalid file paths')
|
||||
}
|
||||
if (sanitizedFiles.some((file) => !isTextFile(file.path ?? '', file.contentType ?? undefined))) {
|
||||
const safeFiles = sanitizedFiles.map((file) => ({
|
||||
...file,
|
||||
path: file.path as string,
|
||||
}))
|
||||
if (safeFiles.some((file) => !isTextFile(file.path, file.contentType ?? undefined))) {
|
||||
throw new ConvexError('Only text-based files are allowed')
|
||||
}
|
||||
|
||||
const totalBytes = sanitizedFiles.reduce((sum, file) => sum + file.size, 0)
|
||||
const totalBytes = safeFiles.reduce((sum, file) => sum + file.size, 0)
|
||||
if (totalBytes > MAX_TOTAL_BYTES) {
|
||||
throw new ConvexError('Skill bundle exceeds 50MB limit')
|
||||
}
|
||||
|
||||
const readmeFile = sanitizedFiles.find(
|
||||
const readmeFile = safeFiles.find(
|
||||
(file) => file.path?.toLowerCase() === 'skill.md' || file.path?.toLowerCase() === 'skills.md',
|
||||
)
|
||||
if (!readmeFile) throw new ConvexError('SKILL.md is required')
|
||||
@@ -95,7 +101,7 @@ export async function publishVersionForUser(
|
||||
const metadata = mergeSourceIntoMetadata(getFrontmatterMetadata(frontmatter), args.source)
|
||||
|
||||
const otherFiles = [] as Array<{ path: string; content: string }>
|
||||
for (const file of sanitizedFiles) {
|
||||
for (const file of safeFiles) {
|
||||
if (!file.path || file.path.toLowerCase().endsWith('.md')) continue
|
||||
if (!isTextFile(file.path, file.contentType ?? undefined)) continue
|
||||
const content = await fetchText(ctx, file.storageId)
|
||||
@@ -110,7 +116,7 @@ export async function publishVersionForUser(
|
||||
})
|
||||
|
||||
const fingerprintPromise = hashSkillFiles(
|
||||
sanitizedFiles.map((file) => ({ path: file.path ?? '', sha256: file.sha256 })),
|
||||
safeFiles.map((file) => ({ path: file.path, sha256: file.sha256 })),
|
||||
)
|
||||
|
||||
const changelogPromise =
|
||||
@@ -120,7 +126,7 @@ export async function publishVersionForUser(
|
||||
slug,
|
||||
version,
|
||||
readmeText,
|
||||
files: sanitizedFiles.map((file) => ({ path: file.path ?? '', sha256: file.sha256 })),
|
||||
files: safeFiles.map((file) => ({ path: file.path, sha256: file.sha256 })),
|
||||
})
|
||||
|
||||
const embeddingPromise = generateEmbedding(embeddingText)
|
||||
@@ -148,9 +154,9 @@ export async function publishVersionForUser(
|
||||
version: args.forkOf.version?.trim() || undefined,
|
||||
}
|
||||
: undefined,
|
||||
files: sanitizedFiles.map((file) => ({
|
||||
files: safeFiles.map((file) => ({
|
||||
...file,
|
||||
path: file.path ?? '',
|
||||
path: file.path,
|
||||
})),
|
||||
parsed: {
|
||||
frontmatter,
|
||||
@@ -160,7 +166,9 @@ export async function publishVersionForUser(
|
||||
embedding,
|
||||
})) as PublishResult
|
||||
|
||||
const owner = (await ctx.runQuery(api.users.getById, { userId })) as Doc<'users'> | null
|
||||
const owner = (await ctx.runQuery(internal.users.getByIdInternal, {
|
||||
userId,
|
||||
})) as Doc<'users'> | null
|
||||
const ownerHandle = owner?.handle ?? owner?.displayName ?? owner?.name ?? 'unknown'
|
||||
|
||||
void ctx.scheduler
|
||||
@@ -169,7 +177,7 @@ export async function publishVersionForUser(
|
||||
version,
|
||||
displayName,
|
||||
ownerHandle,
|
||||
files: sanitizedFiles,
|
||||
files: safeFiles,
|
||||
publishedAt: Date.now(),
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -212,13 +220,14 @@ 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 payload: WebhookSkillPayload = {
|
||||
slug: skill.slug,
|
||||
displayName: skill.displayName,
|
||||
summary: skill.summary ?? undefined,
|
||||
version: latestVersion?.version ?? undefined,
|
||||
ownerHandle: owner?.handle ?? owner?.name ?? undefined,
|
||||
batch: skill.batch ?? undefined,
|
||||
highlighted: isSkillHighlighted({ badges }),
|
||||
tags: Object.keys(skill.tags ?? {}),
|
||||
}
|
||||
|
||||
@@ -255,7 +264,7 @@ async function schedulePublishWebhook(
|
||||
) {
|
||||
const result = (await ctx.runQuery(api.skills.getBySlug, {
|
||||
slug: params.slug,
|
||||
})) as { skill: Doc<'skills'>; owner: Doc<'users'> | null } | null
|
||||
})) as { skill: Doc<'skills'>; owner: PublicUser | null } | null
|
||||
if (!result?.skill) return
|
||||
|
||||
const payload: WebhookSkillPayload = {
|
||||
@@ -264,7 +273,7 @@ async function schedulePublishWebhook(
|
||||
summary: result.skill.summary ?? undefined,
|
||||
version: params.version,
|
||||
ownerHandle: result.owner?.handle ?? result.owner?.name ?? undefined,
|
||||
batch: result.skill.batch ?? undefined,
|
||||
highlighted: isSkillHighlighted(result.skill),
|
||||
tags: Object.keys(result.skill.tags ?? {}),
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
parseArk,
|
||||
type SkillInstallSpec,
|
||||
TEXT_FILE_EXTENSION_SET,
|
||||
} from 'clawdhub-schema'
|
||||
} from 'clawhub-schema'
|
||||
import { parse as parseYaml } from 'yaml'
|
||||
|
||||
export type ParsedSkillFrontmatter = Record<string, unknown>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ConvexError } from 'convex/values'
|
||||
import semver from 'semver'
|
||||
import { api, internal } from '../_generated/api'
|
||||
import { internal } from '../_generated/api'
|
||||
import type { Doc, Id } from '../_generated/dataModel'
|
||||
import type { ActionCtx } from '../_generated/server'
|
||||
import { generateEmbedding } from './embeddings'
|
||||
@@ -171,7 +171,9 @@ export async function publishSoulVersionForUser(
|
||||
embedding,
|
||||
})) as PublishResult
|
||||
|
||||
const owner = (await ctx.runQuery(api.users.getById, { userId })) as Doc<'users'> | null
|
||||
const owner = (await ctx.runQuery(internal.users.getByIdInternal, {
|
||||
userId,
|
||||
})) as Doc<'users'> | null
|
||||
const ownerHandle = owner?.handle ?? owner?.name ?? userId
|
||||
|
||||
void ctx.scheduler
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('webhook config', () => {
|
||||
delete process.env.SITE_URL
|
||||
process.env.DISCORD_WEBHOOK_URL = 'https://example.com'
|
||||
const config = getWebhookConfig()
|
||||
expect(config.siteUrl).toBe('https://clawdhub.com')
|
||||
expect(config.siteUrl).toBe('https://clawhub.ai')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -36,11 +36,11 @@ describe('webhook filtering', () => {
|
||||
const config = {
|
||||
url: 'https://example.com',
|
||||
highlightedOnly: true,
|
||||
siteUrl: 'https://clawdhub.com',
|
||||
siteUrl: 'https://clawhub.ai',
|
||||
}
|
||||
const allowed = shouldSendWebhook(
|
||||
'skill.publish',
|
||||
{ slug: 'demo', displayName: 'Demo', batch: 'latest' },
|
||||
{ slug: 'demo', displayName: 'Demo', highlighted: false },
|
||||
config,
|
||||
)
|
||||
expect(allowed).toBe(false)
|
||||
@@ -50,11 +50,11 @@ describe('webhook filtering', () => {
|
||||
const config = {
|
||||
url: 'https://example.com',
|
||||
highlightedOnly: true,
|
||||
siteUrl: 'https://clawdhub.com',
|
||||
siteUrl: 'https://clawhub.ai',
|
||||
}
|
||||
const allowed = shouldSendWebhook(
|
||||
'skill.highlighted',
|
||||
{ slug: 'demo', displayName: 'Demo', batch: 'latest' },
|
||||
{ slug: 'demo', displayName: 'Demo', highlighted: true },
|
||||
config,
|
||||
)
|
||||
expect(allowed).toBe(true)
|
||||
@@ -65,9 +65,9 @@ describe('payload building', () => {
|
||||
it('builds canonical url with owner', () => {
|
||||
const url = buildSkillUrl(
|
||||
{ slug: 'beeper', displayName: 'Beeper', ownerHandle: 'KrauseFx' },
|
||||
'https://clawdhub.com',
|
||||
'https://clawhub.ai',
|
||||
)
|
||||
expect(url).toBe('https://clawdhub.com/KrauseFx/beeper')
|
||||
expect(url).toBe('https://clawhub.ai/KrauseFx/beeper')
|
||||
})
|
||||
|
||||
it('builds a publish embed', () => {
|
||||
@@ -81,7 +81,7 @@ describe('payload building', () => {
|
||||
ownerHandle: 'steipete',
|
||||
tags: ['latest', 'discord'],
|
||||
},
|
||||
{ url: 'https://example.com', highlightedOnly: false, siteUrl: 'https://clawdhub.com' },
|
||||
{ url: 'https://example.com', highlightedOnly: false, siteUrl: 'https://clawhub.ai' },
|
||||
)
|
||||
const embed = payload.embeds[0]
|
||||
expect(embed.title).toBe('Demo Skill')
|
||||
|
||||
@@ -6,7 +6,7 @@ export type WebhookSkillPayload = {
|
||||
summary?: string
|
||||
version?: string
|
||||
ownerHandle?: string
|
||||
batch?: string
|
||||
highlighted?: boolean
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export type WebhookConfig = {
|
||||
siteUrl: string
|
||||
}
|
||||
|
||||
const DEFAULT_SITE_URL = 'https://clawdhub.com'
|
||||
const DEFAULT_SITE_URL = 'https://clawhub.ai'
|
||||
|
||||
export function getWebhookConfig(env: NodeJS.ProcessEnv = process.env): WebhookConfig {
|
||||
const url = env.DISCORD_WEBHOOK_URL?.trim() || null
|
||||
@@ -33,7 +33,7 @@ export function shouldSendWebhook(
|
||||
if (!config.url) return false
|
||||
if (!config.highlightedOnly) return true
|
||||
if (event === 'skill.highlighted') return true
|
||||
return skill.batch === 'highlighted'
|
||||
return Boolean(skill.highlighted)
|
||||
}
|
||||
|
||||
export function buildDiscordPayload(
|
||||
@@ -72,7 +72,7 @@ export function buildDiscordPayload(
|
||||
},
|
||||
],
|
||||
footer: {
|
||||
text: 'ClawdHub',
|
||||
text: 'OpenClaw',
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
@@ -89,9 +89,9 @@ export function buildSkillUrl(skill: WebhookSkillPayload, siteUrl: string) {
|
||||
function buildDescription(event: WebhookEvent, skill: WebhookSkillPayload) {
|
||||
const summary = (skill.summary ?? '').trim()
|
||||
if (summary) return truncate(summary, 200)
|
||||
if (event === 'skill.highlighted') return 'Newly highlighted skill on ClawdHub.'
|
||||
if (skill.version) return `New version v${skill.version} published on ClawdHub.`
|
||||
return 'New skill published on ClawdHub.'
|
||||
if (event === 'skill.highlighted') return 'Newly highlighted skill on OpenClaw.'
|
||||
if (skill.version) return `New version v${skill.version} published on OpenClaw.`
|
||||
return 'New skill published on OpenClaw.'
|
||||
}
|
||||
|
||||
function parseBoolean(value?: string) {
|
||||
|
||||
@@ -265,6 +265,34 @@ type FingerprintBackfillPageResult = {
|
||||
isDone: boolean
|
||||
}
|
||||
|
||||
type BadgeBackfillStats = {
|
||||
skillsScanned: number
|
||||
skillsPatched: number
|
||||
highlightsPatched: number
|
||||
}
|
||||
|
||||
type SkillBadgeTableBackfillStats = {
|
||||
skillsScanned: number
|
||||
recordsInserted: number
|
||||
}
|
||||
|
||||
type BadgeBackfillPageItem = {
|
||||
skillId: Id<'skills'>
|
||||
ownerUserId: Id<'users'>
|
||||
createdAt?: number
|
||||
updatedAt?: number
|
||||
batch?: string
|
||||
badges?: Doc<'skills'>['badges']
|
||||
}
|
||||
|
||||
type BadgeBackfillPageResult = {
|
||||
items: BadgeBackfillPageItem[]
|
||||
cursor: string | null
|
||||
isDone: boolean
|
||||
}
|
||||
|
||||
type BadgeKind = Doc<'skillBadges'>['kind']
|
||||
|
||||
export const getSkillFingerprintBackfillPageInternal = internalQuery({
|
||||
args: {
|
||||
cursor: v.optional(v.string()),
|
||||
@@ -467,6 +495,344 @@ export const scheduleBackfillSkillFingerprints: ReturnType<typeof action> = acti
|
||||
},
|
||||
})
|
||||
|
||||
export const getSkillBadgeBackfillPageInternal = internalQuery({
|
||||
args: {
|
||||
cursor: v.optional(v.string()),
|
||||
batchSize: v.optional(v.number()),
|
||||
},
|
||||
handler: async (ctx, args): Promise<BadgeBackfillPageResult> => {
|
||||
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: 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,
|
||||
}))
|
||||
|
||||
return { items, cursor: continueCursor, isDone }
|
||||
},
|
||||
})
|
||||
|
||||
export const applySkillBadgeBackfillPatchInternal = 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'),
|
||||
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) => {
|
||||
const existing = await ctx.db
|
||||
.query('skillBadges')
|
||||
.withIndex('by_skill_kind', (q) => q.eq('skillId', args.skillId).eq('kind', args.kind))
|
||||
.unique()
|
||||
if (existing) return { inserted: false as const }
|
||||
await ctx.db.insert('skillBadges', {
|
||||
skillId: args.skillId,
|
||||
kind: args.kind,
|
||||
byUserId: args.byUserId,
|
||||
at: args.at,
|
||||
})
|
||||
return { inserted: true as const }
|
||||
},
|
||||
})
|
||||
|
||||
export type BadgeBackfillActionArgs = {
|
||||
dryRun?: boolean
|
||||
batchSize?: number
|
||||
maxBatches?: number
|
||||
}
|
||||
|
||||
export type BadgeBackfillActionResult = { ok: true; stats: BadgeBackfillStats }
|
||||
|
||||
export async function backfillSkillBadgesInternalHandler(
|
||||
ctx: ActionCtx,
|
||||
args: BadgeBackfillActionArgs,
|
||||
): Promise<BadgeBackfillActionResult> {
|
||||
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 = {
|
||||
skillsScanned: 0,
|
||||
skillsPatched: 0,
|
||||
highlightsPatched: 0,
|
||||
}
|
||||
|
||||
let cursor: string | null = null
|
||||
let isDone = false
|
||||
|
||||
for (let i = 0; i < maxBatches; i++) {
|
||||
const page = (await ctx.runQuery(internal.maintenance.getSkillBadgeBackfillPageInternal, {
|
||||
cursor: cursor ?? undefined,
|
||||
batchSize,
|
||||
})) as BadgeBackfillPageResult
|
||||
|
||||
cursor = page.cursor
|
||||
isDone = page.isDone
|
||||
|
||||
for (const item of page.items) {
|
||||
totals.skillsScanned++
|
||||
|
||||
const shouldHighlight = item.batch === 'highlighted' && !item.badges?.highlighted
|
||||
if (!shouldHighlight) continue
|
||||
|
||||
totals.skillsPatched++
|
||||
totals.highlightsPatched++
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (isDone) break
|
||||
}
|
||||
|
||||
if (!isDone) {
|
||||
throw new ConvexError('Backfill incomplete (maxBatches reached)')
|
||||
}
|
||||
|
||||
return { ok: true as const, stats: totals }
|
||||
}
|
||||
|
||||
export const backfillSkillBadgesInternal = internalAction({
|
||||
args: {
|
||||
dryRun: v.optional(v.boolean()),
|
||||
batchSize: v.optional(v.number()),
|
||||
maxBatches: v.optional(v.number()),
|
||||
},
|
||||
handler: backfillSkillBadgesInternalHandler,
|
||||
})
|
||||
|
||||
export const backfillSkillBadges: ReturnType<typeof action> = action({
|
||||
args: {
|
||||
dryRun: v.optional(v.boolean()),
|
||||
batchSize: v.optional(v.number()),
|
||||
maxBatches: v.optional(v.number()),
|
||||
},
|
||||
handler: async (ctx, args): Promise<BadgeBackfillActionResult> => {
|
||||
const { user } = await requireUserFromAction(ctx)
|
||||
assertRole(user, ['admin'])
|
||||
return ctx.runAction(
|
||||
internal.maintenance.backfillSkillBadgesInternal,
|
||||
args,
|
||||
) as Promise<BadgeBackfillActionResult>
|
||||
},
|
||||
})
|
||||
|
||||
export const scheduleBackfillSkillBadges: ReturnType<typeof action> = 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, {
|
||||
dryRun: Boolean(args.dryRun),
|
||||
batchSize: DEFAULT_BATCH_SIZE,
|
||||
maxBatches: DEFAULT_MAX_BATCHES,
|
||||
})
|
||||
return { ok: true as const }
|
||||
},
|
||||
})
|
||||
|
||||
export type SkillBadgeTableBackfillActionResult = {
|
||||
ok: true
|
||||
stats: SkillBadgeTableBackfillStats
|
||||
}
|
||||
|
||||
export async function backfillSkillBadgeTableInternalHandler(
|
||||
ctx: ActionCtx,
|
||||
args: BadgeBackfillActionArgs,
|
||||
): Promise<SkillBadgeTableBackfillActionResult> {
|
||||
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 = {
|
||||
skillsScanned: 0,
|
||||
recordsInserted: 0,
|
||||
}
|
||||
|
||||
let cursor: string | null = null
|
||||
let isDone = false
|
||||
|
||||
for (let i = 0; i < maxBatches; i++) {
|
||||
const page = (await ctx.runQuery(internal.maintenance.getSkillBadgeBackfillPageInternal, {
|
||||
cursor: cursor ?? undefined,
|
||||
batchSize,
|
||||
})) as BadgeBackfillPageResult
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
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++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isDone) break
|
||||
}
|
||||
|
||||
if (!isDone) {
|
||||
throw new ConvexError('Backfill incomplete (maxBatches reached)')
|
||||
}
|
||||
|
||||
return { ok: true as const, stats: totals }
|
||||
}
|
||||
|
||||
export const backfillSkillBadgeTableInternal = internalAction({
|
||||
args: {
|
||||
dryRun: v.optional(v.boolean()),
|
||||
batchSize: v.optional(v.number()),
|
||||
maxBatches: v.optional(v.number()),
|
||||
},
|
||||
handler: backfillSkillBadgeTableInternalHandler,
|
||||
})
|
||||
|
||||
export const backfillSkillBadgeTable: ReturnType<typeof action> = action({
|
||||
args: {
|
||||
dryRun: v.optional(v.boolean()),
|
||||
batchSize: v.optional(v.number()),
|
||||
maxBatches: v.optional(v.number()),
|
||||
},
|
||||
handler: async (ctx, args): Promise<SkillBadgeTableBackfillActionResult> => {
|
||||
const { user } = await requireUserFromAction(ctx)
|
||||
assertRole(user, ['admin'])
|
||||
return ctx.runAction(
|
||||
internal.maintenance.backfillSkillBadgeTableInternal,
|
||||
args,
|
||||
) as Promise<SkillBadgeTableBackfillActionResult>
|
||||
},
|
||||
})
|
||||
|
||||
export const scheduleBackfillSkillBadgeTable: ReturnType<typeof action> = 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, {
|
||||
dryRun: Boolean(args.dryRun),
|
||||
batchSize: DEFAULT_BATCH_SIZE,
|
||||
maxBatches: DEFAULT_MAX_BATCHES,
|
||||
})
|
||||
return { ok: true as const }
|
||||
},
|
||||
})
|
||||
|
||||
function clampInt(value: number, min: number, max: number) {
|
||||
const rounded = Math.trunc(value)
|
||||
if (!Number.isFinite(rounded)) return min
|
||||
|
||||
+94
-1
@@ -3,6 +3,8 @@ import { defineSchema, defineTable } from 'convex/server'
|
||||
import { v } from 'convex/values'
|
||||
import { EMBEDDING_DIMENSIONS } from './lib/embeddings'
|
||||
|
||||
const authSchema = authTables as unknown as Record<string, ReturnType<typeof defineTable>>
|
||||
|
||||
const users = defineTable({
|
||||
name: v.optional(v.string()),
|
||||
image: v.optional(v.string()),
|
||||
@@ -47,7 +49,36 @@ const skills = defineTable({
|
||||
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()),
|
||||
@@ -72,6 +103,7 @@ const skills = defineTable({
|
||||
.index('by_stats_installs_current', ['statsInstallsCurrent', 'updatedAt'])
|
||||
.index('by_stats_installs_all_time', ['statsInstallsAllTime', 'updatedAt'])
|
||||
.index('by_batch', ['batch'])
|
||||
.index('by_active_updated', ['softDeletedAt', 'updatedAt'])
|
||||
|
||||
const souls = defineTable({
|
||||
slug: v.string(),
|
||||
@@ -113,6 +145,7 @@ const skillVersions = defineTable({
|
||||
frontmatter: v.record(v.string(), v.any()),
|
||||
metadata: v.optional(v.any()),
|
||||
clawdis: v.optional(v.any()),
|
||||
moltbot: v.optional(v.any()),
|
||||
}),
|
||||
createdBy: v.id('users'),
|
||||
createdAt: v.number(),
|
||||
@@ -139,6 +172,8 @@ const soulVersions = defineTable({
|
||||
parsed: v.object({
|
||||
frontmatter: v.record(v.string(), v.any()),
|
||||
metadata: v.optional(v.any()),
|
||||
clawdis: v.optional(v.any()),
|
||||
moltbot: v.optional(v.any()),
|
||||
}),
|
||||
createdBy: v.id('users'),
|
||||
createdAt: v.number(),
|
||||
@@ -157,6 +192,21 @@ const skillVersionFingerprints = defineTable({
|
||||
.index('by_fingerprint', ['fingerprint'])
|
||||
.index('by_skill_fingerprint', ['skillId', 'fingerprint'])
|
||||
|
||||
const skillBadges = defineTable({
|
||||
skillId: v.id('skills'),
|
||||
kind: v.union(
|
||||
v.literal('highlighted'),
|
||||
v.literal('official'),
|
||||
v.literal('deprecated'),
|
||||
v.literal('redactionApproved'),
|
||||
),
|
||||
byUserId: v.id('users'),
|
||||
at: v.number(),
|
||||
})
|
||||
.index('by_skill', ['skillId'])
|
||||
.index('by_skill_kind', ['skillId', 'kind'])
|
||||
.index('by_kind_at', ['kind', 'at'])
|
||||
|
||||
const soulVersionFingerprints = defineTable({
|
||||
soulId: v.id('souls'),
|
||||
versionId: v.id('soulVersions'),
|
||||
@@ -217,6 +267,35 @@ const skillStatBackfillState = defineTable({
|
||||
updatedAt: v.number(),
|
||||
}).index('by_key', ['key'])
|
||||
|
||||
const skillStatEvents = defineTable({
|
||||
skillId: v.id('skills'),
|
||||
kind: v.union(
|
||||
v.literal('download'),
|
||||
v.literal('star'),
|
||||
v.literal('unstar'),
|
||||
v.literal('install_new'),
|
||||
v.literal('install_reactivate'),
|
||||
v.literal('install_deactivate'),
|
||||
v.literal('install_clear'),
|
||||
),
|
||||
delta: v.optional(
|
||||
v.object({
|
||||
allTime: v.number(),
|
||||
current: v.number(),
|
||||
}),
|
||||
),
|
||||
occurredAt: v.number(),
|
||||
processedAt: v.optional(v.number()),
|
||||
})
|
||||
.index('by_unprocessed', ['processedAt'])
|
||||
.index('by_skill', ['skillId'])
|
||||
|
||||
const skillStatUpdateCursors = defineTable({
|
||||
key: v.string(),
|
||||
cursorCreationTime: v.optional(v.number()),
|
||||
updatedAt: v.number(),
|
||||
}).index('by_key', ['key'])
|
||||
|
||||
const soulEmbeddings = defineTable({
|
||||
soulId: v.id('souls'),
|
||||
versionId: v.id('soulVersions'),
|
||||
@@ -246,6 +325,16 @@ const comments = defineTable({
|
||||
.index('by_skill', ['skillId'])
|
||||
.index('by_user', ['userId'])
|
||||
|
||||
const skillReports = defineTable({
|
||||
skillId: v.id('skills'),
|
||||
userId: v.id('users'),
|
||||
reason: v.optional(v.string()),
|
||||
createdAt: v.number(),
|
||||
})
|
||||
.index('by_skill', ['skillId'])
|
||||
.index('by_user', ['userId'])
|
||||
.index('by_skill_user', ['skillId', 'userId'])
|
||||
|
||||
const soulComments = defineTable({
|
||||
soulId: v.id('souls'),
|
||||
userId: v.id('users'),
|
||||
@@ -353,20 +442,24 @@ const userSkillRootInstalls = defineTable({
|
||||
.index('by_skill', ['skillId'])
|
||||
|
||||
export default defineSchema({
|
||||
...authTables,
|
||||
...authSchema,
|
||||
users,
|
||||
skills,
|
||||
souls,
|
||||
skillVersions,
|
||||
soulVersions,
|
||||
skillVersionFingerprints,
|
||||
skillBadges,
|
||||
soulVersionFingerprints,
|
||||
skillEmbeddings,
|
||||
soulEmbeddings,
|
||||
skillDailyStats,
|
||||
skillLeaderboards,
|
||||
skillStatBackfillState,
|
||||
skillStatEvents,
|
||||
skillStatUpdateCursors,
|
||||
comments,
|
||||
skillReports,
|
||||
soulComments,
|
||||
stars,
|
||||
soulStars,
|
||||
|
||||
+56
-18
@@ -2,12 +2,14 @@ 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 { generateEmbedding } from './lib/embeddings'
|
||||
import { toPublicSkill, toPublicSoul } from './lib/public'
|
||||
import { matchesExactTokens, tokenize } from './lib/searchText'
|
||||
|
||||
type HydratedEntry = {
|
||||
embeddingId: Id<'skillEmbeddings'>
|
||||
skill: Doc<'skills'> | null
|
||||
skill: NonNullable<ReturnType<typeof toPublicSkill>>
|
||||
version: Doc<'skillVersions'> | null
|
||||
ownerHandle: string | null
|
||||
}
|
||||
@@ -38,8 +40,9 @@ export const searchSkills: ReturnType<typeof action> = action({
|
||||
return []
|
||||
}
|
||||
const limit = args.limit ?? 10
|
||||
const maxCandidate = Math.min(Math.max(limit * 10, 200), 1000)
|
||||
let candidateLimit = Math.max(limit * 3, 50)
|
||||
// Convex vectorSearch max limit is 256; clamp candidate sizes accordingly.
|
||||
const maxCandidate = Math.min(Math.max(limit * 10, 200), 256)
|
||||
let candidateLimit = Math.min(Math.max(limit * 3, 50), 256)
|
||||
let hydrated: HydratedEntry[] = []
|
||||
let scoreById = new Map<Id<'skillEmbeddings'>, number>()
|
||||
let exactMatches: HydratedEntry[] = []
|
||||
@@ -59,15 +62,27 @@ export const searchSkills: ReturnType<typeof action> = 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 hydratedWithBadges = hydrated.map((entry) => ({
|
||||
...entry,
|
||||
skill: {
|
||||
...entry.skill,
|
||||
badges: badgeMapBySkillId.get(entry.skill._id) ?? {},
|
||||
},
|
||||
}))
|
||||
|
||||
const filtered = args.highlightedOnly
|
||||
? hydrated.filter((entry) => entry.skill?.batch === 'highlighted')
|
||||
: hydrated
|
||||
? hydratedWithBadges.filter((entry) => isSkillHighlighted(entry.skill))
|
||||
: hydratedWithBadges
|
||||
|
||||
exactMatches = filtered.filter((entry) =>
|
||||
matchesExactTokens(queryTokens, [
|
||||
entry.skill?.displayName,
|
||||
entry.skill?.slug,
|
||||
entry.skill?.summary,
|
||||
entry.skill.displayName,
|
||||
entry.skill.slug,
|
||||
entry.skill.summary,
|
||||
]),
|
||||
)
|
||||
|
||||
@@ -90,6 +105,14 @@ export const searchSkills: ReturnType<typeof action> = action({
|
||||
},
|
||||
})
|
||||
|
||||
export const getBadgeMapsForSkills = internalQuery({
|
||||
args: { skillIds: v.array(v.id('skills')) },
|
||||
handler: async (ctx, args): Promise<Array<[Id<'skills'>, SkillBadgeMap]>> => {
|
||||
const badgeMap = await getSkillBadgeMaps(ctx, args.skillIds)
|
||||
return Array.from(badgeMap.entries())
|
||||
},
|
||||
})
|
||||
|
||||
export const hydrateResults = internalQuery({
|
||||
args: { embeddingIds: v.array(v.id('skillEmbeddings')) },
|
||||
handler: async (ctx, args): Promise<HydratedEntry[]> => {
|
||||
@@ -98,7 +121,9 @@ export const hydrateResults = internalQuery({
|
||||
const getOwnerHandle = (ownerUserId: Id<'users'>) => {
|
||||
const cached = ownerHandleCache.get(ownerUserId)
|
||||
if (cached) return cached
|
||||
const handlePromise = ctx.db.get(ownerUserId).then((owner) => owner?.handle ?? owner?._id ?? null)
|
||||
const handlePromise = ctx.db
|
||||
.get(ownerUserId)
|
||||
.then((owner) => owner?.handle ?? owner?._id ?? null)
|
||||
ownerHandleCache.set(ownerUserId, handlePromise)
|
||||
return handlePromise
|
||||
}
|
||||
@@ -108,12 +133,14 @@ export const hydrateResults = internalQuery({
|
||||
const embedding = await ctx.db.get(embeddingId)
|
||||
if (!embedding) return null
|
||||
const skill = await ctx.db.get(embedding.skillId)
|
||||
if (skill?.softDeletedAt) return null
|
||||
if (!skill || skill.softDeletedAt) return null
|
||||
const [version, ownerHandle] = await Promise.all([
|
||||
ctx.db.get(embedding.versionId),
|
||||
getOwnerHandle(skill.ownerUserId),
|
||||
])
|
||||
return { embeddingId, skill, version, ownerHandle }
|
||||
const publicSkill = toPublicSkill(skill)
|
||||
if (!publicSkill) return null
|
||||
return { embeddingId, skill: publicSkill, version, ownerHandle }
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -123,7 +150,7 @@ export const hydrateResults = internalQuery({
|
||||
|
||||
type HydratedSoulEntry = {
|
||||
embeddingId: Id<'soulEmbeddings'>
|
||||
soul: Doc<'souls'> | null
|
||||
soul: NonNullable<ReturnType<typeof toPublicSoul>>
|
||||
version: Doc<'soulVersions'> | null
|
||||
}
|
||||
|
||||
@@ -147,8 +174,9 @@ export const searchSouls: ReturnType<typeof action> = action({
|
||||
return []
|
||||
}
|
||||
const limit = args.limit ?? 10
|
||||
const maxCandidate = Math.min(Math.max(limit * 10, 200), 1000)
|
||||
let candidateLimit = Math.max(limit * 3, 50)
|
||||
// Convex vectorSearch max limit is 256; clamp candidate sizes accordingly.
|
||||
const maxCandidate = Math.min(Math.max(limit * 10, 200), 256)
|
||||
let candidateLimit = Math.min(Math.max(limit * 3, 50), 256)
|
||||
let hydrated: HydratedSoulEntry[] = []
|
||||
let scoreById = new Map<Id<'soulEmbeddings'>, number>()
|
||||
let exactMatches: HydratedSoulEntry[] = []
|
||||
@@ -170,9 +198,9 @@ export const searchSouls: ReturnType<typeof action> = action({
|
||||
|
||||
exactMatches = hydrated.filter((entry) =>
|
||||
matchesExactTokens(queryTokens, [
|
||||
entry.soul?.displayName,
|
||||
entry.soul?.slug,
|
||||
entry.soul?.summary,
|
||||
entry.soul.displayName,
|
||||
entry.soul.slug,
|
||||
entry.soul.summary,
|
||||
]),
|
||||
)
|
||||
|
||||
@@ -206,11 +234,21 @@ export const hydrateSoulResults = internalQuery({
|
||||
const soul = await ctx.db.get(embedding.soulId)
|
||||
if (soul?.softDeletedAt) continue
|
||||
const version = await ctx.db.get(embedding.versionId)
|
||||
entries.push({ embeddingId, soul, version })
|
||||
const publicSoul = toPublicSoul(soul)
|
||||
if (!publicSoul) continue
|
||||
entries.push({ embeddingId, soul: publicSoul, version })
|
||||
}
|
||||
|
||||
return entries
|
||||
},
|
||||
})
|
||||
|
||||
export const getSkillBadgeMapsInternal = internalQuery({
|
||||
args: { skillIds: v.array(v.id('skills')) },
|
||||
handler: async (ctx, args) => {
|
||||
const badgeMap = await getSkillBadgeMaps(ctx, args.skillIds)
|
||||
return Array.from(badgeMap.entries())
|
||||
},
|
||||
})
|
||||
|
||||
export const __test = { getNextCandidateLimit }
|
||||
|
||||
+2
-1
@@ -242,7 +242,8 @@ export const ensureSeedUserInternal = internalMutation({
|
||||
})
|
||||
|
||||
async function sha256Hex(bytes: Uint8Array) {
|
||||
const digest = await crypto.subtle.digest('SHA-256', bytes)
|
||||
const data = new Uint8Array(bytes)
|
||||
const digest = await crypto.subtle.digest('SHA-256', data)
|
||||
return toHex(new Uint8Array(digest))
|
||||
}
|
||||
|
||||
|
||||
+3
-3
File diff suppressed because one or more lines are too long
@@ -0,0 +1,568 @@
|
||||
/**
|
||||
* Skill Stat Events - Event-sourced stats processing for skills
|
||||
*
|
||||
* Instead of updating skill stats synchronously in the hot path (which can cause
|
||||
* contention when multiple users download/star/install the same skill), we insert
|
||||
* lightweight event records and process them in batches via a cron job.
|
||||
*
|
||||
* Flow:
|
||||
* 1. User action (download, star, install) → insertStatEvent() writes to skillStatEvents table
|
||||
* 2. Cron job runs every 5 minutes → processSkillStatEventsInternal() processes batches
|
||||
* 3. Events are aggregated per-skill to minimize database operations
|
||||
* 4. Stats are applied to skill documents and daily stats tables
|
||||
* 5. Events are marked as processed (kept forever for auditing)
|
||||
*/
|
||||
|
||||
import { v } from 'convex/values'
|
||||
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 { applySkillStatDeltas, bumpDailySkillStats } from './lib/skillStats'
|
||||
|
||||
/**
|
||||
* Event types that affect skill stats:
|
||||
*
|
||||
* - download: User downloaded skill as zip (+1 downloads)
|
||||
* - star: User starred the skill (+1 stars)
|
||||
* - unstar: User removed their star (-1 stars)
|
||||
* - install_new: First time this user installed this skill (+1 installsAllTime, +1 installsCurrent)
|
||||
* - install_reactivate: User re-added skill after removing it (+1 installsCurrent only)
|
||||
* - install_deactivate: User removed skill from all projects (-1 installsCurrent)
|
||||
* - install_clear: User cleared all telemetry data (custom delta for both allTime and current)
|
||||
*/
|
||||
export type StatEventKind =
|
||||
| 'download'
|
||||
| 'star'
|
||||
| 'unstar'
|
||||
| 'install_new'
|
||||
| 'install_reactivate'
|
||||
| 'install_deactivate'
|
||||
| 'install_clear'
|
||||
|
||||
/**
|
||||
* Insert a stat event to be processed later by the cron job.
|
||||
*
|
||||
* This is called from the hot path (downloads, stars, telemetry) instead of
|
||||
* directly updating skill stats. It's a single insert with no read-modify-write
|
||||
* cycle, so it's fast and doesn't contend with other operations on the same skill.
|
||||
*
|
||||
* @param ctx - Mutation context
|
||||
* @param params.skillId - The skill being affected
|
||||
* @param params.kind - Type of event (download, star, install_new, etc.)
|
||||
* @param params.occurredAt - When the event happened (defaults to now). Important for
|
||||
* daily stats bucketing - we want downloads at 11:55 PM Monday
|
||||
* to count toward Monday's stats even if processed on Tuesday.
|
||||
* @param params.delta - Only used for install_clear events, specifies exact delta amounts
|
||||
*/
|
||||
export async function insertStatEvent(
|
||||
ctx: MutationCtx,
|
||||
params: {
|
||||
skillId: Id<'skills'>
|
||||
kind: StatEventKind
|
||||
occurredAt?: number
|
||||
delta?: { allTime: number; current: number }
|
||||
},
|
||||
) {
|
||||
await ctx.db.insert('skillStatEvents', {
|
||||
skillId: params.skillId,
|
||||
kind: params.kind,
|
||||
delta: params.delta,
|
||||
occurredAt: params.occurredAt ?? Date.now(),
|
||||
processedAt: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregated deltas for a single skill after processing multiple events.
|
||||
*
|
||||
* When we process a batch of 100 events, many might be for the same skill.
|
||||
* Instead of updating the skill document once per event, we aggregate all
|
||||
* events for each skill and apply a single update.
|
||||
*
|
||||
* The downloadEvents and installNewEvents arrays store the original timestamps
|
||||
* so we can update daily stats with the correct day bucket for each event.
|
||||
*/
|
||||
type AggregatedDeltas = {
|
||||
downloads: number
|
||||
stars: number
|
||||
installsAllTime: number
|
||||
installsCurrent: number
|
||||
/** Original timestamps for each download event (for daily stats bucketing) */
|
||||
downloadEvents: number[]
|
||||
/** Original timestamps for each new install event (for daily stats bucketing) */
|
||||
installNewEvents: number[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregate multiple events for a single skill into net deltas.
|
||||
*
|
||||
* Example: If a skill has these events in the batch:
|
||||
* - download (Mon 11pm)
|
||||
* - download (Tue 1am)
|
||||
* - star
|
||||
* - unstar
|
||||
* - star
|
||||
*
|
||||
* The result would be:
|
||||
* - downloads: 2
|
||||
* - stars: 1 (net: +1 -1 +1 = +1)
|
||||
* - downloadEvents: [<Mon 11pm timestamp>, <Tue 1am timestamp>]
|
||||
*
|
||||
* This aggregation reduces the number of database operations from N events
|
||||
* to 1 skill update + N daily stat updates (which themselves may coalesce
|
||||
* if multiple events fall on the same day).
|
||||
*/
|
||||
function aggregateEvents(events: Doc<'skillStatEvents'>[]): AggregatedDeltas {
|
||||
const result: AggregatedDeltas = {
|
||||
downloads: 0,
|
||||
stars: 0,
|
||||
installsAllTime: 0,
|
||||
installsCurrent: 0,
|
||||
downloadEvents: [],
|
||||
installNewEvents: [],
|
||||
}
|
||||
|
||||
for (const event of events) {
|
||||
switch (event.kind) {
|
||||
case 'download':
|
||||
result.downloads += 1
|
||||
result.downloadEvents.push(event.occurredAt)
|
||||
break
|
||||
case 'star':
|
||||
result.stars += 1
|
||||
break
|
||||
case 'unstar':
|
||||
result.stars -= 1
|
||||
break
|
||||
case 'install_new':
|
||||
// New user installing for the first time: count toward both lifetime and current
|
||||
result.installsAllTime += 1
|
||||
result.installsCurrent += 1
|
||||
result.installNewEvents.push(event.occurredAt)
|
||||
break
|
||||
case 'install_reactivate':
|
||||
// User re-added skill after removing: only affects current count
|
||||
result.installsCurrent += 1
|
||||
break
|
||||
case 'install_deactivate':
|
||||
// User removed skill from all projects: only affects current count
|
||||
result.installsCurrent -= 1
|
||||
break
|
||||
case 'install_clear':
|
||||
// User cleared telemetry: uses custom delta values (typically negative)
|
||||
if (event.delta) {
|
||||
result.installsAllTime += event.delta.allTime
|
||||
result.installsCurrent += event.delta.current
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a batch of unprocessed stat events.
|
||||
*
|
||||
* Called by cron every 5 minutes. Processes up to batchSize events (default 100).
|
||||
* If the batch is full, schedules an immediate follow-up run to drain the queue.
|
||||
*
|
||||
* Processing steps:
|
||||
* 1. Query unprocessed events (processedAt is undefined)
|
||||
* 2. Group events by skillId to minimize skill document fetches
|
||||
* 3. For each skill:
|
||||
* a. Fetch the skill document once
|
||||
* b. Aggregate all events for this skill into net deltas
|
||||
* c. Apply deltas to skill stats (downloads, stars, installs)
|
||||
* d. Update daily stats for trending (using original event timestamps)
|
||||
* e. Mark all events as processed
|
||||
* 4. If batch was full, schedule another run immediately
|
||||
*
|
||||
* Aggregation levels:
|
||||
* - Level 1: Batch of 100 events from the queue
|
||||
* - Level 2: Group by skillId (e.g., 100 events → 30 unique skills)
|
||||
* - Level 3: Aggregate events per skill (e.g., 5 events → 1 skill update)
|
||||
* - Level 4: Daily stats may coalesce (e.g., 3 downloads same day → 1 upsert)
|
||||
*/
|
||||
export const processSkillStatEventsInternal = internalMutation({
|
||||
args: { batchSize: v.optional(v.number()) },
|
||||
handler: async (ctx, args) => {
|
||||
const batchSize = args.batchSize ?? 100
|
||||
const now = Date.now()
|
||||
|
||||
// Level 1: Fetch a batch of unprocessed events
|
||||
const events = await ctx.db
|
||||
.query('skillStatEvents')
|
||||
.withIndex('by_unprocessed', (q) => q.eq('processedAt', undefined))
|
||||
.take(batchSize)
|
||||
|
||||
if (events.length === 0) {
|
||||
return { processed: 0 }
|
||||
}
|
||||
|
||||
// Level 2: Group events by skillId to minimize database reads
|
||||
// Instead of fetching the same skill document multiple times,
|
||||
// we fetch it once and process all its events together
|
||||
const eventsBySkill = new Map<Id<'skills'>, Doc<'skillStatEvents'>[]>()
|
||||
for (const event of events) {
|
||||
const existing = eventsBySkill.get(event.skillId) ?? []
|
||||
existing.push(event)
|
||||
eventsBySkill.set(event.skillId, existing)
|
||||
}
|
||||
|
||||
// Process each skill's events
|
||||
for (const [skillId, skillEvents] of eventsBySkill) {
|
||||
const skill = await ctx.db.get(skillId)
|
||||
|
||||
// Skill was deleted - just mark events as processed
|
||||
if (!skill) {
|
||||
for (const event of skillEvents) {
|
||||
await ctx.db.patch(event._id, { processedAt: now })
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Level 3: Aggregate all events for this skill into net deltas
|
||||
// e.g., 3 downloads + 2 stars - 1 unstar → { downloads: 3, stars: 1 }
|
||||
const deltas = aggregateEvents(skillEvents)
|
||||
|
||||
// Apply aggregated deltas to skill stats (single update per skill)
|
||||
if (
|
||||
deltas.downloads !== 0 ||
|
||||
deltas.stars !== 0 ||
|
||||
deltas.installsAllTime !== 0 ||
|
||||
deltas.installsCurrent !== 0
|
||||
) {
|
||||
const patch = applySkillStatDeltas(skill, {
|
||||
downloads: deltas.downloads,
|
||||
stars: deltas.stars,
|
||||
installsAllTime: deltas.installsAllTime,
|
||||
installsCurrent: deltas.installsCurrent,
|
||||
})
|
||||
await ctx.db.patch(skill._id, {
|
||||
...patch,
|
||||
updatedAt: now,
|
||||
})
|
||||
}
|
||||
|
||||
// Update daily stats for trending/leaderboards
|
||||
// We use the ORIGINAL event timestamp (occurredAt) so that:
|
||||
// - A download at Mon 11:55 PM counts toward Monday's stats
|
||||
// - Even if the cron processes it on Tuesday
|
||||
//
|
||||
// Level 4: bumpDailySkillStats does its own coalescing - multiple
|
||||
// events on the same day will update the same daily record
|
||||
for (const occurredAt of deltas.downloadEvents) {
|
||||
await bumpDailySkillStats(ctx, { skillId, now: occurredAt, downloads: 1 })
|
||||
}
|
||||
for (const occurredAt of deltas.installNewEvents) {
|
||||
await bumpDailySkillStats(ctx, { skillId, now: occurredAt, installs: 1 })
|
||||
}
|
||||
|
||||
// Mark all events for this skill as processed
|
||||
for (const event of skillEvents) {
|
||||
await ctx.db.patch(event._id, { processedAt: now })
|
||||
}
|
||||
}
|
||||
|
||||
// If we hit the batch limit, there may be more events waiting.
|
||||
// Schedule an immediate follow-up run to drain the queue.
|
||||
// This ensures high-volume periods don't create a backlog.
|
||||
if (events.length === batchSize) {
|
||||
await ctx.scheduler.runAfter(0, internal.skillStatEvents.processSkillStatEventsInternal, {
|
||||
batchSize,
|
||||
})
|
||||
}
|
||||
|
||||
return { processed: events.length }
|
||||
},
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Action-based processing (cursor-based, runs outside transaction window)
|
||||
// ============================================================================
|
||||
|
||||
const CURSOR_KEY = 'skill_stat_events'
|
||||
const EVENT_BATCH_SIZE = 500
|
||||
const MAX_SKILLS_PER_RUN = 500
|
||||
|
||||
/**
|
||||
* Fetch a batch of events after the given cursor (by _creationTime).
|
||||
* Returns events sorted by _creationTime ascending.
|
||||
*/
|
||||
export const getUnprocessedEventBatch = internalQuery({
|
||||
args: {
|
||||
cursorCreationTime: v.optional(v.number()),
|
||||
limit: v.optional(v.number()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const limit = args.limit ?? EVENT_BATCH_SIZE
|
||||
const cursor = args.cursorCreationTime
|
||||
|
||||
// Query events after the cursor using the built-in creation time index
|
||||
const events = await ctx.db
|
||||
.query('skillStatEvents')
|
||||
.withIndex('by_creation_time', (q) =>
|
||||
cursor !== undefined ? q.gt('_creationTime', cursor) : q,
|
||||
)
|
||||
.take(limit)
|
||||
return events
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Get the current cursor position from the cursors table.
|
||||
*/
|
||||
export const getStatEventCursor = internalQuery({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const cursor = await ctx.db
|
||||
.query('skillStatUpdateCursors')
|
||||
.withIndex('by_key', (q) => q.eq('key', CURSOR_KEY))
|
||||
.unique()
|
||||
return cursor?.cursorCreationTime
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Validator for skill deltas passed to the mutation.
|
||||
*/
|
||||
const skillDeltaValidator = v.object({
|
||||
skillId: v.id('skills'),
|
||||
downloads: v.number(),
|
||||
stars: v.number(),
|
||||
installsAllTime: v.number(),
|
||||
installsCurrent: v.number(),
|
||||
downloadEvents: v.array(v.number()),
|
||||
installNewEvents: v.array(v.number()),
|
||||
})
|
||||
|
||||
/**
|
||||
* Apply aggregated stats to skills and update the cursor.
|
||||
* This is a single atomic mutation that:
|
||||
* 1. Updates all affected skills with their aggregated deltas
|
||||
* 2. Updates daily stats for trending
|
||||
* 3. Advances the cursor to the new position
|
||||
*/
|
||||
export const applyAggregatedStatsAndUpdateCursor = internalMutation({
|
||||
args: {
|
||||
skillDeltas: v.array(skillDeltaValidator),
|
||||
newCursor: v.number(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const now = Date.now()
|
||||
|
||||
// Process each skill's aggregated deltas
|
||||
for (const delta of args.skillDeltas) {
|
||||
const skill = await ctx.db.get(delta.skillId)
|
||||
|
||||
// Skill was deleted - skip
|
||||
if (!skill) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Apply aggregated deltas to skill stats
|
||||
if (
|
||||
delta.downloads !== 0 ||
|
||||
delta.stars !== 0 ||
|
||||
delta.installsAllTime !== 0 ||
|
||||
delta.installsCurrent !== 0
|
||||
) {
|
||||
const patch = applySkillStatDeltas(skill, {
|
||||
downloads: delta.downloads,
|
||||
stars: delta.stars,
|
||||
installsAllTime: delta.installsAllTime,
|
||||
installsCurrent: delta.installsCurrent,
|
||||
})
|
||||
await ctx.db.patch(skill._id, {
|
||||
...patch,
|
||||
updatedAt: now,
|
||||
})
|
||||
}
|
||||
|
||||
// Update daily stats for trending/leaderboards
|
||||
for (const occurredAt of delta.downloadEvents) {
|
||||
await bumpDailySkillStats(ctx, { skillId: delta.skillId, now: occurredAt, downloads: 1 })
|
||||
}
|
||||
for (const occurredAt of delta.installNewEvents) {
|
||||
await bumpDailySkillStats(ctx, { skillId: delta.skillId, now: occurredAt, installs: 1 })
|
||||
}
|
||||
}
|
||||
|
||||
// Update cursor position (upsert)
|
||||
const existingCursor = await ctx.db
|
||||
.query('skillStatUpdateCursors')
|
||||
.withIndex('by_key', (q) => q.eq('key', CURSOR_KEY))
|
||||
.unique()
|
||||
|
||||
if (existingCursor) {
|
||||
await ctx.db.patch(existingCursor._id, {
|
||||
cursorCreationTime: args.newCursor,
|
||||
updatedAt: now,
|
||||
})
|
||||
} else {
|
||||
await ctx.db.insert('skillStatUpdateCursors', {
|
||||
key: CURSOR_KEY,
|
||||
cursorCreationTime: args.newCursor,
|
||||
updatedAt: now,
|
||||
})
|
||||
}
|
||||
|
||||
return { skillsUpdated: args.skillDeltas.length }
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Action that processes skill stat events in batches outside the transaction window.
|
||||
*
|
||||
* Algorithm:
|
||||
* 1. Get current cursor position
|
||||
* 2. Fetch events in batches of 500, aggregating as we go
|
||||
* 3. Stop when we have >= 500 unique skills OR run out of events
|
||||
* 4. Call mutation to apply all deltas and update cursor atomically
|
||||
* 5. Self-schedule if we stopped due to skill limit (not exhaustion)
|
||||
*/
|
||||
export const processSkillStatEventsAction = internalAction({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
// Get current cursor position (convert null to undefined for consistency)
|
||||
const cursorResult = await ctx.runQuery(internal.skillStatEvents.getStatEventCursor)
|
||||
let cursor: number | undefined = cursorResult ?? undefined
|
||||
|
||||
console.log(`[STAT-AGG] Starting aggregation, cursor=${cursor ?? 'none'}`)
|
||||
|
||||
// Aggregated deltas per skill
|
||||
const aggregatedBySkill = new Map<
|
||||
Id<'skills'>,
|
||||
{
|
||||
downloads: number
|
||||
stars: number
|
||||
installsAllTime: number
|
||||
installsCurrent: number
|
||||
downloadEvents: number[]
|
||||
installNewEvents: number[]
|
||||
}
|
||||
>()
|
||||
|
||||
let maxCreationTime: number | undefined = cursor
|
||||
let exhausted = false
|
||||
let totalEventsFetched = 0
|
||||
|
||||
// Fetch and aggregate until we have enough skills or run out of events
|
||||
while (aggregatedBySkill.size < MAX_SKILLS_PER_RUN) {
|
||||
const events = await ctx.runQuery(internal.skillStatEvents.getUnprocessedEventBatch, {
|
||||
cursorCreationTime: cursor,
|
||||
limit: EVENT_BATCH_SIZE,
|
||||
})
|
||||
|
||||
if (events.length === 0) {
|
||||
exhausted = true
|
||||
break
|
||||
}
|
||||
|
||||
totalEventsFetched += events.length
|
||||
const skillsBefore = aggregatedBySkill.size
|
||||
|
||||
// Aggregate events into per-skill deltas
|
||||
for (const event of events) {
|
||||
let skillDelta = aggregatedBySkill.get(event.skillId)
|
||||
if (!skillDelta) {
|
||||
skillDelta = {
|
||||
downloads: 0,
|
||||
stars: 0,
|
||||
installsAllTime: 0,
|
||||
installsCurrent: 0,
|
||||
downloadEvents: [],
|
||||
installNewEvents: [],
|
||||
}
|
||||
aggregatedBySkill.set(event.skillId, skillDelta)
|
||||
}
|
||||
|
||||
// Apply event to aggregated deltas
|
||||
switch (event.kind) {
|
||||
case 'download':
|
||||
skillDelta.downloads += 1
|
||||
skillDelta.downloadEvents.push(event.occurredAt)
|
||||
break
|
||||
case 'star':
|
||||
skillDelta.stars += 1
|
||||
break
|
||||
case 'unstar':
|
||||
skillDelta.stars -= 1
|
||||
break
|
||||
case 'install_new':
|
||||
skillDelta.installsAllTime += 1
|
||||
skillDelta.installsCurrent += 1
|
||||
skillDelta.installNewEvents.push(event.occurredAt)
|
||||
break
|
||||
case 'install_reactivate':
|
||||
skillDelta.installsCurrent += 1
|
||||
break
|
||||
case 'install_deactivate':
|
||||
skillDelta.installsCurrent -= 1
|
||||
break
|
||||
case 'install_clear':
|
||||
if (event.delta) {
|
||||
skillDelta.installsAllTime += event.delta.allTime
|
||||
skillDelta.installsCurrent += event.delta.current
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Track highest _creationTime seen
|
||||
if (maxCreationTime === undefined || event._creationTime > maxCreationTime) {
|
||||
maxCreationTime = event._creationTime
|
||||
}
|
||||
}
|
||||
|
||||
// Update cursor for next batch fetch
|
||||
cursor = events[events.length - 1]._creationTime
|
||||
|
||||
console.log(
|
||||
`[STAT-AGG] Fetched ${events.length} events, ${aggregatedBySkill.size - skillsBefore} new skills (${aggregatedBySkill.size} total)`,
|
||||
)
|
||||
|
||||
// If we got fewer than requested, we've exhausted the events
|
||||
if (events.length < EVENT_BATCH_SIZE) {
|
||||
exhausted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If we have nothing to process, we're done
|
||||
if (aggregatedBySkill.size === 0 || maxCreationTime === undefined) {
|
||||
console.log('[STAT-AGG] No events to process, done')
|
||||
return { processed: 0, skillsUpdated: 0, exhausted: true }
|
||||
}
|
||||
|
||||
// Convert map to array for mutation
|
||||
const skillDeltas = Array.from(aggregatedBySkill.entries()).map(([skillId, delta]) => ({
|
||||
skillId,
|
||||
...delta,
|
||||
}))
|
||||
|
||||
console.log(
|
||||
`[STAT-AGG] Running mutation for ${skillDeltas.length} skills (${totalEventsFetched} total events)`,
|
||||
)
|
||||
|
||||
// Apply all deltas and update cursor atomically
|
||||
await ctx.runMutation(internal.skillStatEvents.applyAggregatedStatsAndUpdateCursor, {
|
||||
skillDeltas,
|
||||
newCursor: maxCreationTime,
|
||||
})
|
||||
|
||||
// Self-schedule if we stopped because of skill limit, not exhaustion
|
||||
if (!exhausted) {
|
||||
console.log('[STAT-AGG] More events remaining, self-scheduling')
|
||||
await ctx.scheduler.runAfter(0, internal.skillStatEvents.processSkillStatEventsAction, {})
|
||||
} else {
|
||||
console.log('[STAT-AGG] All events processed, done')
|
||||
}
|
||||
|
||||
return {
|
||||
skillsUpdated: skillDeltas.length,
|
||||
exhausted,
|
||||
}
|
||||
},
|
||||
})
|
||||
+783
-49
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,8 @@
|
||||
import { v } from 'convex/values'
|
||||
import type { Doc } from './_generated/dataModel'
|
||||
import { mutation, query } from './_generated/server'
|
||||
import { assertRole, requireUser } from './lib/access'
|
||||
import { assertModerator, requireUser } from './lib/access'
|
||||
import { type PublicUser, toPublicUser } from './lib/public'
|
||||
|
||||
export const listBySoul = query({
|
||||
args: { soulId: v.id('souls'), limit: v.optional(v.number()) },
|
||||
@@ -13,10 +14,10 @@ export const listBySoul = query({
|
||||
.order('desc')
|
||||
.take(limit)
|
||||
|
||||
const results: Array<{ comment: Doc<'soulComments'>; user: Doc<'users'> | null }> = []
|
||||
const results: Array<{ comment: Doc<'soulComments'>; user: PublicUser | null }> = []
|
||||
for (const comment of comments) {
|
||||
if (comment.softDeletedAt) continue
|
||||
const user = await ctx.db.get(comment.userId)
|
||||
const user = toPublicUser(await ctx.db.get(comment.userId))
|
||||
results.push({ comment, user })
|
||||
}
|
||||
return results
|
||||
@@ -59,7 +60,7 @@ export const remove = mutation({
|
||||
|
||||
const isOwner = comment.userId === user._id
|
||||
if (!isOwner) {
|
||||
assertRole(user, ['admin', 'moderator'])
|
||||
assertModerator(user)
|
||||
}
|
||||
|
||||
await ctx.db.patch(comment._id, {
|
||||
|
||||
+5
-3
@@ -1,7 +1,7 @@
|
||||
import { v } from 'convex/values'
|
||||
import type { Doc } from './_generated/dataModel'
|
||||
import { mutation, query } from './_generated/server'
|
||||
import { requireUser } from './lib/access'
|
||||
import { toPublicSoul } from './lib/public'
|
||||
|
||||
export const isStarred = query({
|
||||
args: { soulId: v.id('souls') },
|
||||
@@ -59,10 +59,12 @@ export const listByUser = query({
|
||||
.order('desc')
|
||||
.take(limit)
|
||||
|
||||
const souls: Doc<'souls'>[] = []
|
||||
const souls: NonNullable<ReturnType<typeof toPublicSoul>>[] = []
|
||||
for (const star of stars) {
|
||||
const soul = await ctx.db.get(star.soulId)
|
||||
if (soul) souls.push(soul)
|
||||
const publicSoul = toPublicSoul(soul)
|
||||
if (!publicSoul) continue
|
||||
souls.push(publicSoul)
|
||||
}
|
||||
return souls
|
||||
},
|
||||
|
||||
+26
-10
@@ -2,7 +2,8 @@ import { ConvexError, v } from 'convex/values'
|
||||
import { internal } from './_generated/api'
|
||||
import type { Doc, Id } from './_generated/dataModel'
|
||||
import { action, internalMutation, internalQuery, mutation, query } from './_generated/server'
|
||||
import { assertRole, requireUser, requireUserFromAction } from './lib/access'
|
||||
import { assertModerator, requireUser, requireUserFromAction } from './lib/access'
|
||||
import { toPublicSoul, toPublicUser } from './lib/public'
|
||||
import { getFrontmatterValue, hashSkillFiles } from './lib/skills'
|
||||
import { generateSoulChangelogPreview } from './lib/soulChangelog'
|
||||
import { fetchText, type PublishResult, publishSoulVersionForUser } from './lib/soulPublish'
|
||||
@@ -27,9 +28,11 @@ export const getBySlug = query({
|
||||
const soul = matches[0] ?? null
|
||||
if (!soul || soul.softDeletedAt) return null
|
||||
const latestVersion = soul.latestVersionId ? await ctx.db.get(soul.latestVersionId) : null
|
||||
const owner = await ctx.db.get(soul.ownerUserId)
|
||||
const owner = toPublicUser(await ctx.db.get(soul.ownerUserId))
|
||||
const publicSoul = toPublicSoul(soul)
|
||||
if (!publicSoul) return null
|
||||
|
||||
return { soul, latestVersion, owner }
|
||||
return { soul: publicSoul, latestVersion, owner }
|
||||
},
|
||||
})
|
||||
|
||||
@@ -59,13 +62,21 @@ export const list = query({
|
||||
.withIndex('by_owner', (q) => q.eq('ownerUserId', ownerUserId))
|
||||
.order('desc')
|
||||
.take(limit * 5)
|
||||
return entries.filter((soul) => !soul.softDeletedAt).slice(0, limit)
|
||||
return entries
|
||||
.filter((soul) => !soul.softDeletedAt)
|
||||
.slice(0, limit)
|
||||
.map((soul) => toPublicSoul(soul))
|
||||
.filter((soul): soul is NonNullable<typeof soul> => Boolean(soul))
|
||||
}
|
||||
const entries = await ctx.db
|
||||
.query('souls')
|
||||
.order('desc')
|
||||
.take(limit * 5)
|
||||
return entries.filter((soul) => !soul.softDeletedAt).slice(0, limit)
|
||||
return entries
|
||||
.filter((soul) => !soul.softDeletedAt)
|
||||
.slice(0, limit)
|
||||
.map((soul) => toPublicSoul(soul))
|
||||
.filter((soul): soul is NonNullable<typeof soul> => Boolean(soul))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -82,12 +93,17 @@ export const listPublicPage = query({
|
||||
.order('desc')
|
||||
.paginate({ cursor: args.cursor ?? null, numItems: limit })
|
||||
|
||||
const items: Array<{ soul: Doc<'souls'>; latestVersion: Doc<'soulVersions'> | null }> = []
|
||||
const items: Array<{
|
||||
soul: NonNullable<ReturnType<typeof toPublicSoul>>
|
||||
latestVersion: Doc<'soulVersions'> | null
|
||||
}> = []
|
||||
|
||||
for (const soul of page) {
|
||||
if (soul.softDeletedAt) continue
|
||||
const latestVersion = soul.latestVersionId ? await ctx.db.get(soul.latestVersionId) : null
|
||||
items.push({ soul, latestVersion })
|
||||
const publicSoul = toPublicSoul(soul)
|
||||
if (!publicSoul) continue
|
||||
items.push({ soul: publicSoul, latestVersion })
|
||||
}
|
||||
|
||||
return { items, nextCursor: isDone ? null : continueCursor }
|
||||
@@ -309,7 +325,7 @@ export const updateTags = mutation({
|
||||
const soul = await ctx.db.get(args.soulId)
|
||||
if (!soul) throw new Error('Soul not found')
|
||||
if (soul.ownerUserId !== user._id) {
|
||||
assertRole(user, ['admin', 'moderator'])
|
||||
assertModerator(user)
|
||||
}
|
||||
|
||||
const nextTags = { ...soul.tags }
|
||||
@@ -377,7 +393,7 @@ export const insertVersion = internalMutation({
|
||||
.withIndex('by_slug', (q) => q.eq('slug', args.slug))
|
||||
.order('desc')
|
||||
.take(2)
|
||||
let soul = soulMatches[0] ?? null
|
||||
let soul: Doc<'souls'> | null = soulMatches[0] ?? null
|
||||
|
||||
if (soul && soul.ownerUserId !== userId) {
|
||||
throw new Error('Only the owner can publish updates')
|
||||
@@ -506,7 +522,7 @@ export const setSoulSoftDeletedInternal = internalMutation({
|
||||
if (!soul) throw new Error('Soul not found')
|
||||
|
||||
if (soul.ownerUserId !== args.userId) {
|
||||
assertRole(user, ['admin', 'moderator'])
|
||||
assertModerator(user)
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
|
||||
+10
-21
@@ -1,8 +1,8 @@
|
||||
import { v } from 'convex/values'
|
||||
import type { Doc } from './_generated/dataModel'
|
||||
import { internalMutation, mutation, query } from './_generated/server'
|
||||
import { requireUser } from './lib/access'
|
||||
import { applySkillStatDeltas } from './lib/skillStats'
|
||||
import { toPublicSkill } from './lib/public'
|
||||
import { insertStatEvent } from './skillStatEvents'
|
||||
|
||||
export const isStarred = query({
|
||||
args: { skillId: v.id('skills') },
|
||||
@@ -30,11 +30,7 @@ export const toggle = mutation({
|
||||
|
||||
if (existing) {
|
||||
await ctx.db.delete(existing._id)
|
||||
const patch = applySkillStatDeltas(skill, { stars: -1 })
|
||||
await ctx.db.patch(skill._id, {
|
||||
...patch,
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
await insertStatEvent(ctx, { skillId: skill._id, kind: 'unstar' })
|
||||
return { starred: false }
|
||||
}
|
||||
|
||||
@@ -44,10 +40,7 @@ export const toggle = mutation({
|
||||
createdAt: Date.now(),
|
||||
})
|
||||
|
||||
await ctx.db.patch(skill._id, {
|
||||
...applySkillStatDeltas(skill, { stars: 1 }),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
await insertStatEvent(ctx, { skillId: skill._id, kind: 'star' })
|
||||
|
||||
return { starred: true }
|
||||
},
|
||||
@@ -62,10 +55,12 @@ export const listByUser = query({
|
||||
.withIndex('by_user', (q) => q.eq('userId', args.userId))
|
||||
.order('desc')
|
||||
.take(limit)
|
||||
const skills: Doc<'skills'>[] = []
|
||||
const skills: NonNullable<ReturnType<typeof toPublicSkill>>[] = []
|
||||
for (const star of stars) {
|
||||
const skill = await ctx.db.get(star.skillId)
|
||||
if (skill) skills.push(skill)
|
||||
const publicSkill = toPublicSkill(skill)
|
||||
if (!publicSkill) continue
|
||||
skills.push(publicSkill)
|
||||
}
|
||||
return skills
|
||||
},
|
||||
@@ -88,10 +83,7 @@ export const addStarInternal = internalMutation({
|
||||
createdAt: Date.now(),
|
||||
})
|
||||
|
||||
await ctx.db.patch(skill._id, {
|
||||
...applySkillStatDeltas(skill, { stars: 1 }),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
await insertStatEvent(ctx, { skillId: skill._id, kind: 'star' })
|
||||
|
||||
return { ok: true as const, starred: true, alreadyStarred: false }
|
||||
},
|
||||
@@ -109,10 +101,7 @@ export const removeStarInternal = internalMutation({
|
||||
if (!existing) return { ok: true as const, unstarred: false, alreadyUnstarred: true }
|
||||
|
||||
await ctx.db.delete(existing._id)
|
||||
await ctx.db.patch(skill._id, {
|
||||
...applySkillStatDeltas(skill, { stars: -1 }),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
await insertStatEvent(ctx, { skillId: skill._id, kind: 'unstar' })
|
||||
|
||||
return { ok: true as const, unstarred: true, alreadyUnstarred: false }
|
||||
},
|
||||
|
||||
+82
-57
@@ -1,6 +1,7 @@
|
||||
import { v } from 'convex/values'
|
||||
import { internal } from './_generated/api'
|
||||
import type { Doc } from './_generated/dataModel'
|
||||
import type { ActionCtx } from './_generated/server'
|
||||
import { internalAction, internalMutation, internalQuery } from './_generated/server'
|
||||
|
||||
const DEFAULT_BATCH_SIZE = 200
|
||||
@@ -44,6 +45,25 @@ type BackfillState = {
|
||||
doneAt?: number
|
||||
}
|
||||
|
||||
type BackfillActionArgs = {
|
||||
batchSize?: number
|
||||
maxBatches?: number
|
||||
resetCursor?: boolean
|
||||
}
|
||||
|
||||
type BackfillStats = {
|
||||
scanned: number
|
||||
patched: number
|
||||
batches: number
|
||||
}
|
||||
|
||||
type BackfillActionResult = {
|
||||
ok: true
|
||||
isDone: boolean
|
||||
cursor: string | null
|
||||
stats: BackfillStats
|
||||
}
|
||||
|
||||
export const getSkillStatBackfillStateInternal = internalQuery({
|
||||
args: {},
|
||||
handler: async (ctx): Promise<BackfillState> => {
|
||||
@@ -87,68 +107,73 @@ export const setSkillStatBackfillStateInternal = internalMutation({
|
||||
},
|
||||
})
|
||||
|
||||
export const runSkillStatBackfillInternal = internalAction({
|
||||
async function runSkillStatBackfillInternalHandler(
|
||||
ctx: ActionCtx,
|
||||
args: BackfillActionArgs,
|
||||
): Promise<BackfillActionResult> {
|
||||
const batchSize = clampInt(args.batchSize ?? DEFAULT_BATCH_SIZE, 1, MAX_BATCH_SIZE)
|
||||
const maxBatches = clampInt(args.maxBatches ?? DEFAULT_MAX_BATCHES, 1, MAX_MAX_BATCHES)
|
||||
|
||||
if (args.resetCursor) {
|
||||
await ctx.runMutation(internal.statsMaintenance.setSkillStatBackfillStateInternal, {
|
||||
cursor: undefined,
|
||||
doneAt: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
const state = (await ctx.runQuery(
|
||||
internal.statsMaintenance.getSkillStatBackfillStateInternal,
|
||||
{},
|
||||
)) as BackfillState
|
||||
if (state.doneAt && !args.resetCursor) {
|
||||
return {
|
||||
ok: true,
|
||||
isDone: true,
|
||||
cursor: null,
|
||||
stats: { scanned: 0, patched: 0, batches: 0 },
|
||||
}
|
||||
}
|
||||
|
||||
let cursor: string | null = state.cursor ?? null
|
||||
const stats: BackfillStats = { scanned: 0, patched: 0, batches: 0 }
|
||||
|
||||
for (let i = 0; i < maxBatches; i += 1) {
|
||||
const result = (await ctx.runMutation(
|
||||
internal.statsMaintenance.backfillSkillStatFieldsInternal,
|
||||
{
|
||||
cursor: cursor ?? undefined,
|
||||
batchSize,
|
||||
},
|
||||
)) as { scanned: number; patched: number; cursor: string | null; isDone: boolean }
|
||||
stats.scanned += result.scanned
|
||||
stats.patched += result.patched
|
||||
stats.batches += 1
|
||||
cursor = result.cursor
|
||||
|
||||
if (result.isDone) {
|
||||
await ctx.runMutation(internal.statsMaintenance.setSkillStatBackfillStateInternal, {
|
||||
cursor: undefined,
|
||||
doneAt: Date.now(),
|
||||
})
|
||||
return { ok: true, isDone: true, cursor: null, stats }
|
||||
}
|
||||
|
||||
await ctx.runMutation(internal.statsMaintenance.setSkillStatBackfillStateInternal, {
|
||||
cursor: cursor ?? undefined,
|
||||
doneAt: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
return { ok: true, isDone: false, cursor, stats }
|
||||
}
|
||||
|
||||
export const runSkillStatBackfillInternal: ReturnType<typeof internalAction> = internalAction({
|
||||
args: {
|
||||
batchSize: v.optional(v.number()),
|
||||
maxBatches: v.optional(v.number()),
|
||||
resetCursor: v.optional(v.boolean()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const batchSize = clampInt(args.batchSize ?? DEFAULT_BATCH_SIZE, 1, MAX_BATCH_SIZE)
|
||||
const maxBatches = clampInt(args.maxBatches ?? DEFAULT_MAX_BATCHES, 1, MAX_MAX_BATCHES)
|
||||
|
||||
if (args.resetCursor) {
|
||||
await ctx.runMutation(internal.statsMaintenance.setSkillStatBackfillStateInternal, {
|
||||
cursor: undefined,
|
||||
doneAt: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
const state = await ctx.runQuery(
|
||||
internal.statsMaintenance.getSkillStatBackfillStateInternal,
|
||||
{},
|
||||
)
|
||||
if (state.doneAt && !args.resetCursor) {
|
||||
return {
|
||||
ok: true as const,
|
||||
isDone: true,
|
||||
cursor: null,
|
||||
stats: { scanned: 0, patched: 0, batches: 0 },
|
||||
}
|
||||
}
|
||||
|
||||
let cursor = state.cursor ?? null
|
||||
const stats = { scanned: 0, patched: 0, batches: 0 }
|
||||
|
||||
for (let i = 0; i < maxBatches; i += 1) {
|
||||
const result = await ctx.runMutation(
|
||||
internal.statsMaintenance.backfillSkillStatFieldsInternal,
|
||||
{
|
||||
cursor: cursor ?? undefined,
|
||||
batchSize,
|
||||
},
|
||||
)
|
||||
stats.scanned += result.scanned
|
||||
stats.patched += result.patched
|
||||
stats.batches += 1
|
||||
cursor = result.cursor
|
||||
|
||||
if (result.isDone) {
|
||||
await ctx.runMutation(internal.statsMaintenance.setSkillStatBackfillStateInternal, {
|
||||
cursor: undefined,
|
||||
doneAt: Date.now(),
|
||||
})
|
||||
return { ok: true as const, isDone: true, cursor: null, stats }
|
||||
}
|
||||
|
||||
await ctx.runMutation(internal.statsMaintenance.setSkillStatBackfillStateInternal, {
|
||||
cursor: cursor ?? undefined,
|
||||
doneAt: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
return { ok: true as const, isDone: false, cursor, stats }
|
||||
},
|
||||
handler: runSkillStatBackfillInternalHandler,
|
||||
})
|
||||
|
||||
function buildSkillStatPatch(skill: Doc<'skills'>) {
|
||||
|
||||
+29
-29
@@ -4,7 +4,7 @@ import type { Id } from './_generated/dataModel'
|
||||
import type { MutationCtx, QueryCtx } from './_generated/server'
|
||||
import { internalMutation, mutation, query } from './_generated/server'
|
||||
import { requireUser } from './lib/access'
|
||||
import { applySkillStatDeltas, bumpDailySkillStats } from './lib/skillStats'
|
||||
import { insertStatEvent } from './skillStatEvents'
|
||||
|
||||
const TELEMETRY_STALE_MS = 120 * 24 * 60 * 60 * 1000
|
||||
|
||||
@@ -88,7 +88,13 @@ export const getMyInstalled = query({
|
||||
lastSeenAt: number
|
||||
expiredAt?: number
|
||||
skills: Array<{
|
||||
skill: { slug: string; displayName: string; summary?: string; stats: unknown }
|
||||
skill: {
|
||||
slug: string
|
||||
displayName: string
|
||||
summary?: string
|
||||
stats: unknown
|
||||
ownerUserId: Id<'users'>
|
||||
}
|
||||
firstSeenAt: number
|
||||
lastSeenAt: number
|
||||
lastVersion?: string
|
||||
@@ -105,7 +111,13 @@ export const getMyInstalled = query({
|
||||
|
||||
const filtered = includeRemoved ? installs : installs.filter((entry) => !entry.removedAt)
|
||||
const skills: Array<{
|
||||
skill: { slug: string; displayName: string; summary?: string; stats: unknown }
|
||||
skill: {
|
||||
slug: string
|
||||
displayName: string
|
||||
summary?: string
|
||||
stats: unknown
|
||||
ownerUserId: Id<'users'>
|
||||
}
|
||||
firstSeenAt: number
|
||||
lastSeenAt: number
|
||||
lastVersion?: string
|
||||
@@ -121,6 +133,7 @@ export const getMyInstalled = query({
|
||||
displayName: skill.displayName,
|
||||
summary: skill.summary,
|
||||
stats: skill.stats,
|
||||
ownerUserId: skill.ownerUserId,
|
||||
},
|
||||
firstSeenAt: entry.firstSeenAt,
|
||||
lastSeenAt: entry.lastSeenAt,
|
||||
@@ -158,13 +171,13 @@ async function clearTelemetryForUser(ctx: MutationCtx, params: { userId: Id<'use
|
||||
await ctx.db.delete(entry._id)
|
||||
continue
|
||||
}
|
||||
const patch = applySkillStatDeltas(skill, {
|
||||
installsCurrent: entry.activeRoots > 0 ? -1 : 0,
|
||||
installsAllTime: -1,
|
||||
})
|
||||
await ctx.db.patch(skill._id, {
|
||||
...patch,
|
||||
updatedAt: Date.now(),
|
||||
await insertStatEvent(ctx, {
|
||||
skillId: skill._id,
|
||||
kind: 'install_clear',
|
||||
delta: {
|
||||
allTime: -1,
|
||||
current: entry.activeRoots > 0 ? -1 : 0,
|
||||
},
|
||||
})
|
||||
await ctx.db.delete(entry._id)
|
||||
}
|
||||
@@ -371,25 +384,12 @@ async function bumpSkillInstallCounts(
|
||||
ctx: MutationCtx,
|
||||
params: { skillId: Id<'skills'>; deltaAllTime: number; deltaCurrent: number },
|
||||
) {
|
||||
const skill = await ctx.db.get(params.skillId)
|
||||
if (!skill) return
|
||||
const now = Date.now()
|
||||
const patch = applySkillStatDeltas(skill, {
|
||||
installsAllTime: params.deltaAllTime,
|
||||
installsCurrent: params.deltaCurrent,
|
||||
})
|
||||
|
||||
await ctx.db.patch(skill._id, {
|
||||
...patch,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
||||
if (params.deltaAllTime > 0) {
|
||||
await bumpDailySkillStats(ctx, {
|
||||
skillId: params.skillId,
|
||||
now,
|
||||
installs: params.deltaAllTime,
|
||||
})
|
||||
if (params.deltaAllTime === 1 && params.deltaCurrent === 1) {
|
||||
await insertStatEvent(ctx, { skillId: params.skillId, kind: 'install_new' })
|
||||
} else if (params.deltaAllTime === 0 && params.deltaCurrent === 1) {
|
||||
await insertStatEvent(ctx, { skillId: params.skillId, kind: 'install_reactivate' })
|
||||
} else if (params.deltaAllTime === 0 && params.deltaCurrent === -1) {
|
||||
await insertStatEvent(ctx, { skillId: params.skillId, kind: 'install_deactivate' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
-5
@@ -1,13 +1,19 @@
|
||||
import { getAuthUserId } from '@convex-dev/auth/server'
|
||||
import { v } from 'convex/values'
|
||||
import { internal } from './_generated/api'
|
||||
import { mutation, query } from './_generated/server'
|
||||
import { assertRole, requireUser } from './lib/access'
|
||||
import { internalQuery, mutation, query } from './_generated/server'
|
||||
import { assertAdmin, requireUser } from './lib/access'
|
||||
import { toPublicUser } from './lib/public'
|
||||
|
||||
const DEFAULT_ROLE = 'user'
|
||||
const ADMIN_HANDLE = 'steipete'
|
||||
|
||||
export const getById = query({
|
||||
args: { userId: v.id('users') },
|
||||
handler: async (ctx, args) => toPublicUser(await ctx.db.get(args.userId)),
|
||||
})
|
||||
|
||||
export const getByIdInternal = internalQuery({
|
||||
args: { userId: v.id('users') },
|
||||
handler: async (ctx, args) => ctx.db.get(args.userId),
|
||||
})
|
||||
@@ -78,7 +84,7 @@ export const list = query({
|
||||
args: { limit: v.optional(v.number()) },
|
||||
handler: async (ctx, args) => {
|
||||
const { user } = await requireUser(ctx)
|
||||
assertRole(user, ['admin'])
|
||||
assertAdmin(user)
|
||||
const limit = args.limit ?? 50
|
||||
return ctx.db.query('users').order('desc').take(limit)
|
||||
},
|
||||
@@ -87,10 +93,11 @@ export const list = query({
|
||||
export const getByHandle = query({
|
||||
args: { handle: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
return ctx.db
|
||||
const user = await ctx.db
|
||||
.query('users')
|
||||
.withIndex('handle', (q) => q.eq('handle', args.handle))
|
||||
.unique()
|
||||
return toPublicUser(user)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -101,7 +108,7 @@ export const setRole = mutation({
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const { user } = await requireUser(ctx)
|
||||
assertRole(user, ['admin'])
|
||||
assertAdmin(user)
|
||||
await ctx.db.patch(args.userId, { role: args.role, updatedAt: Date.now() })
|
||||
await ctx.db.insert('auditLogs', {
|
||||
actorUserId: user._id,
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ export const sendDiscordWebhook = internalAction({
|
||||
summary: v.optional(v.string()),
|
||||
version: v.optional(v.string()),
|
||||
ownerHandle: v.optional(v.string()),
|
||||
batch: v.optional(v.string()),
|
||||
highlighted: v.optional(v.boolean()),
|
||||
tags: v.optional(v.array(v.string())),
|
||||
}),
|
||||
},
|
||||
@@ -21,7 +21,7 @@ export const sendDiscordWebhook = internalAction({
|
||||
event: args.event,
|
||||
slug: args.skill.slug,
|
||||
version: args.skill.version ?? null,
|
||||
batch: args.skill.batch ?? null,
|
||||
highlighted: args.skill.highlighted ?? false,
|
||||
highlightedOnly: config.highlightedOnly,
|
||||
}
|
||||
if (!shouldSendWebhook(args.event, args.skill, config)) {
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ Reading order (new contributor):
|
||||
Feature/ops docs (already present):
|
||||
|
||||
- `docs/spec.md`: product + implementation spec (data model + flows).
|
||||
- `docs/telemetry.md`: what `clawdhub sync` reports; opt-out.
|
||||
- `docs/telemetry.md`: what `clawhub sync` reports; opt-out.
|
||||
- `docs/webhook.md`: Discord webhook events/payload.
|
||||
- `docs/diffing.md`: version-to-version diff UI spec.
|
||||
- `docs/manual-testing.md`: CLI smoke scripts.
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ read_when:
|
||||
|
||||
# API v1
|
||||
|
||||
Base: `https://clawdhub.com`
|
||||
Base: `https://clawhub.ai`
|
||||
|
||||
OpenAPI: `/api/v1/openapi.json`
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ read_when:
|
||||
|
||||
- Web app: TanStack Start (React) under `src/`.
|
||||
- Backend: Convex under `convex/` (DB, storage, actions, HTTP routes).
|
||||
- CLI: `packages/clawdhub/` (published as `clawdhub`).
|
||||
- Shared schemas/routes: `packages/schema/` (`clawdhub-schema`).
|
||||
- CLI: `packages/clawdhub/` (published as `clawhub`, legacy `clawdhub`).
|
||||
- Shared schemas/routes: `packages/schema/` (`clawhub-schema`).
|
||||
|
||||
## Data + storage
|
||||
|
||||
@@ -38,8 +38,8 @@ read_when:
|
||||
- Download zip via `/api/v1/download?slug=...&version=...`.
|
||||
- Extract into `./skills/<slug>` (default).
|
||||
- Persist install state:
|
||||
- `./.clawdhub/lock.json` (per workdir)
|
||||
- `./skills/<slug>/.clawdhub/origin.json` (per skill folder)
|
||||
- `./.clawhub/lock.json` (per workdir, legacy `.clawdhub`)
|
||||
- `./skills/<slug>/.clawhub/origin.json` (per skill folder, legacy `.clawdhub`)
|
||||
|
||||
### Update (CLI)
|
||||
|
||||
|
||||
+4
-4
@@ -23,7 +23,7 @@ The CLI uses a long-lived API token (Bearer token) for publish/sync/delete.
|
||||
|
||||
### Browser flow (default)
|
||||
|
||||
`clawdhub login` does:
|
||||
`clawhub login` does:
|
||||
|
||||
1. Starts a loopback HTTP server on `127.0.0.1` (random port).
|
||||
2. Opens `<site>/cli/auth?redirect_uri=http://127.0.0.1:<port>/callback&state=...`.
|
||||
@@ -35,18 +35,18 @@ The CLI uses a long-lived API token (Bearer token) for publish/sync/delete.
|
||||
Create a token in the web UI (Settings → API tokens) and paste it:
|
||||
|
||||
```bash
|
||||
clawdhub login --token clh_...
|
||||
clawhub login --token clh_...
|
||||
```
|
||||
|
||||
### Token storage
|
||||
|
||||
Default global config path:
|
||||
|
||||
- macOS: `~/Library/Application Support/clawdhub/config.json`
|
||||
- macOS: `~/Library/Application Support/clawhub/config.json`
|
||||
|
||||
Override:
|
||||
|
||||
- `CLAWDHUB_CONFIG_PATH=/path/to/config.json`
|
||||
- `CLAWHUB_CONFIG_PATH=/path/to/config.json` (legacy `CLAWDHUB_CONFIG_PATH`)
|
||||
|
||||
### Revocation
|
||||
|
||||
|
||||
+15
-15
@@ -7,41 +7,41 @@ read_when:
|
||||
|
||||
# CLI
|
||||
|
||||
CLI package: `packages/clawdhub/` (bin: `clawdhub`).
|
||||
CLI package: `packages/clawdhub/` (published as `clawhub`, bin: `clawhub`).
|
||||
|
||||
From this repo you can run it via the wrapper script:
|
||||
|
||||
```bash
|
||||
bun clawdhub --help
|
||||
bun clawhub --help
|
||||
```
|
||||
|
||||
## Global flags
|
||||
|
||||
- `--workdir <dir>`: working directory (default: cwd; falls back to Clawdbot workspace if configured)
|
||||
- `--dir <dir>`: install dir under workdir (default: `skills`)
|
||||
- `--site <url>`: base URL for browser login (default: `https://clawdhub.com`)
|
||||
- `--registry <url>`: API base URL (default: discovered, else `https://clawdhub.com`)
|
||||
- `--site <url>`: base URL for browser login (default: `https://clawhub.ai`)
|
||||
- `--registry <url>`: API base URL (default: discovered, else `https://clawhub.ai`)
|
||||
- `--no-input`: disable prompts
|
||||
|
||||
Env equivalents:
|
||||
|
||||
- `CLAWDHUB_SITE`
|
||||
- `CLAWDHUB_REGISTRY`
|
||||
- `CLAWDHUB_WORKDIR`
|
||||
- `CLAWHUB_SITE` (legacy `CLAWDHUB_SITE`)
|
||||
- `CLAWHUB_REGISTRY` (legacy `CLAWDHUB_REGISTRY`)
|
||||
- `CLAWHUB_WORKDIR` (legacy `CLAWDHUB_WORKDIR`)
|
||||
|
||||
## Config file
|
||||
|
||||
Stores your API token + cached registry URL.
|
||||
|
||||
- macOS: `~/Library/Application Support/clawdhub/config.json`
|
||||
- override: `CLAWDHUB_CONFIG_PATH`
|
||||
- macOS: `~/Library/Application Support/clawhub/config.json`
|
||||
- override: `CLAWHUB_CONFIG_PATH` (legacy `CLAWDHUB_CONFIG_PATH`)
|
||||
|
||||
## Commands
|
||||
|
||||
### `login` / `auth login`
|
||||
|
||||
- Default: opens browser to `<site>/cli/auth` and completes via loopback callback.
|
||||
- Headless: `clawdhub login --token clh_...`
|
||||
- Headless: `clawhub login --token clh_...`
|
||||
|
||||
### `whoami`
|
||||
|
||||
@@ -72,12 +72,12 @@ Stores your API token + cached registry URL.
|
||||
- Downloads zip via `/api/v1/download`.
|
||||
- Extracts into `<workdir>/<dir>/<slug>`.
|
||||
- Writes:
|
||||
- `<workdir>/.clawdhub/lock.json`
|
||||
- `<skill>/.clawdhub/origin.json`
|
||||
- `<workdir>/.clawhub/lock.json` (legacy `.clawdhub`)
|
||||
- `<skill>/.clawhub/origin.json` (legacy `.clawdhub`)
|
||||
|
||||
### `list`
|
||||
|
||||
- Reads `<workdir>/.clawdhub/lock.json`.
|
||||
- Reads `<workdir>/.clawhub/lock.json` (legacy `.clawdhub`).
|
||||
|
||||
### `update [slug]` / `update --all`
|
||||
|
||||
@@ -101,7 +101,7 @@ Stores your API token + cached registry URL.
|
||||
- `routing.agents.*.workspace/skills` (per-agent)
|
||||
- `~/.clawdbot/skills` (shared)
|
||||
- `skills.load.extraDirs` (shared packs)
|
||||
- Respects `CLAWDBOT_CONFIG_PATH` and `CLAWDBOT_STATE_DIR`.
|
||||
- Respects `CLAWDBOT_CONFIG_PATH` / `CLAWDBOT_STATE_DIR` and `OPENCLAW_CONFIG_PATH` / `OPENCLAW_STATE_DIR`.
|
||||
- Flags:
|
||||
- `--root <dir...>` extra scan roots
|
||||
- `--all` upload without prompting
|
||||
@@ -113,5 +113,5 @@ Stores your API token + cached registry URL.
|
||||
|
||||
Telemetry:
|
||||
|
||||
- Sent during `sync` when logged in, unless `CLAWDHUB_DISABLE_TELEMETRY=1`.
|
||||
- Sent during `sync` when logged in, unless `CLAWHUB_DISABLE_TELEMETRY=1` (legacy `CLAWDHUB_DISABLE_TELEMETRY=1`).
|
||||
- Details: `docs/telemetry.md`.
|
||||
|
||||
+6
-5
@@ -7,7 +7,7 @@ read_when:
|
||||
|
||||
# Deploy
|
||||
|
||||
ClawdHub is two deployables:
|
||||
OpenClaw is two deployables:
|
||||
|
||||
- Web app (TanStack Start) → typically Vercel.
|
||||
- Convex backend → Convex deployment (serves `/api/...` routes).
|
||||
@@ -55,12 +55,13 @@ For self-host:
|
||||
|
||||
The CLI can discover the API base from:
|
||||
|
||||
- `/.well-known/clawdhub.json`
|
||||
- `/.well-known/clawhub.json` (preferred)
|
||||
- `/.well-known/clawdhub.json` (legacy)
|
||||
|
||||
If you don’t serve that file, users must set:
|
||||
|
||||
```bash
|
||||
export CLAWDHUB_REGISTRY=https://your-site.example
|
||||
export CLAWHUB_REGISTRY=https://your-site.example
|
||||
```
|
||||
|
||||
## 5) Post-deploy checks
|
||||
@@ -73,6 +74,6 @@ curl -i "https://<site>/api/v1/skills/gifgrep"
|
||||
Then:
|
||||
|
||||
```bash
|
||||
clawdhub login --site https://<site>
|
||||
clawdhub whoami
|
||||
clawhub login --site https://<site>
|
||||
clawhub whoami
|
||||
```
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@ read_when:
|
||||
## Goals
|
||||
- Compare any file between two versions.
|
||||
- Default compare: `latest` vs `previous` (SemVer precedence).
|
||||
- UX feels native to ClawdHub (theme + typography + motion).
|
||||
- UX feels native to OpenClaw (theme + typography + motion).
|
||||
- Inline or side-by-side toggle.
|
||||
- Public access.
|
||||
|
||||
@@ -57,7 +57,7 @@ Optional helper action:
|
||||
- Feed into Monaco diff editor.
|
||||
|
||||
## Monaco theming
|
||||
- Define `clawdhub-light` / `clawdhub-dark` via `monaco.editor.defineTheme`.
|
||||
- Define `clawhub-light` / `clawhub-dark` via `monaco.editor.defineTheme`.
|
||||
- Derive colors from CSS variables on `document.documentElement`:
|
||||
- `--surface`, `--surface-muted`, `--ink`, `--ink-soft`, `--line`, `--accent`.
|
||||
- Apply theme on load + when theme changes (`data-theme`).
|
||||
|
||||
+6
-5
@@ -7,7 +7,7 @@ read_when:
|
||||
|
||||
# HTTP API
|
||||
|
||||
Base URL: `https://clawdhub.com` (default).
|
||||
Base URL: `https://clawhub.ai` (default).
|
||||
|
||||
All v1 paths are under `/api/v1/...` and implemented by Convex HTTP routes (`convex/http.ts`).
|
||||
Legacy `/api/...` and `/api/cli/...` remain for compatibility (see `DEPRECATIONS.md`).
|
||||
@@ -172,16 +172,17 @@ Still supported for older CLI versions:
|
||||
|
||||
See `DEPRECATIONS.md` for removal plan.
|
||||
|
||||
## Registry discovery (`/.well-known/clawdhub.json`)
|
||||
## Registry discovery (`/.well-known/clawhub.json`)
|
||||
|
||||
The CLI can discover registry/auth settings from the site:
|
||||
|
||||
- `/.well-known/clawdhub.json` (JSON)
|
||||
- `/.well-known/clawhub.json` (JSON, preferred)
|
||||
- `/.well-known/clawdhub.json` (legacy)
|
||||
|
||||
Schema:
|
||||
|
||||
```json
|
||||
{ "apiBase": "https://clawdhub.com", "authBase": "https://clawdhub.com", "minCliVersion": "0.0.5" }
|
||||
{ "apiBase": "https://clawhub.ai", "authBase": "https://clawhub.ai", "minCliVersion": "0.0.5" }
|
||||
```
|
||||
|
||||
If you self-host, serve this file (or set `CLAWDHUB_REGISTRY` explicitly).
|
||||
If you self-host, serve this file (or set `CLAWHUB_REGISTRY` explicitly; legacy `CLAWDHUB_REGISTRY`).
|
||||
|
||||
+20
-20
@@ -8,53 +8,53 @@ read_when:
|
||||
# Manual testing (CLI)
|
||||
|
||||
## Setup
|
||||
- Ensure logged in: `bun clawdhub whoami` (or `bun clawdhub login`).
|
||||
- Ensure logged in: `bun clawhub whoami` (or `bun clawhub login`).
|
||||
- Optional: set env
|
||||
- `CLAWDHUB_SITE=https://clawdhub.com`
|
||||
- `CLAWDHUB_REGISTRY=https://clawdhub.com`
|
||||
- `CLAWHUB_SITE=https://clawhub.ai`
|
||||
- `CLAWHUB_REGISTRY=https://clawhub.ai`
|
||||
|
||||
## Smoke
|
||||
- `bun clawdhub --help`
|
||||
- `bun clawdhub --cli-version`
|
||||
- `bun clawdhub whoami`
|
||||
- `bun clawhub --help`
|
||||
- `bun clawhub --cli-version`
|
||||
- `bun clawhub whoami`
|
||||
|
||||
## Search
|
||||
- `bun clawdhub search gif --limit 5`
|
||||
- `bun clawhub search gif --limit 5`
|
||||
|
||||
## Install / list / update
|
||||
- `mkdir -p /tmp/clawdhub-manual && cd /tmp/clawdhub-manual`
|
||||
- `bunx clawdhub@beta install gifgrep --force`
|
||||
- `bunx clawdhub@beta list`
|
||||
- `bunx clawdhub@beta update gifgrep --force`
|
||||
- `mkdir -p /tmp/clawhub-manual && cd /tmp/clawhub-manual`
|
||||
- `bunx clawhub@beta install gifgrep --force`
|
||||
- `bunx clawhub@beta list`
|
||||
- `bunx clawhub@beta update gifgrep --force`
|
||||
|
||||
## Publish (changelog optional)
|
||||
- `mkdir -p /tmp/clawdhub-skill-demo/SKILL && cd /tmp/clawdhub-skill-demo`
|
||||
- `mkdir -p /tmp/clawhub-skill-demo/SKILL && cd /tmp/clawhub-skill-demo`
|
||||
- Create files:
|
||||
- `SKILL.md`
|
||||
- `notes.md`
|
||||
- Publish:
|
||||
- `bun clawdhub publish . --slug clawdhub-manual-<ts> --name "Manual <ts>" --version 1.0.0 --tags latest`
|
||||
- `bun clawhub publish . --slug clawhub-manual-<ts> --name "Manual <ts>" --version 1.0.0 --tags latest`
|
||||
- Publish update with empty changelog:
|
||||
- `bun clawdhub publish . --slug clawdhub-manual-<ts> --name "Manual <ts>" --version 1.0.1 --tags latest`
|
||||
- `bun clawhub publish . --slug clawhub-manual-<ts> --name "Manual <ts>" --version 1.0.1 --tags latest`
|
||||
|
||||
## Delete / undelete (owner/admin)
|
||||
- `bun clawdhub delete clawdhub-manual-<ts> --yes`
|
||||
- `bun clawhub delete clawhub-manual-<ts> --yes`
|
||||
- Verify hidden:
|
||||
- `curl -i "https://clawdhub.com/api/v1/skills/clawdhub-manual-<ts>"`
|
||||
- `curl -i "https://clawhub.ai/api/v1/skills/clawhub-manual-<ts>"`
|
||||
- Restore:
|
||||
- `bun clawdhub undelete clawdhub-manual-<ts> --yes`
|
||||
- `bun clawhub undelete clawhub-manual-<ts> --yes`
|
||||
- Cleanup:
|
||||
- `bun clawdhub delete clawdhub-manual-<ts> --yes`
|
||||
- `bun clawhub delete clawhub-manual-<ts> --yes`
|
||||
|
||||
## Sync
|
||||
- `bun clawdhub sync --dry-run --all`
|
||||
- `bun clawhub sync --dry-run --all`
|
||||
|
||||
## Playwright (menu smoke)
|
||||
|
||||
Run against prod:
|
||||
|
||||
```
|
||||
PLAYWRIGHT_BASE_URL=https://clawdhub.com bun run test:pw
|
||||
PLAYWRIGHT_BASE_URL=https://clawhub.ai bun run test:pw
|
||||
```
|
||||
|
||||
Run against a local preview server:
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ Example (starter):
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "ClawdHub",
|
||||
"name": "OpenClaw",
|
||||
"logo": "public/logo.svg",
|
||||
"navigation": [
|
||||
{ "group": "Start", "pages": ["docs/README", "docs/quickstart"] },
|
||||
|
||||
+13
-13
@@ -51,29 +51,29 @@ Then paste the printed `JWT_PRIVATE_KEY` + `JWKS` into `.env.local` (and ensure
|
||||
From this repo:
|
||||
|
||||
```bash
|
||||
bun clawdhub --help
|
||||
bun clawdhub login
|
||||
bun clawdhub whoami
|
||||
bun clawdhub search gif --limit 5
|
||||
bun clawhub --help
|
||||
bun clawhub login
|
||||
bun clawhub whoami
|
||||
bun clawhub search gif --limit 5
|
||||
```
|
||||
|
||||
Install a skill into `./skills/<slug>` (if Clawdbot is configured, installs into that workspace instead):
|
||||
|
||||
```bash
|
||||
bun clawdhub install <slug>
|
||||
bun clawdhub list
|
||||
bun clawhub install <slug>
|
||||
bun clawhub list
|
||||
```
|
||||
|
||||
You can also install into any folder:
|
||||
|
||||
```bash
|
||||
bun clawdhub install <slug> --workdir /tmp/clawdhub-demo --dir skills
|
||||
bun clawhub install <slug> --workdir /tmp/clawhub-demo --dir skills
|
||||
```
|
||||
|
||||
Update:
|
||||
|
||||
```bash
|
||||
bun clawdhub update --all
|
||||
bun clawhub update --all
|
||||
```
|
||||
|
||||
## 4) Publish a skill
|
||||
@@ -81,7 +81,7 @@ bun clawdhub update --all
|
||||
Create a folder containing `SKILL.md` (required) plus any supporting text files:
|
||||
|
||||
```bash
|
||||
mkdir -p /tmp/clawdhub-skill-demo && cd /tmp/clawdhub-skill-demo
|
||||
mkdir -p /tmp/clawhub-skill-demo && cd /tmp/clawhub-skill-demo
|
||||
cat > SKILL.md <<'EOF'
|
||||
---
|
||||
name: Demo Skill
|
||||
@@ -97,8 +97,8 @@ EOF
|
||||
Publish:
|
||||
|
||||
```bash
|
||||
bun clawdhub publish . \
|
||||
--slug clawdhub-demo-$(date +%s) \
|
||||
bun clawhub publish . \
|
||||
--slug clawhub-demo-$(date +%s) \
|
||||
--name "Demo $(date +%s)" \
|
||||
--version 1.0.0 \
|
||||
--tags latest \
|
||||
@@ -110,11 +110,11 @@ bun clawdhub publish . \
|
||||
`sync` scans for local skill folders and publishes the ones that aren’t “synced” yet.
|
||||
|
||||
```bash
|
||||
bun clawdhub sync
|
||||
bun clawhub sync
|
||||
```
|
||||
|
||||
Dry run + non-interactive:
|
||||
|
||||
```bash
|
||||
bun clawdhub sync --all --dry-run --no-input
|
||||
bun clawhub sync --all --dry-run --no-input
|
||||
```
|
||||
|
||||
@@ -18,16 +18,16 @@ Required:
|
||||
Optional:
|
||||
|
||||
- any supporting *text-based* files (see “Allowed files”)
|
||||
- `.clawdhubignore` (ignore patterns for publish/sync)
|
||||
- `.clawhubignore` (ignore patterns for publish/sync, legacy `.clawdhubignore`)
|
||||
- `.gitignore` (also honored)
|
||||
|
||||
Local install metadata (written by the CLI):
|
||||
|
||||
- `<skill>/.clawdhub/origin.json`
|
||||
- `<skill>/.clawhub/origin.json` (legacy `.clawdhub`)
|
||||
|
||||
Workdir install state (written by the CLI):
|
||||
|
||||
- `<workdir>/.clawdhub/lock.json`
|
||||
- `<workdir>/.clawhub/lock.json` (legacy `.clawdhub`)
|
||||
|
||||
## `SKILL.md`
|
||||
|
||||
|
||||
+14
-8
@@ -1,12 +1,12 @@
|
||||
---
|
||||
summary: "ClawdHub spec: skills registry, versioning, vector search, moderation"
|
||||
summary: "OpenClaw spec: skills registry, versioning, vector search, moderation"
|
||||
read_when:
|
||||
- Bootstrapping ClawdHub
|
||||
- Bootstrapping OpenClaw
|
||||
- Implementing schema/auth/search/versioning
|
||||
- Reviewing API and upload/download flows
|
||||
---
|
||||
|
||||
# ClawdHub — product + implementation spec (v1)
|
||||
# OpenClaw — product + implementation spec (v1)
|
||||
|
||||
## Goals
|
||||
- onlycrabs.ai mode for sharing `SOUL.md` bundles (host-based entry point).
|
||||
@@ -29,7 +29,7 @@ read_when:
|
||||
- `handle` (GitHub login)
|
||||
- `name`, `bio`
|
||||
- `avatarUrl` (GitHub, fallback gravatar)
|
||||
- `role`: `admin | moderator | user`
|
||||
- `role`: `admin | moderator | user` (moderators can soft-delete and flag; admins can hard-delete + change owners)
|
||||
- `createdAt`, `updatedAt`
|
||||
|
||||
### Skill
|
||||
@@ -40,9 +40,14 @@ read_when:
|
||||
- `latestVersionId`
|
||||
- `latestTagVersionId` (for `latest` tag)
|
||||
- `tags` map: `{ tag -> versionId }`
|
||||
- `badges`: `{ redactionApproved?: { byUserId, at } }`
|
||||
- `badges`: `{ redactionApproved?: { byUserId, at }, highlighted?: { byUserId, at }, official?: { byUserId, at }, deprecated?: { byUserId, at } }`
|
||||
- `official` marks admin-verified/official skills.
|
||||
- `deprecated` marks skills that should not be used for new integrations.
|
||||
- `moderationStatus`: `active | hidden | removed`
|
||||
- `moderationFlags`: `string[]` (automatic detection)
|
||||
- `moderationNotes`, `moderationReason`
|
||||
- `hiddenAt`, `hiddenBy`, `lastReviewedAt`, `reportCount`
|
||||
- `stats`: `{ downloads, stars, versions, comments }`
|
||||
- `status`: `active` only (soft-delete on version/comment only)
|
||||
- `createdAt`, `updatedAt`
|
||||
|
||||
### SkillVersion
|
||||
@@ -118,7 +123,8 @@ From SKILL.md frontmatter + AgentSkills + Clawdis extensions:
|
||||
## Auth + roles
|
||||
- Convex Auth with GitHub OAuth App.
|
||||
- Default role `user`; bootstrap `steipete` to `admin` on first login.
|
||||
- Admin UI to promote/demote roles; all changes logged.
|
||||
- Management console: moderators can hide/restore skills + mark duplicates; admins can change owners, approve badges, and hard-delete.
|
||||
- Role changes are admin-only and audited.
|
||||
|
||||
## Upload flow (50MB per version)
|
||||
1) Client requests upload session.
|
||||
@@ -151,7 +157,7 @@ Seed data lives in `convex/seed.ts` for local dev.
|
||||
- Soft-delete versions; downloads remain for non-deleted versions only.
|
||||
|
||||
## UI (SPA)
|
||||
- Home: search + filters + trending/featured + “Highlighted” batch.
|
||||
- Home: search + filters + trending/featured + “Highlighted” badge.
|
||||
- Skill detail: README render, files list, version history, tags, stats, badges.
|
||||
- Upload/edit: file picker + version + tag + changelog.
|
||||
- Account settings: name + delete account (soft delete).
|
||||
|
||||
+8
-8
@@ -1,5 +1,5 @@
|
||||
---
|
||||
summary: 'Install telemetry collected via `clawdhub sync` + opt-out.'
|
||||
summary: 'Install telemetry collected via `clawhub sync` + opt-out.'
|
||||
read_when:
|
||||
- Working on telemetry / privacy controls
|
||||
- Questions about what data is collected
|
||||
@@ -7,22 +7,22 @@ read_when:
|
||||
|
||||
# Telemetry
|
||||
|
||||
ClawdHub uses **minimal telemetry** to compute **install counts** (what’s actually in use) and to power better sorting/filtering.
|
||||
This is based on the CLI `clawdhub sync` command.
|
||||
OpenClaw uses **minimal telemetry** to compute **install counts** (what’s actually in use) and to power better sorting/filtering.
|
||||
This is based on the CLI `clawhub sync` command.
|
||||
|
||||
## When telemetry is collected
|
||||
|
||||
Telemetry is only sent when:
|
||||
|
||||
- You are **logged in** in the CLI (we already require auth for sync/publish flows).
|
||||
- You run `clawdhub sync`.
|
||||
- You run `clawhub sync`.
|
||||
- Telemetry is **not disabled** (see “How to disable” below).
|
||||
|
||||
If you are not logged in, nothing is reported.
|
||||
|
||||
## What we collect
|
||||
|
||||
On each `clawdhub sync`, the CLI reports a **full snapshot** of what it found, grouped by scan root (“folder/root”).
|
||||
On each `clawhub sync`, the CLI reports a **full snapshot** of what it found, grouped by scan root (“folder/root”).
|
||||
|
||||
For each root we store:
|
||||
|
||||
@@ -70,7 +70,7 @@ This is evaluated lazily (on the next telemetry report) to avoid background jobs
|
||||
|
||||
## Transparency + user controls
|
||||
|
||||
ClawdHub provides a private “Installed” tab on your own profile:
|
||||
OpenClaw provides a private “Installed” tab on your own profile:
|
||||
|
||||
- Shows the exact roots + installed skills we store.
|
||||
- Includes a **JSON export** view.
|
||||
@@ -85,7 +85,7 @@ Deleting your account also deletes your telemetry data.
|
||||
Set the environment variable:
|
||||
|
||||
```bash
|
||||
export CLAWDHUB_DISABLE_TELEMETRY=1
|
||||
export CLAWHUB_DISABLE_TELEMETRY=1
|
||||
```
|
||||
|
||||
With this set, the CLI will not send telemetry during `clawdhub sync`.
|
||||
With this set, the CLI will not send telemetry during `clawhub sync`.
|
||||
|
||||
@@ -6,16 +6,16 @@ read_when:
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
## `clawdhub login` opens browser but never completes
|
||||
## `clawhub login` opens browser but never completes
|
||||
|
||||
- Ensure your browser can reach `http://127.0.0.1:<port>/callback` (local firewalls/VPNs can interfere).
|
||||
- Use headless mode:
|
||||
- create a token in the web UI (Settings → API tokens)
|
||||
- `clawdhub login --token clh_...`
|
||||
- `clawhub login --token clh_...`
|
||||
|
||||
## `whoami` / `publish` returns `Unauthorized` (401)
|
||||
|
||||
- Token missing or revoked: check your config file (`CLAWDHUB_CONFIG_PATH` override?).
|
||||
- Token missing or revoked: check your config file (`CLAWHUB_CONFIG_PATH` override?).
|
||||
- Ensure requests include `Authorization: Bearer ...` (CLI does this automatically).
|
||||
|
||||
## `publish` fails with `OPENAI_API_KEY is not configured`
|
||||
@@ -32,7 +32,7 @@ read_when:
|
||||
- Provide explicit roots:
|
||||
|
||||
```bash
|
||||
clawdhub sync --root /path/to/skills
|
||||
clawhub sync --root /path/to/skills
|
||||
```
|
||||
|
||||
## `update` refuses due to “local changes (no match)”
|
||||
@@ -40,8 +40,8 @@ clawdhub sync --root /path/to/skills
|
||||
- Your local files don’t match any published fingerprint.
|
||||
- Options:
|
||||
- keep local edits; skip updating
|
||||
- overwrite: `clawdhub update <slug> --force`
|
||||
- publish as fork: copy to new folder/slug then `clawdhub publish ... --fork-of upstream@version`
|
||||
- overwrite: `clawhub update <slug> --force`
|
||||
- publish as fork: copy to new folder/slug then `clawhub publish ... --fork-of upstream@version`
|
||||
|
||||
## `GET /api/*` works locally but not on Vercel
|
||||
|
||||
|
||||
+4
-4
@@ -6,7 +6,7 @@ read_when:
|
||||
|
||||
# Webhooks (Discord)
|
||||
|
||||
ClawdHub can post Discord embeds when skills are published or highlighted.
|
||||
OpenClaw can post Discord embeds when skills are published or highlighted.
|
||||
|
||||
## Setup
|
||||
|
||||
@@ -14,7 +14,7 @@ Set the webhook URL in the Convex environment:
|
||||
|
||||
- `DISCORD_WEBHOOK_URL` (required): Discord webhook URL.
|
||||
- `DISCORD_WEBHOOK_HIGHLIGHTED_ONLY` (optional): `true` to only send for highlighted skills.
|
||||
- `SITE_URL` (optional): Base site URL for links (default `https://clawdhub.com`).
|
||||
- `SITE_URL` (optional): Base site URL for links (default `https://clawhub.ai`).
|
||||
|
||||
## Events
|
||||
|
||||
@@ -38,13 +38,13 @@ Discord receives a JSON payload with a single embed:
|
||||
{
|
||||
"title": "Demo Skill",
|
||||
"description": "Nice skill",
|
||||
"url": "https://clawdhub.com/owner/demo-skill",
|
||||
"url": "https://clawhub.ai/owner/demo-skill",
|
||||
"fields": [
|
||||
{ "name": "Version", "value": "v1.2.3", "inline": true },
|
||||
{ "name": "Owner", "value": "@owner", "inline": true },
|
||||
{ "name": "Tags", "value": "latest, discord", "inline": false }
|
||||
],
|
||||
"footer": { "text": "ClawdHub" }
|
||||
"footer": { "text": "OpenClaw" }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+64
-50
@@ -9,7 +9,7 @@ import {
|
||||
ApiV1SearchResponseSchema,
|
||||
ApiV1WhoamiResponseSchema,
|
||||
parseArk,
|
||||
} from 'clawdhub-schema'
|
||||
} from 'clawhub-schema'
|
||||
import { unzipSync } from 'fflate'
|
||||
import { Agent, setGlobalDispatcher } from 'undici'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
@@ -29,13 +29,27 @@ try {
|
||||
}
|
||||
|
||||
function mustGetToken() {
|
||||
const fromEnv = process.env.CLAWDHUB_E2E_TOKEN?.trim()
|
||||
const fromEnv = process.env.CLAWHUB_E2E_TOKEN?.trim() || process.env.CLAWDHUB_E2E_TOKEN?.trim()
|
||||
if (fromEnv) return fromEnv
|
||||
return null
|
||||
}
|
||||
|
||||
function getRegistry() {
|
||||
return (
|
||||
process.env.CLAWHUB_REGISTRY?.trim() ||
|
||||
process.env.CLAWDHUB_REGISTRY?.trim() ||
|
||||
'https://clawhub.ai'
|
||||
)
|
||||
}
|
||||
|
||||
function getSite() {
|
||||
return (
|
||||
process.env.CLAWHUB_SITE?.trim() || process.env.CLAWDHUB_SITE?.trim() || 'https://clawhub.ai'
|
||||
)
|
||||
}
|
||||
|
||||
async function makeTempConfig(registry: string, token: string | null) {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'clawdhub-e2e-'))
|
||||
const dir = await mkdtemp(join(tmpdir(), 'clawhub-e2e-'))
|
||||
const path = join(dir, 'config.json')
|
||||
await writeFile(
|
||||
path,
|
||||
@@ -55,9 +69,9 @@ async function fetchWithTimeout(input: RequestInfo | URL, init?: RequestInit) {
|
||||
}
|
||||
}
|
||||
|
||||
describe('clawdhub e2e', () => {
|
||||
describe('clawhub e2e', () => {
|
||||
it('prints CLI version via --cli-version', async () => {
|
||||
const result = spawnSync('bun', ['clawdhub', '--cli-version'], {
|
||||
const result = spawnSync('bun', ['clawhub', '--cli-version'], {
|
||||
cwd: process.cwd(),
|
||||
encoding: 'utf8',
|
||||
})
|
||||
@@ -66,7 +80,7 @@ describe('clawdhub e2e', () => {
|
||||
})
|
||||
|
||||
it('search endpoint returns a results array (schema parse)', async () => {
|
||||
const registry = process.env.CLAWDHUB_REGISTRY?.trim() || 'https://clawdhub.com'
|
||||
const registry = getRegistry()
|
||||
const url = new URL(ApiRoutes.search, registry)
|
||||
url.searchParams.set('q', 'gif')
|
||||
url.searchParams.set('limit', '5')
|
||||
@@ -81,17 +95,17 @@ describe('clawdhub e2e', () => {
|
||||
})
|
||||
|
||||
it('cli search does not error on multi-result responses', async () => {
|
||||
const registry = process.env.CLAWDHUB_REGISTRY?.trim() || 'https://clawdhub.com'
|
||||
const site = process.env.CLAWDHUB_SITE?.trim() || 'https://clawdhub.com'
|
||||
const registry = getRegistry()
|
||||
const site = getSite()
|
||||
const token = mustGetToken() ?? (await readGlobalConfig())?.token ?? null
|
||||
|
||||
const cfg = await makeTempConfig(registry, token)
|
||||
try {
|
||||
const workdir = await mkdtemp(join(tmpdir(), 'clawdhub-e2e-workdir-'))
|
||||
const workdir = await mkdtemp(join(tmpdir(), 'clawhub-e2e-workdir-'))
|
||||
const result = spawnSync(
|
||||
'bun',
|
||||
[
|
||||
'clawdhub',
|
||||
'clawhub',
|
||||
'search',
|
||||
'gif',
|
||||
'--limit',
|
||||
@@ -105,7 +119,7 @@ describe('clawdhub e2e', () => {
|
||||
],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
env: { ...process.env, CLAWDHUB_CONFIG_PATH: cfg.path, CLAWDHUB_DISABLE_TELEMETRY: '1' },
|
||||
env: { ...process.env, CLAWHUB_CONFIG_PATH: cfg.path, CLAWHUB_DISABLE_TELEMETRY: '1' },
|
||||
encoding: 'utf8',
|
||||
},
|
||||
)
|
||||
@@ -119,11 +133,11 @@ describe('clawdhub e2e', () => {
|
||||
})
|
||||
|
||||
it('assumes a logged-in user (whoami succeeds)', async () => {
|
||||
const registry = process.env.CLAWDHUB_REGISTRY?.trim() || 'https://clawdhub.com'
|
||||
const site = process.env.CLAWDHUB_SITE?.trim() || 'https://clawdhub.com'
|
||||
const registry = getRegistry()
|
||||
const site = getSite()
|
||||
const token = mustGetToken() ?? (await readGlobalConfig())?.token ?? null
|
||||
if (!token) {
|
||||
throw new Error('Missing token. Set CLAWDHUB_E2E_TOKEN or run: bun clawdhub auth login')
|
||||
throw new Error('Missing token. Set CLAWHUB_E2E_TOKEN or run: bun clawhub auth login')
|
||||
}
|
||||
|
||||
const cfg = await makeTempConfig(registry, token)
|
||||
@@ -142,10 +156,10 @@ describe('clawdhub e2e', () => {
|
||||
|
||||
const result = spawnSync(
|
||||
'bun',
|
||||
['clawdhub', 'whoami', '--site', site, '--registry', registry],
|
||||
['clawhub', 'whoami', '--site', site, '--registry', registry],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
env: { ...process.env, CLAWDHUB_CONFIG_PATH: cfg.path, CLAWDHUB_DISABLE_TELEMETRY: '1' },
|
||||
env: { ...process.env, CLAWHUB_CONFIG_PATH: cfg.path, CLAWHUB_DISABLE_TELEMETRY: '1' },
|
||||
encoding: 'utf8',
|
||||
},
|
||||
)
|
||||
@@ -157,15 +171,15 @@ describe('clawdhub e2e', () => {
|
||||
})
|
||||
|
||||
it('sync dry-run finds skills from an explicit root', async () => {
|
||||
const registry = process.env.CLAWDHUB_REGISTRY?.trim() || 'https://clawdhub.com'
|
||||
const site = process.env.CLAWDHUB_SITE?.trim() || 'https://clawdhub.com'
|
||||
const registry = getRegistry()
|
||||
const site = getSite()
|
||||
const token = mustGetToken() ?? (await readGlobalConfig())?.token ?? null
|
||||
if (!token) {
|
||||
throw new Error('Missing token. Set CLAWDHUB_E2E_TOKEN or run: bun clawdhub auth login')
|
||||
throw new Error('Missing token. Set CLAWHUB_E2E_TOKEN or run: bun clawhub auth login')
|
||||
}
|
||||
|
||||
const cfg = await makeTempConfig(registry, token)
|
||||
const root = await mkdtemp(join(tmpdir(), 'clawdhub-e2e-sync-'))
|
||||
const root = await mkdtemp(join(tmpdir(), 'clawhub-e2e-sync-'))
|
||||
try {
|
||||
const skillDir = join(root, 'cool-skill')
|
||||
await mkdir(skillDir, { recursive: true })
|
||||
@@ -174,7 +188,7 @@ describe('clawdhub e2e', () => {
|
||||
const result = spawnSync(
|
||||
'bun',
|
||||
[
|
||||
'clawdhub',
|
||||
'clawhub',
|
||||
'sync',
|
||||
'--dry-run',
|
||||
'--all',
|
||||
@@ -187,7 +201,7 @@ describe('clawdhub e2e', () => {
|
||||
],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
env: { ...process.env, CLAWDHUB_CONFIG_PATH: cfg.path, CLAWDHUB_DISABLE_TELEMETRY: '1' },
|
||||
env: { ...process.env, CLAWHUB_CONFIG_PATH: cfg.path, CLAWHUB_DISABLE_TELEMETRY: '1' },
|
||||
encoding: 'utf8',
|
||||
},
|
||||
)
|
||||
@@ -201,15 +215,15 @@ describe('clawdhub e2e', () => {
|
||||
})
|
||||
|
||||
it('sync dry-run finds skills from clawdbot.json roots', async () => {
|
||||
const registry = process.env.CLAWDHUB_REGISTRY?.trim() || 'https://clawdhub.com'
|
||||
const site = process.env.CLAWDHUB_SITE?.trim() || 'https://clawdhub.com'
|
||||
const registry = getRegistry()
|
||||
const site = getSite()
|
||||
const token = mustGetToken() ?? (await readGlobalConfig())?.token ?? null
|
||||
if (!token) {
|
||||
throw new Error('Missing token. Set CLAWDHUB_E2E_TOKEN or run: bun clawdhub auth login')
|
||||
throw new Error('Missing token. Set CLAWHUB_E2E_TOKEN or run: bun clawhub auth login')
|
||||
}
|
||||
|
||||
const cfg = await makeTempConfig(registry, token)
|
||||
const root = await mkdtemp(join(tmpdir(), 'clawdhub-e2e-clawdbot-'))
|
||||
const root = await mkdtemp(join(tmpdir(), 'clawhub-e2e-clawdbot-'))
|
||||
const stateDir = join(root, 'state')
|
||||
const configPath = join(root, 'clawdbot.json')
|
||||
const workspace = join(root, 'clawd-work')
|
||||
@@ -232,13 +246,13 @@ describe('clawdhub e2e', () => {
|
||||
|
||||
const result = spawnSync(
|
||||
'bun',
|
||||
['clawdhub', 'sync', '--dry-run', '--all', '--site', site, '--registry', registry],
|
||||
['clawhub', 'sync', '--dry-run', '--all', '--site', site, '--registry', registry],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
CLAWDHUB_CONFIG_PATH: cfg.path,
|
||||
CLAWDHUB_DISABLE_TELEMETRY: '1',
|
||||
CLAWHUB_CONFIG_PATH: cfg.path,
|
||||
CLAWHUB_DISABLE_TELEMETRY: '1',
|
||||
CLAWDBOT_CONFIG_PATH: configPath,
|
||||
CLAWDBOT_STATE_DIR: stateDir,
|
||||
},
|
||||
@@ -256,16 +270,16 @@ describe('clawdhub e2e', () => {
|
||||
})
|
||||
|
||||
it('publishes, deletes, and undeletes a skill (logged-in)', async () => {
|
||||
const registry = process.env.CLAWDHUB_REGISTRY?.trim() || 'https://clawdhub.com'
|
||||
const site = process.env.CLAWDHUB_SITE?.trim() || 'https://clawdhub.com'
|
||||
const registry = getRegistry()
|
||||
const site = getSite()
|
||||
const token = mustGetToken() ?? (await readGlobalConfig())?.token ?? null
|
||||
if (!token) {
|
||||
throw new Error('Missing token. Set CLAWDHUB_E2E_TOKEN or run: bun clawdhub auth login')
|
||||
throw new Error('Missing token. Set CLAWHUB_E2E_TOKEN or run: bun clawhub auth login')
|
||||
}
|
||||
|
||||
const cfg = await makeTempConfig(registry, token)
|
||||
const workdir = await mkdtemp(join(tmpdir(), 'clawdhub-e2e-publish-'))
|
||||
const installWorkdir = await mkdtemp(join(tmpdir(), 'clawdhub-e2e-install-'))
|
||||
const workdir = await mkdtemp(join(tmpdir(), 'clawhub-e2e-publish-'))
|
||||
const installWorkdir = await mkdtemp(join(tmpdir(), 'clawhub-e2e-install-'))
|
||||
const slug = `e2e-${Date.now()}`
|
||||
const skillDir = join(workdir, slug)
|
||||
|
||||
@@ -276,7 +290,7 @@ describe('clawdhub e2e', () => {
|
||||
const publish1 = spawnSync(
|
||||
'bun',
|
||||
[
|
||||
'clawdhub',
|
||||
'clawhub',
|
||||
'publish',
|
||||
skillDir,
|
||||
'--slug',
|
||||
@@ -296,7 +310,7 @@ describe('clawdhub e2e', () => {
|
||||
],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
env: { ...process.env, CLAWDHUB_CONFIG_PATH: cfg.path, CLAWDHUB_DISABLE_TELEMETRY: '1' },
|
||||
env: { ...process.env, CLAWHUB_CONFIG_PATH: cfg.path, CLAWHUB_DISABLE_TELEMETRY: '1' },
|
||||
encoding: 'utf8',
|
||||
},
|
||||
)
|
||||
@@ -306,7 +320,7 @@ describe('clawdhub e2e', () => {
|
||||
const publish2 = spawnSync(
|
||||
'bun',
|
||||
[
|
||||
'clawdhub',
|
||||
'clawhub',
|
||||
'publish',
|
||||
skillDir,
|
||||
'--slug',
|
||||
@@ -326,7 +340,7 @@ describe('clawdhub e2e', () => {
|
||||
],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
env: { ...process.env, CLAWDHUB_CONFIG_PATH: cfg.path, CLAWDHUB_DISABLE_TELEMETRY: '1' },
|
||||
env: { ...process.env, CLAWHUB_CONFIG_PATH: cfg.path, CLAWHUB_DISABLE_TELEMETRY: '1' },
|
||||
encoding: 'utf8',
|
||||
},
|
||||
)
|
||||
@@ -345,7 +359,7 @@ describe('clawdhub e2e', () => {
|
||||
const install = spawnSync(
|
||||
'bun',
|
||||
[
|
||||
'clawdhub',
|
||||
'clawhub',
|
||||
'install',
|
||||
slug,
|
||||
'--version',
|
||||
@@ -360,7 +374,7 @@ describe('clawdhub e2e', () => {
|
||||
],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
env: { ...process.env, CLAWDHUB_CONFIG_PATH: cfg.path, CLAWDHUB_DISABLE_TELEMETRY: '1' },
|
||||
env: { ...process.env, CLAWHUB_CONFIG_PATH: cfg.path, CLAWHUB_DISABLE_TELEMETRY: '1' },
|
||||
encoding: 'utf8',
|
||||
},
|
||||
)
|
||||
@@ -368,10 +382,10 @@ describe('clawdhub e2e', () => {
|
||||
|
||||
const list = spawnSync(
|
||||
'bun',
|
||||
['clawdhub', 'list', '--site', site, '--registry', registry, '--workdir', installWorkdir],
|
||||
['clawhub', 'list', '--site', site, '--registry', registry, '--workdir', installWorkdir],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
env: { ...process.env, CLAWDHUB_CONFIG_PATH: cfg.path, CLAWDHUB_DISABLE_TELEMETRY: '1' },
|
||||
env: { ...process.env, CLAWHUB_CONFIG_PATH: cfg.path, CLAWHUB_DISABLE_TELEMETRY: '1' },
|
||||
encoding: 'utf8',
|
||||
},
|
||||
)
|
||||
@@ -381,7 +395,7 @@ describe('clawdhub e2e', () => {
|
||||
const update = spawnSync(
|
||||
'bun',
|
||||
[
|
||||
'clawdhub',
|
||||
'clawhub',
|
||||
'update',
|
||||
slug,
|
||||
'--force',
|
||||
@@ -394,7 +408,7 @@ describe('clawdhub e2e', () => {
|
||||
],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
env: { ...process.env, CLAWDHUB_CONFIG_PATH: cfg.path, CLAWDHUB_DISABLE_TELEMETRY: '1' },
|
||||
env: { ...process.env, CLAWHUB_CONFIG_PATH: cfg.path, CLAWHUB_DISABLE_TELEMETRY: '1' },
|
||||
encoding: 'utf8',
|
||||
},
|
||||
)
|
||||
@@ -409,7 +423,7 @@ describe('clawdhub e2e', () => {
|
||||
const del = spawnSync(
|
||||
'bun',
|
||||
[
|
||||
'clawdhub',
|
||||
'clawhub',
|
||||
'delete',
|
||||
slug,
|
||||
'--yes',
|
||||
@@ -422,7 +436,7 @@ describe('clawdhub e2e', () => {
|
||||
],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
env: { ...process.env, CLAWDHUB_CONFIG_PATH: cfg.path, CLAWDHUB_DISABLE_TELEMETRY: '1' },
|
||||
env: { ...process.env, CLAWHUB_CONFIG_PATH: cfg.path, CLAWHUB_DISABLE_TELEMETRY: '1' },
|
||||
encoding: 'utf8',
|
||||
},
|
||||
)
|
||||
@@ -439,7 +453,7 @@ describe('clawdhub e2e', () => {
|
||||
const undelete = spawnSync(
|
||||
'bun',
|
||||
[
|
||||
'clawdhub',
|
||||
'clawhub',
|
||||
'undelete',
|
||||
slug,
|
||||
'--yes',
|
||||
@@ -452,7 +466,7 @@ describe('clawdhub e2e', () => {
|
||||
],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
env: { ...process.env, CLAWDHUB_CONFIG_PATH: cfg.path, CLAWDHUB_DISABLE_TELEMETRY: '1' },
|
||||
env: { ...process.env, CLAWHUB_CONFIG_PATH: cfg.path, CLAWHUB_DISABLE_TELEMETRY: '1' },
|
||||
encoding: 'utf8',
|
||||
},
|
||||
)
|
||||
@@ -466,7 +480,7 @@ describe('clawdhub e2e', () => {
|
||||
const cleanup = spawnSync(
|
||||
'bun',
|
||||
[
|
||||
'clawdhub',
|
||||
'clawhub',
|
||||
'delete',
|
||||
slug,
|
||||
'--yes',
|
||||
@@ -479,7 +493,7 @@ describe('clawdhub e2e', () => {
|
||||
],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
env: { ...process.env, CLAWDHUB_CONFIG_PATH: cfg.path, CLAWDHUB_DISABLE_TELEMETRY: '1' },
|
||||
env: { ...process.env, CLAWHUB_CONFIG_PATH: cfg.path, CLAWHUB_DISABLE_TELEMETRY: '1' },
|
||||
encoding: 'utf8',
|
||||
},
|
||||
)
|
||||
|
||||
@@ -43,7 +43,7 @@ test('header menu routes render', async ({ page }) => {
|
||||
|
||||
if (label === 'Search') {
|
||||
await expect(page).toHaveURL(/\/?(\?|$)/)
|
||||
await expect(page.locator('h1', { hasText: 'ClawdHub' })).toBeVisible()
|
||||
await expect(page.locator('h1', { hasText: 'OpenClaw' })).toBeVisible()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
+6
-3
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "clawdhub",
|
||||
"name": "clawhub",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"preinstall": "bunx only-allow bun",
|
||||
"dev": "bun --bun vite dev --port 3000",
|
||||
"build": "bun --bun vite build",
|
||||
"preview": "bun --bun vite preview",
|
||||
@@ -40,9 +41,10 @@
|
||||
"@tanstack/react-start": "^1.152.0",
|
||||
"@tanstack/router-plugin": "^1.151.6",
|
||||
"@vercel/analytics": "^1.6.1",
|
||||
"clawdhub-schema": "workspace:*",
|
||||
"clawhub-schema": "workspace:*",
|
||||
"clsx": "^2.1.1",
|
||||
"convex": "^1.31.5",
|
||||
"convex": "^1.31.6",
|
||||
"convex-helpers": "^0.1.111",
|
||||
"fflate": "^0.8.2",
|
||||
"h3": "2.0.1-rc.8",
|
||||
"lucide-react": "^0.562.0",
|
||||
@@ -71,6 +73,7 @@
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"@vitest/coverage-v8": "^4.0.17",
|
||||
"jsdom": "^27.4.0",
|
||||
"only-allow": "^1.2.1",
|
||||
"oxlint": "^1.39.0",
|
||||
"oxlint-tsgolint": "^0.11.1",
|
||||
"typescript": "^5.9.3",
|
||||
|
||||
+20
-20
@@ -1,57 +1,57 @@
|
||||
# `clawdhub`
|
||||
# `clawhub`
|
||||
|
||||
ClawdHub CLI — install, update, search, and publish agent skills as folders.
|
||||
OpenClaw CLI — install, update, search, and publish agent skills as folders.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
# From this repo (shortcut script at repo root)
|
||||
bun clawdhub --help
|
||||
bun clawhub --help
|
||||
|
||||
# Once published to npm
|
||||
# npm i -g clawdhub
|
||||
# npm i -g clawhub
|
||||
```
|
||||
|
||||
## Auth (publish)
|
||||
|
||||
```bash
|
||||
clawdhub login
|
||||
clawhub login
|
||||
# or
|
||||
clawdhub auth login
|
||||
clawhub auth login
|
||||
|
||||
# Headless / token paste
|
||||
# or (token paste / headless)
|
||||
clawdhub login --token clh_...
|
||||
clawhub login --token clh_...
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Browser login opens `https://clawdhub.com/cli/auth` and completes via a loopback callback.
|
||||
- Token stored in `~/Library/Application Support/clawdhub/config.json` on macOS (override via `CLAWDHUB_CONFIG_PATH`).
|
||||
- Browser login opens `https://clawhub.ai/cli/auth` and completes via a loopback callback.
|
||||
- Token stored in `~/Library/Application Support/clawhub/config.json` on macOS (override via `CLAWHUB_CONFIG_PATH`, legacy `CLAWDHUB_CONFIG_PATH`).
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
clawdhub search "postgres backups"
|
||||
clawdhub install my-skill-pack
|
||||
clawdhub update --all
|
||||
clawdhub update --all --no-input --force
|
||||
clawdhub publish ./my-skill-pack --slug my-skill-pack --name "My Skill Pack" --version 1.2.0 --changelog "Fixes + docs"
|
||||
clawhub search "postgres backups"
|
||||
clawhub install my-skill-pack
|
||||
clawhub update --all
|
||||
clawhub update --all --no-input --force
|
||||
clawhub publish ./my-skill-pack --slug my-skill-pack --name "My Skill Pack" --version 1.2.0 --changelog "Fixes + docs"
|
||||
```
|
||||
|
||||
## Sync (upload local skills)
|
||||
|
||||
```bash
|
||||
# Start anywhere; scans workdir first, then legacy Clawdis/Clawd locations.
|
||||
clawdhub sync
|
||||
# Start anywhere; scans workdir first, then legacy Clawdis/Clawd/OpenClaw/Moltbot locations.
|
||||
clawhub sync
|
||||
|
||||
# Explicit roots + non-interactive dry-run
|
||||
clawdhub sync --root ../clawdis/skills --all --dry-run
|
||||
clawhub sync --root ../clawdis/skills --all --dry-run
|
||||
```
|
||||
|
||||
## Defaults
|
||||
|
||||
- Site: `https://clawdhub.com` (override via `--site` or `CLAWDHUB_SITE`)
|
||||
- Registry: discovered from `/.well-known/clawdhub.json` on the site (override via `--registry` or `CLAWDHUB_REGISTRY`)
|
||||
- Workdir: current directory (falls back to Clawdbot workspace if configured; override via `--workdir` or `CLAWDHUB_WORKDIR`)
|
||||
- Site: `https://clawhub.ai` (override via `--site` or `CLAWHUB_SITE`, legacy `CLAWDHUB_SITE`)
|
||||
- Registry: discovered from `/.well-known/clawhub.json` on the site (legacy `/.well-known/clawdhub.json`; override via `--registry` or `CLAWHUB_REGISTRY`)
|
||||
- Workdir: current directory (falls back to Clawdbot workspace if configured; override via `--workdir` or `CLAWHUB_WORKDIR`)
|
||||
- Install dir: `./skills` under workdir (override via `--dir`)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"name": "clawdhub",
|
||||
"version": "0.3.0",
|
||||
"description": "ClawdHub CLI \\u2014 install, update, search, and publish agent skills.",
|
||||
"name": "clawhub",
|
||||
"version": "0.4.0",
|
||||
"description": "OpenClaw CLI \\u2014 install, update, search, and publish agent skills.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"clawhub": "bin/clawdhub.js",
|
||||
"clawdhub": "bin/clawdhub.js"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -60,7 +60,7 @@ describe('browserAuth', () => {
|
||||
const response = await fetch(server.redirectUri)
|
||||
expect(response.status).toBe(200)
|
||||
const text = await response.text()
|
||||
expect(text).toContain('ClawdHub CLI Login')
|
||||
expect(text).toContain('OpenClaw CLI Login')
|
||||
server.close()
|
||||
})
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ const CALLBACK_HTML = `<!doctype html>
|
||||
<html lang="en">
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>ClawdHub CLI Login</title>
|
||||
<title>OpenClaw CLI Login</title>
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
body { font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif; padding: 24px; }
|
||||
|
||||
@@ -18,9 +18,9 @@ import { fail } from './cli/ui.js'
|
||||
import { readGlobalConfig } from './config.js'
|
||||
|
||||
const program = new Command()
|
||||
.name('clawdhub')
|
||||
.name('clawhub')
|
||||
.description(
|
||||
`${styleTitle(`ClawdHub CLI ${getCliBuildLabel()}`)}\n${styleEnvBlock(
|
||||
`${styleTitle(`OpenClaw CLI ${getCliBuildLabel()}`)}\n${styleEnvBlock(
|
||||
'install, update, search, and publish agent skills.',
|
||||
)}`,
|
||||
)
|
||||
@@ -34,7 +34,9 @@ const program = new Command()
|
||||
.showSuggestionAfterError()
|
||||
.addHelpText(
|
||||
'after',
|
||||
styleEnvBlock('\nEnv:\n CLAWDHUB_SITE\n CLAWDHUB_REGISTRY\n CLAWDHUB_WORKDIR\n'),
|
||||
styleEnvBlock(
|
||||
'\nEnv:\n CLAWHUB_SITE\n CLAWHUB_REGISTRY\n CLAWHUB_WORKDIR\n (CLAWDHUB_* supported)\n',
|
||||
),
|
||||
)
|
||||
|
||||
configureCommanderHelp(program)
|
||||
@@ -43,9 +45,17 @@ async function resolveGlobalOpts(): Promise<GlobalOpts> {
|
||||
const raw = program.opts<{ workdir?: string; dir?: string; site?: string; registry?: string }>()
|
||||
const workdir = await resolveWorkdir(raw.workdir)
|
||||
const dir = resolve(workdir, raw.dir ?? 'skills')
|
||||
const site = raw.site ?? process.env.CLAWDHUB_SITE ?? DEFAULT_SITE
|
||||
const registrySource = raw.registry ? 'cli' : process.env.CLAWDHUB_REGISTRY ? 'env' : 'default'
|
||||
const registry = raw.registry ?? process.env.CLAWDHUB_REGISTRY ?? DEFAULT_REGISTRY
|
||||
const site = raw.site ?? process.env.CLAWHUB_SITE ?? process.env.CLAWDHUB_SITE ?? DEFAULT_SITE
|
||||
const registrySource = raw.registry
|
||||
? 'cli'
|
||||
: process.env.CLAWHUB_REGISTRY || process.env.CLAWDHUB_REGISTRY
|
||||
? 'env'
|
||||
: 'default'
|
||||
const registry =
|
||||
raw.registry ??
|
||||
process.env.CLAWHUB_REGISTRY ??
|
||||
process.env.CLAWDHUB_REGISTRY ??
|
||||
DEFAULT_REGISTRY
|
||||
return { workdir, dir, site, registry, registrySource }
|
||||
}
|
||||
|
||||
@@ -56,22 +66,26 @@ function isInputAllowed() {
|
||||
|
||||
async function resolveWorkdir(explicit?: string) {
|
||||
if (explicit?.trim()) return resolve(explicit.trim())
|
||||
const envWorkdir = process.env.CLAWDHUB_WORKDIR?.trim()
|
||||
const envWorkdir = process.env.CLAWHUB_WORKDIR?.trim() ?? process.env.CLAWDHUB_WORKDIR?.trim()
|
||||
if (envWorkdir) return resolve(envWorkdir)
|
||||
|
||||
const cwd = resolve(process.cwd())
|
||||
const hasMarker = await hasClawdhubMarker(cwd)
|
||||
const hasMarker = await hasClawhubMarker(cwd)
|
||||
if (hasMarker) return cwd
|
||||
|
||||
const clawdbotWorkspace = await resolveClawdbotDefaultWorkspace()
|
||||
return clawdbotWorkspace ? resolve(clawdbotWorkspace) : cwd
|
||||
}
|
||||
|
||||
async function hasClawdhubMarker(workdir: string) {
|
||||
const lockfile = join(workdir, '.clawdhub', 'lock.json')
|
||||
async function hasClawhubMarker(workdir: string) {
|
||||
const lockfile = join(workdir, '.clawhub', 'lock.json')
|
||||
if (await pathExists(lockfile)) return true
|
||||
const markerDir = join(workdir, '.clawdhub')
|
||||
return pathExists(markerDir)
|
||||
const markerDir = join(workdir, '.clawhub')
|
||||
if (await pathExists(markerDir)) return true
|
||||
const legacyLockfile = join(workdir, '.clawdhub', 'lock.json')
|
||||
if (await pathExists(legacyLockfile)) return true
|
||||
const legacyMarkerDir = join(workdir, '.clawdhub')
|
||||
return pathExists(legacyMarkerDir)
|
||||
}
|
||||
|
||||
async function pathExists(path: string) {
|
||||
|
||||
@@ -24,6 +24,7 @@ function shortCommit(value: string) {
|
||||
|
||||
export function getCliCommit() {
|
||||
const candidates = [
|
||||
process.env.CLAWHUB_COMMIT,
|
||||
process.env.CLAWDHUB_COMMIT,
|
||||
process.env.VERCEL_GIT_COMMIT_SHA,
|
||||
process.env.GITHUB_SHA,
|
||||
|
||||
@@ -13,14 +13,17 @@ afterEach(() => {
|
||||
|
||||
describe('resolveClawdbotSkillRoots', () => {
|
||||
it('reads JSON5 config and resolves per-agent + shared skill roots', async () => {
|
||||
const base = await mkdtemp(join(tmpdir(), 'clawdhub-clawdbot-'))
|
||||
const base = await mkdtemp(join(tmpdir(), 'clawhub-clawdbot-'))
|
||||
const home = join(base, 'home')
|
||||
const stateDir = join(base, 'state')
|
||||
const configPath = join(base, 'clawdbot.json')
|
||||
const openclawStateDir = join(base, 'openclaw-state')
|
||||
|
||||
process.env.HOME = home
|
||||
process.env.CLAWDBOT_STATE_DIR = stateDir
|
||||
process.env.CLAWDBOT_CONFIG_PATH = configPath
|
||||
process.env.OPENCLAW_STATE_DIR = openclawStateDir
|
||||
process.env.OPENCLAW_CONFIG_PATH = join(openclawStateDir, 'openclaw.json')
|
||||
|
||||
const config = `{
|
||||
// JSON5 comments + trailing commas supported
|
||||
@@ -49,6 +52,7 @@ describe('resolveClawdbotSkillRoots', () => {
|
||||
|
||||
const expectedRoots = [
|
||||
resolve(stateDir, 'skills'),
|
||||
resolve(openclawStateDir, 'skills'),
|
||||
resolve(home, 'clawd-main', 'skills'),
|
||||
resolve(home, 'clawd-work', 'skills'),
|
||||
resolve(home, 'clawd-family', 'skills'),
|
||||
@@ -58,6 +62,7 @@ describe('resolveClawdbotSkillRoots', () => {
|
||||
|
||||
expect(roots).toEqual(expect.arrayContaining(expectedRoots))
|
||||
expect(labels[resolve(stateDir, 'skills')]).toBe('Shared skills')
|
||||
expect(labels[resolve(openclawStateDir, 'skills')]).toBe('OpenClaw: Shared skills')
|
||||
expect(labels[resolve(home, 'clawd-main', 'skills')]).toBe('Agent: main')
|
||||
expect(labels[resolve(home, 'clawd-work', 'skills')]).toBe('Agent: Work Bot')
|
||||
expect(labels[resolve(home, 'clawd-family', 'skills')]).toBe('Agent: family')
|
||||
@@ -66,16 +71,19 @@ describe('resolveClawdbotSkillRoots', () => {
|
||||
})
|
||||
|
||||
it('resolves default workspace from agents.defaults and agents.list', async () => {
|
||||
const base = await mkdtemp(join(tmpdir(), 'clawdhub-clawdbot-default-'))
|
||||
const base = await mkdtemp(join(tmpdir(), 'clawhub-clawdbot-default-'))
|
||||
const home = join(base, 'home')
|
||||
const stateDir = join(base, 'state')
|
||||
const configPath = join(base, 'clawdbot.json')
|
||||
const workspaceMain = join(base, 'workspace-main')
|
||||
const workspaceList = join(base, 'workspace-list')
|
||||
const openclawStateDir = join(base, 'openclaw-state')
|
||||
|
||||
process.env.HOME = home
|
||||
process.env.CLAWDBOT_STATE_DIR = stateDir
|
||||
process.env.CLAWDBOT_CONFIG_PATH = configPath
|
||||
process.env.OPENCLAW_STATE_DIR = openclawStateDir
|
||||
process.env.OPENCLAW_CONFIG_PATH = join(openclawStateDir, 'openclaw.json')
|
||||
|
||||
const config = `{
|
||||
agents: {
|
||||
@@ -92,14 +100,17 @@ describe('resolveClawdbotSkillRoots', () => {
|
||||
})
|
||||
|
||||
it('falls back to default agent in agents.list when defaults missing', async () => {
|
||||
const base = await mkdtemp(join(tmpdir(), 'clawdhub-clawdbot-list-'))
|
||||
const base = await mkdtemp(join(tmpdir(), 'clawhub-clawdbot-list-'))
|
||||
const home = join(base, 'home')
|
||||
const configPath = join(base, 'clawdbot.json')
|
||||
const workspaceMain = join(base, 'workspace-main')
|
||||
const workspaceWork = join(base, 'workspace-work')
|
||||
const openclawStateDir = join(base, 'openclaw-state')
|
||||
|
||||
process.env.HOME = home
|
||||
process.env.CLAWDBOT_CONFIG_PATH = configPath
|
||||
process.env.OPENCLAW_STATE_DIR = openclawStateDir
|
||||
process.env.OPENCLAW_CONFIG_PATH = join(openclawStateDir, 'openclaw.json')
|
||||
|
||||
const config = `{
|
||||
agents: {
|
||||
@@ -116,14 +127,17 @@ describe('resolveClawdbotSkillRoots', () => {
|
||||
})
|
||||
|
||||
it('respects CLAWDBOT_STATE_DIR and CLAWDBOT_CONFIG_PATH overrides', async () => {
|
||||
const base = await mkdtemp(join(tmpdir(), 'clawdhub-clawdbot-override-'))
|
||||
const base = await mkdtemp(join(tmpdir(), 'clawhub-clawdbot-override-'))
|
||||
const home = join(base, 'home')
|
||||
const stateDir = join(base, 'custom-state')
|
||||
const configPath = join(base, 'config', 'clawdbot.json')
|
||||
const openclawStateDir = join(base, 'openclaw-state')
|
||||
|
||||
process.env.HOME = home
|
||||
process.env.CLAWDBOT_STATE_DIR = stateDir
|
||||
process.env.CLAWDBOT_CONFIG_PATH = configPath
|
||||
process.env.OPENCLAW_STATE_DIR = openclawStateDir
|
||||
process.env.OPENCLAW_CONFIG_PATH = join(openclawStateDir, 'openclaw.json')
|
||||
|
||||
const config = `{
|
||||
agent: { workspace: "${join(base, 'workspace-main')}" },
|
||||
@@ -136,24 +150,54 @@ describe('resolveClawdbotSkillRoots', () => {
|
||||
expect(roots).toEqual(
|
||||
expect.arrayContaining([
|
||||
resolve(stateDir, 'skills'),
|
||||
resolve(openclawStateDir, 'skills'),
|
||||
resolve(join(base, 'workspace-main'), 'skills'),
|
||||
]),
|
||||
)
|
||||
expect(labels[resolve(stateDir, 'skills')]).toBe('Shared skills')
|
||||
expect(labels[resolve(openclawStateDir, 'skills')]).toBe('OpenClaw: Shared skills')
|
||||
expect(labels[resolve(join(base, 'workspace-main'), 'skills')]).toBe('Agent: main')
|
||||
})
|
||||
|
||||
it('returns shared skills root when config is missing', async () => {
|
||||
const base = await mkdtemp(join(tmpdir(), 'clawdhub-clawdbot-missing-'))
|
||||
const base = await mkdtemp(join(tmpdir(), 'clawhub-clawdbot-missing-'))
|
||||
const stateDir = join(base, 'state')
|
||||
const configPath = join(base, 'missing', 'clawdbot.json')
|
||||
const openclawStateDir = join(base, 'openclaw-state')
|
||||
|
||||
process.env.CLAWDBOT_STATE_DIR = stateDir
|
||||
process.env.CLAWDBOT_CONFIG_PATH = configPath
|
||||
process.env.OPENCLAW_STATE_DIR = openclawStateDir
|
||||
process.env.OPENCLAW_CONFIG_PATH = join(openclawStateDir, 'openclaw.json')
|
||||
|
||||
const { roots, labels } = await resolveClawdbotSkillRoots()
|
||||
|
||||
expect(roots).toEqual([resolve(stateDir, 'skills')])
|
||||
expect(roots).toEqual([resolve(stateDir, 'skills'), resolve(openclawStateDir, 'skills')])
|
||||
expect(labels[resolve(stateDir, 'skills')]).toBe('Shared skills')
|
||||
expect(labels[resolve(openclawStateDir, 'skills')]).toBe('OpenClaw: Shared skills')
|
||||
})
|
||||
|
||||
it('supports OpenClaw configuration files', async () => {
|
||||
const base = await mkdtemp(join(tmpdir(), 'clawhub-openclaw-'))
|
||||
const stateDir = join(base, 'openclaw-state')
|
||||
const workspace = join(base, 'openclaw-main')
|
||||
const configPath = join(stateDir, 'openclaw.json')
|
||||
|
||||
process.env.OPENCLAW_STATE_DIR = stateDir
|
||||
|
||||
await mkdir(stateDir, { recursive: true })
|
||||
const config = `{
|
||||
agents: {
|
||||
defaults: { workspace: "${workspace}", },
|
||||
},
|
||||
}`
|
||||
await writeFile(configPath, config, 'utf8')
|
||||
|
||||
const { roots, labels } = await resolveClawdbotSkillRoots()
|
||||
expect(roots).toEqual(
|
||||
expect.arrayContaining([resolve(stateDir, 'skills'), resolve(workspace, 'skills')]),
|
||||
)
|
||||
expect(labels[resolve(stateDir, 'skills')]).toBe('OpenClaw: Shared skills')
|
||||
expect(labels[resolve(workspace, 'skills')]).toBe('OpenClaw: Agent: main')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -39,42 +39,25 @@ export async function resolveClawdbotSkillRoots(): Promise<ClawdbotSkillRoots> {
|
||||
const roots: string[] = []
|
||||
const labels: Record<string, string> = {}
|
||||
|
||||
const stateDir = resolveClawdbotStateDir()
|
||||
const sharedSkills = resolveUserPath(join(stateDir, 'skills'))
|
||||
const clawdbotStateDir = resolveClawdbotStateDir()
|
||||
const sharedSkills = resolveUserPath(join(clawdbotStateDir, 'skills'))
|
||||
pushRoot(roots, labels, sharedSkills, 'Shared skills')
|
||||
|
||||
const config = await readClawdbotConfig()
|
||||
if (!config) return { roots, labels }
|
||||
const openclawStateDir = resolveOpenclawStateDir()
|
||||
const openclawShared = resolveUserPath(join(openclawStateDir, 'skills'))
|
||||
pushRoot(roots, labels, openclawShared, 'OpenClaw: Shared skills')
|
||||
|
||||
const mainWorkspace = resolveUserPath(
|
||||
config.agents?.defaults?.workspace ?? config.agent?.workspace ?? '',
|
||||
)
|
||||
if (mainWorkspace) {
|
||||
pushRoot(roots, labels, join(mainWorkspace, 'skills'), 'Agent: main')
|
||||
const [clawdbotConfig, openclawConfig] = await Promise.all([
|
||||
readClawdbotConfig(),
|
||||
readOpenclawConfig(),
|
||||
])
|
||||
if (!clawdbotConfig && !openclawConfig) return { roots, labels }
|
||||
|
||||
if (clawdbotConfig) {
|
||||
addConfigRoots(clawdbotConfig, roots, labels)
|
||||
}
|
||||
|
||||
const listedAgents = config.agents?.list ?? []
|
||||
for (const entry of listedAgents) {
|
||||
const workspace = resolveUserPath(entry?.workspace ?? '')
|
||||
if (!workspace) continue
|
||||
const name = entry?.name?.trim() || entry?.id?.trim() || 'agent'
|
||||
pushRoot(roots, labels, join(workspace, 'skills'), `Agent: ${name}`)
|
||||
}
|
||||
|
||||
const agents = config.routing?.agents ?? {}
|
||||
for (const [agentId, entry] of Object.entries(agents)) {
|
||||
const workspace = resolveUserPath(entry?.workspace ?? '')
|
||||
if (!workspace) continue
|
||||
const name = entry?.name?.trim() || agentId
|
||||
pushRoot(roots, labels, join(workspace, 'skills'), `Agent: ${name}`)
|
||||
}
|
||||
|
||||
const extraDirs = config.skills?.load?.extraDirs ?? []
|
||||
for (const dir of extraDirs) {
|
||||
const resolved = resolveUserPath(String(dir))
|
||||
if (!resolved) continue
|
||||
const label = `Extra: ${basename(resolved) || resolved}`
|
||||
pushRoot(roots, labels, resolved, label)
|
||||
if (openclawConfig) {
|
||||
addConfigRoots(openclawConfig, roots, labels, 'OpenClaw')
|
||||
}
|
||||
|
||||
return { roots, labels }
|
||||
@@ -82,18 +65,31 @@ export async function resolveClawdbotSkillRoots(): Promise<ClawdbotSkillRoots> {
|
||||
|
||||
export async function resolveClawdbotDefaultWorkspace(): Promise<string | null> {
|
||||
const config = await readClawdbotConfig()
|
||||
if (!config) return null
|
||||
const openclawConfig = await readOpenclawConfig()
|
||||
if (!config && !openclawConfig) return null
|
||||
|
||||
const defaultsWorkspace = resolveUserPath(
|
||||
config.agents?.defaults?.workspace ?? config.agent?.workspace ?? '',
|
||||
config?.agents?.defaults?.workspace ?? config?.agent?.workspace ?? '',
|
||||
)
|
||||
if (defaultsWorkspace) return defaultsWorkspace
|
||||
|
||||
const listedAgents = config.agents?.list ?? []
|
||||
const listedAgents = config?.agents?.list ?? []
|
||||
const defaultAgent =
|
||||
listedAgents.find((entry) => entry.default) ?? listedAgents.find((entry) => entry.id === 'main')
|
||||
const listWorkspace = resolveUserPath(defaultAgent?.workspace ?? '')
|
||||
return listWorkspace || null
|
||||
if (listWorkspace) return listWorkspace
|
||||
|
||||
if (!openclawConfig) return null
|
||||
const openclawDefaults = resolveUserPath(
|
||||
openclawConfig.agents?.defaults?.workspace ?? openclawConfig.agent?.workspace ?? '',
|
||||
)
|
||||
if (openclawDefaults) return openclawDefaults
|
||||
const openclawAgents = openclawConfig.agents?.list ?? []
|
||||
const openclawDefaultAgent =
|
||||
openclawAgents.find((entry) => entry.default) ??
|
||||
openclawAgents.find((entry) => entry.id === 'main')
|
||||
const openclawWorkspace = resolveUserPath(openclawDefaultAgent?.workspace ?? '')
|
||||
return openclawWorkspace || null
|
||||
}
|
||||
|
||||
function resolveClawdbotStateDir() {
|
||||
@@ -108,6 +104,18 @@ function resolveClawdbotConfigPath() {
|
||||
return join(resolveClawdbotStateDir(), 'clawdbot.json')
|
||||
}
|
||||
|
||||
function resolveOpenclawStateDir() {
|
||||
const override = process.env.OPENCLAW_STATE_DIR?.trim()
|
||||
if (override) return resolveUserPath(override)
|
||||
return join(homedir(), '.openclaw')
|
||||
}
|
||||
|
||||
function resolveOpenclawConfigPath() {
|
||||
const override = process.env.OPENCLAW_CONFIG_PATH?.trim()
|
||||
if (override) return resolveUserPath(override)
|
||||
return join(resolveOpenclawStateDir(), 'openclaw.json')
|
||||
}
|
||||
|
||||
function resolveUserPath(input: string) {
|
||||
const trimmed = input.trim()
|
||||
if (!trimmed) return ''
|
||||
@@ -118,8 +126,16 @@ function resolveUserPath(input: string) {
|
||||
}
|
||||
|
||||
async function readClawdbotConfig(): Promise<ClawdbotConfig | null> {
|
||||
return readConfigFile(resolveClawdbotConfigPath())
|
||||
}
|
||||
|
||||
async function readOpenclawConfig(): Promise<ClawdbotConfig | null> {
|
||||
return readConfigFile(resolveOpenclawConfigPath())
|
||||
}
|
||||
|
||||
async function readConfigFile(path: string): Promise<ClawdbotConfig | null> {
|
||||
try {
|
||||
const raw = await readFile(resolveClawdbotConfigPath(), 'utf8')
|
||||
const raw = await readFile(path, 'utf8')
|
||||
const parsed = JSON5.parse(raw)
|
||||
if (!parsed || typeof parsed !== 'object') return null
|
||||
return parsed as ClawdbotConfig
|
||||
@@ -128,6 +144,46 @@ async function readClawdbotConfig(): Promise<ClawdbotConfig | null> {
|
||||
}
|
||||
}
|
||||
|
||||
function addConfigRoots(
|
||||
config: ClawdbotConfig,
|
||||
roots: string[],
|
||||
labels: Record<string, string>,
|
||||
labelPrefix?: string,
|
||||
) {
|
||||
const prefix = labelPrefix ? `${labelPrefix}: ` : ''
|
||||
|
||||
const mainWorkspace = resolveUserPath(
|
||||
config.agents?.defaults?.workspace ?? config.agent?.workspace ?? '',
|
||||
)
|
||||
if (mainWorkspace) {
|
||||
pushRoot(roots, labels, join(mainWorkspace, 'skills'), `${prefix}Agent: main`)
|
||||
}
|
||||
|
||||
const listedAgents = config.agents?.list ?? []
|
||||
for (const entry of listedAgents) {
|
||||
const workspace = resolveUserPath(entry?.workspace ?? '')
|
||||
if (!workspace) continue
|
||||
const name = entry?.name?.trim() || entry?.id?.trim() || 'agent'
|
||||
pushRoot(roots, labels, join(workspace, 'skills'), `${prefix}Agent: ${name}`)
|
||||
}
|
||||
|
||||
const agents = config.routing?.agents ?? {}
|
||||
for (const [agentId, entry] of Object.entries(agents)) {
|
||||
const workspace = resolveUserPath(entry?.workspace ?? '')
|
||||
if (!workspace) continue
|
||||
const name = entry?.name?.trim() || agentId
|
||||
pushRoot(roots, labels, join(workspace, 'skills'), `${prefix}Agent: ${name}`)
|
||||
}
|
||||
|
||||
const extraDirs = config.skills?.load?.extraDirs ?? []
|
||||
for (const dir of extraDirs) {
|
||||
const resolved = resolveUserPath(String(dir))
|
||||
if (!resolved) continue
|
||||
const label = `${prefix}Extra: ${basename(resolved) || resolved}`
|
||||
pushRoot(roots, labels, resolved, label)
|
||||
}
|
||||
}
|
||||
|
||||
function pushRoot(roots: string[], labels: Record<string, string>, root: string, label?: string) {
|
||||
const resolved = resolveUserPath(root)
|
||||
if (!resolved) return
|
||||
|
||||
@@ -47,7 +47,7 @@ export async function cmdLogin(
|
||||
) {
|
||||
if (!tokenFlag && !inputAllowed) fail('Token required (use --token or remove --no-input)')
|
||||
|
||||
const token = tokenFlag || (await promptHidden('ClawdHub token: '))
|
||||
const token = tokenFlag || (await promptHidden('OpenClaw token: '))
|
||||
if (!token) fail('Token required')
|
||||
|
||||
const registry = await getRegistry(opts, { cache: true })
|
||||
@@ -79,7 +79,7 @@ export async function cmdLogout(opts: GlobalOpts) {
|
||||
export async function cmdWhoami(opts: GlobalOpts) {
|
||||
const cfg = await readGlobalConfig()
|
||||
const token = cfg?.token
|
||||
if (!token) fail('Not logged in. Run: clawdhub login')
|
||||
if (!token) fail('Not logged in. Run: clawhub login')
|
||||
const registry = await getRegistry(opts, { cache: true })
|
||||
|
||||
const spinner = createSpinner('Checking token')
|
||||
|
||||
@@ -4,11 +4,11 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { GlobalOpts } from '../types'
|
||||
|
||||
vi.mock('../../config.js', () => ({
|
||||
readGlobalConfig: vi.fn(async () => ({ registry: 'https://clawdhub.com', token: 'tkn' })),
|
||||
readGlobalConfig: vi.fn(async () => ({ registry: 'https://clawhub.ai', token: 'tkn' })),
|
||||
}))
|
||||
|
||||
vi.mock('../registry.js', () => ({
|
||||
getRegistry: vi.fn(async () => 'https://clawdhub.com'),
|
||||
getRegistry: vi.fn(async () => 'https://clawhub.ai'),
|
||||
}))
|
||||
|
||||
const mockApiRequest = vi.fn()
|
||||
@@ -35,8 +35,8 @@ function makeOpts(): GlobalOpts {
|
||||
return {
|
||||
workdir: '/work',
|
||||
dir: '/work/skills',
|
||||
site: 'https://clawdhub.com',
|
||||
registry: 'https://clawdhub.com',
|
||||
site: 'https://clawhub.ai',
|
||||
registry: 'https://clawhub.ai',
|
||||
registrySource: 'default',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { createSpinner, fail, formatError, isInteractive, promptConfirm } from '
|
||||
async function requireToken() {
|
||||
const cfg = await readGlobalConfig()
|
||||
const token = cfg?.token
|
||||
if (!token) fail('Not logged in. Run: clawdhub login')
|
||||
if (!token) fail('Not logged in. Run: clawhub login')
|
||||
return token
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,10 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { GlobalOpts } from '../types'
|
||||
|
||||
vi.mock('../../config.js', () => ({
|
||||
readGlobalConfig: vi.fn(async () => ({ registry: 'https://clawdhub.com', token: 'tkn' })),
|
||||
readGlobalConfig: vi.fn(async () => ({ registry: 'https://clawhub.ai', token: 'tkn' })),
|
||||
}))
|
||||
|
||||
const mockGetRegistry = vi.fn(async (_opts: unknown, _params?: unknown) => 'https://clawdhub.com')
|
||||
const mockGetRegistry = vi.fn(async (_opts: unknown, _params?: unknown) => 'https://clawhub.ai')
|
||||
vi.mock('../registry.js', () => ({
|
||||
getRegistry: (opts: unknown, params?: unknown) => mockGetRegistry(opts, params),
|
||||
}))
|
||||
@@ -34,7 +34,7 @@ vi.mock('../ui.js', () => ({
|
||||
const { cmdPublish } = await import('./publish')
|
||||
|
||||
async function makeTmpWorkdir() {
|
||||
const root = await mkdtemp(join(tmpdir(), 'clawdhub-publish-'))
|
||||
const root = await mkdtemp(join(tmpdir(), 'clawhub-publish-'))
|
||||
return root
|
||||
}
|
||||
|
||||
@@ -42,8 +42,8 @@ function makeOpts(workdir: string): GlobalOpts {
|
||||
return {
|
||||
workdir,
|
||||
dir: join(workdir, 'skills'),
|
||||
site: 'https://clawdhub.com',
|
||||
registry: 'https://clawdhub.com',
|
||||
site: 'https://clawhub.ai',
|
||||
registry: 'https://clawhub.ai',
|
||||
registrySource: 'default',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export async function cmdPublish(
|
||||
|
||||
const cfg = await readGlobalConfig()
|
||||
const token = cfg?.token
|
||||
if (!token) fail('Not logged in. Run: clawdhub login')
|
||||
if (!token) fail('Not logged in. Run: clawhub login')
|
||||
const registry = await getRegistry(opts, { cache: true })
|
||||
|
||||
const slug = options.slug ?? sanitizeSlug(basename(folder))
|
||||
|
||||
@@ -1,25 +1,66 @@
|
||||
/* @vitest-environment node */
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ApiRoutes } from '../../schema/index.js'
|
||||
import type { GlobalOpts } from '../types'
|
||||
|
||||
const mockApiRequest = vi.fn()
|
||||
const mockDownloadZip = vi.fn()
|
||||
vi.mock('../../http.js', () => ({
|
||||
apiRequest: (...args: unknown[]) => mockApiRequest(...args),
|
||||
downloadZip: (...args: unknown[]) => mockDownloadZip(...args),
|
||||
}))
|
||||
|
||||
const mockGetRegistry = vi.fn(async () => 'https://clawdhub.com')
|
||||
const mockGetRegistry = vi.fn(async () => 'https://clawhub.ai')
|
||||
vi.mock('../registry.js', () => ({
|
||||
getRegistry: () => mockGetRegistry(),
|
||||
}))
|
||||
|
||||
const mockSpinner = { stop: vi.fn(), fail: vi.fn() }
|
||||
const mockSpinner = {
|
||||
stop: vi.fn(),
|
||||
fail: vi.fn(),
|
||||
start: vi.fn(),
|
||||
succeed: vi.fn(),
|
||||
isSpinning: false,
|
||||
text: '',
|
||||
}
|
||||
vi.mock('../ui.js', () => ({
|
||||
createSpinner: vi.fn(() => mockSpinner),
|
||||
fail: (message: string) => {
|
||||
throw new Error(message)
|
||||
},
|
||||
formatError: (error: unknown) => (error instanceof Error ? error.message : String(error)),
|
||||
isInteractive: () => false,
|
||||
promptConfirm: vi.fn(async () => false),
|
||||
}))
|
||||
|
||||
const { clampLimit, cmdExplore, formatExploreLine } = await import('./skills')
|
||||
vi.mock('../../skills.js', () => ({
|
||||
extractZipToDir: vi.fn(),
|
||||
hashSkillFiles: vi.fn(),
|
||||
listTextFiles: vi.fn(),
|
||||
readLockfile: vi.fn(),
|
||||
readSkillOrigin: vi.fn(),
|
||||
writeLockfile: vi.fn(),
|
||||
writeSkillOrigin: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('node:fs/promises', () => ({
|
||||
mkdir: vi.fn(),
|
||||
rm: vi.fn(),
|
||||
stat: vi.fn(),
|
||||
}))
|
||||
|
||||
const { clampLimit, cmdExplore, cmdUpdate, formatExploreLine } = await import('./skills')
|
||||
const {
|
||||
extractZipToDir,
|
||||
hashSkillFiles,
|
||||
listTextFiles,
|
||||
readLockfile,
|
||||
readSkillOrigin,
|
||||
writeLockfile,
|
||||
writeSkillOrigin,
|
||||
} = await import('../../skills.js')
|
||||
const { rm, stat } = await import('node:fs/promises')
|
||||
|
||||
const mockLog = vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
|
||||
@@ -27,8 +68,8 @@ function makeOpts(): GlobalOpts {
|
||||
return {
|
||||
workdir: '/work',
|
||||
dir: '/work/skills',
|
||||
site: 'https://clawdhub.com',
|
||||
registry: 'https://clawdhub.com',
|
||||
site: 'https://clawhub.ai',
|
||||
registry: 'https://clawhub.ai',
|
||||
registrySource: 'default',
|
||||
}
|
||||
}
|
||||
@@ -123,3 +164,28 @@ describe('cmdExplore', () => {
|
||||
expect(second.searchParams.get('sort')).toBe('trending')
|
||||
})
|
||||
})
|
||||
|
||||
describe('cmdUpdate', () => {
|
||||
it('uses path-based skill lookup when no local fingerprint is available', async () => {
|
||||
mockApiRequest.mockResolvedValue({ latestVersion: { version: '1.0.0' } })
|
||||
mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3]))
|
||||
vi.mocked(readLockfile).mockResolvedValue({
|
||||
version: 1,
|
||||
skills: { demo: { version: '0.1.0', installedAt: 123 } },
|
||||
})
|
||||
vi.mocked(writeLockfile).mockResolvedValue()
|
||||
vi.mocked(readSkillOrigin).mockResolvedValue(null)
|
||||
vi.mocked(writeSkillOrigin).mockResolvedValue()
|
||||
vi.mocked(extractZipToDir).mockResolvedValue()
|
||||
vi.mocked(listTextFiles).mockResolvedValue([])
|
||||
vi.mocked(hashSkillFiles).mockReturnValue({ fingerprint: 'hash', files: [] })
|
||||
vi.mocked(stat).mockRejectedValue(new Error('missing'))
|
||||
vi.mocked(rm).mockResolvedValue()
|
||||
|
||||
await cmdUpdate(makeOpts(), 'demo', {}, false)
|
||||
|
||||
const [, args] = mockApiRequest.mock.calls[0] ?? []
|
||||
expect(args?.path).toBe(`${ApiRoutes.skills}/${encodeURIComponent('demo')}`)
|
||||
expect(args?.url).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -153,7 +153,7 @@ export async function cmdUpdate(
|
||||
} else {
|
||||
const meta = await apiRequest(
|
||||
registry,
|
||||
{ method: 'GET', url: `${ApiRoutes.skills}/${encodeURIComponent(entry)}` },
|
||||
{ method: 'GET', path: `${ApiRoutes.skills}/${encodeURIComponent(entry)}` },
|
||||
ApiV1SkillResponseSchema,
|
||||
)
|
||||
resolveResult = { match: null, latestVersion: meta.latestVersion ?? null }
|
||||
|
||||
@@ -8,7 +8,7 @@ import { createSpinner, fail, formatError, isInteractive, promptConfirm } from '
|
||||
async function requireToken() {
|
||||
const cfg = await readGlobalConfig()
|
||||
const token = cfg?.token
|
||||
if (!token) fail('Not logged in. Run: clawdhub login')
|
||||
if (!token) fail('Not logged in. Run: clawhub login')
|
||||
return token
|
||||
}
|
||||
|
||||
|
||||
@@ -27,10 +27,10 @@ vi.mock('@clack/prompts', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('../../config.js', () => ({
|
||||
readGlobalConfig: vi.fn(async () => ({ registry: 'https://clawdhub.com', token: 'tkn' })),
|
||||
readGlobalConfig: vi.fn(async () => ({ registry: 'https://clawhub.ai', token: 'tkn' })),
|
||||
}))
|
||||
|
||||
const mockGetRegistry = vi.fn(async () => 'https://clawdhub.com')
|
||||
const mockGetRegistry = vi.fn(async () => 'https://clawhub.ai')
|
||||
vi.mock('../registry.js', () => ({
|
||||
getRegistry: () => mockGetRegistry(),
|
||||
}))
|
||||
@@ -89,8 +89,8 @@ function makeOpts(): GlobalOpts {
|
||||
return {
|
||||
workdir: '/work',
|
||||
dir: '/work/skills',
|
||||
site: 'https://clawdhub.com',
|
||||
registry: 'https://clawdhub.com',
|
||||
site: 'https://clawhub.ai',
|
||||
registry: 'https://clawhub.ai',
|
||||
registrySource: 'default',
|
||||
}
|
||||
}
|
||||
@@ -290,9 +290,9 @@ describe('cmdSync', () => {
|
||||
expect(update.changelog).toBe('')
|
||||
})
|
||||
|
||||
it('skips telemetry when CLAWDHUB_DISABLE_TELEMETRY is set', async () => {
|
||||
it('skips telemetry when CLAWHUB_DISABLE_TELEMETRY is set', async () => {
|
||||
interactive = false
|
||||
process.env.CLAWDHUB_DISABLE_TELEMETRY = '1'
|
||||
process.env.CLAWHUB_DISABLE_TELEMETRY = '1'
|
||||
mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => {
|
||||
if (args.path === '/api/v1/whoami') return { user: { handle: 'steipete' } }
|
||||
if (args.path.startsWith('/api/v1/resolve?')) {
|
||||
@@ -305,6 +305,6 @@ describe('cmdSync', () => {
|
||||
expect(
|
||||
mockApiRequest.mock.calls.some((call) => call[1]?.path === '/api/cli/telemetry/sync'),
|
||||
).toBe(false)
|
||||
delete process.env.CLAWDHUB_DISABLE_TELEMETRY
|
||||
delete process.env.CLAWHUB_DISABLE_TELEMETRY
|
||||
})
|
||||
})
|
||||
|
||||
@@ -30,11 +30,11 @@ import type { Candidate, LocalSkill, SyncOptions } from './syncTypes.js'
|
||||
|
||||
export async function cmdSync(opts: GlobalOpts, options: SyncOptions, inputAllowed: boolean) {
|
||||
const allowPrompt = isInteractive() && inputAllowed !== false
|
||||
intro('ClawdHub sync')
|
||||
intro('OpenClaw sync')
|
||||
|
||||
const cfg = await readGlobalConfig()
|
||||
const token = cfg?.token
|
||||
if (!token) fail('Not logged in. Run: clawdhub login')
|
||||
if (!token) fail('Not logged in. Run: clawhub login')
|
||||
|
||||
const registry = await getRegistryWithAuth(opts, token)
|
||||
const selectedRoots = buildScanRoots(opts, options.root)
|
||||
|
||||
@@ -58,7 +58,7 @@ export async function reportTelemetryIfEnabled(params: {
|
||||
}
|
||||
|
||||
function isTelemetryDisabled() {
|
||||
const raw = process.env.CLAWDHUB_DISABLE_TELEMETRY
|
||||
const raw = process.env.CLAWHUB_DISABLE_TELEMETRY ?? process.env.CLAWDHUB_DISABLE_TELEMETRY
|
||||
if (!raw) return false
|
||||
return ['1', 'true', 'yes', 'on'].includes(raw.trim().toLowerCase())
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { createSpinner, fail, formatError, isInteractive, promptConfirm } from '
|
||||
async function requireToken() {
|
||||
const cfg = await readGlobalConfig()
|
||||
const token = cfg?.token
|
||||
if (!token) fail('Not logged in. Run: clawdhub login')
|
||||
if (!token) fail('Not logged in. Run: clawhub login')
|
||||
return token
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ function makeOpts(overrides: Partial<GlobalOpts> = {}): GlobalOpts {
|
||||
return {
|
||||
workdir: '/work',
|
||||
dir: '/work/skills',
|
||||
site: 'https://clawdhub.com',
|
||||
site: 'https://clawhub.ai',
|
||||
registry: DEFAULT_REGISTRY,
|
||||
registrySource: 'default',
|
||||
...overrides,
|
||||
@@ -38,7 +38,7 @@ beforeEach(() => {
|
||||
describe('registry resolution', () => {
|
||||
it('prefers explicit registry over discovery/cache', async () => {
|
||||
readGlobalConfig.mockResolvedValue({ registry: 'https://auth.clawdhub.com' })
|
||||
discoverRegistryFromSite.mockResolvedValue({ apiBase: 'https://clawdhub.com' })
|
||||
discoverRegistryFromSite.mockResolvedValue({ apiBase: 'https://clawhub.ai' })
|
||||
|
||||
const registry = await resolveRegistry(
|
||||
makeOpts({ registry: 'https://custom.example', registrySource: 'cli' }),
|
||||
@@ -50,13 +50,13 @@ describe('registry resolution', () => {
|
||||
|
||||
it('ignores legacy registry and updates cache from discovery', async () => {
|
||||
readGlobalConfig.mockResolvedValue({ registry: 'https://auth.clawdhub.com', token: 'tkn' })
|
||||
discoverRegistryFromSite.mockResolvedValue({ apiBase: 'https://clawdhub.com' })
|
||||
discoverRegistryFromSite.mockResolvedValue({ apiBase: 'https://clawhub.ai' })
|
||||
|
||||
const registry = await getRegistry(makeOpts(), { cache: true })
|
||||
|
||||
expect(registry).toBe('https://clawdhub.com')
|
||||
expect(registry).toBe('https://clawhub.ai')
|
||||
expect(writeGlobalConfig).toHaveBeenCalledWith({
|
||||
registry: 'https://clawdhub.com',
|
||||
registry: 'https://clawhub.ai',
|
||||
token: 'tkn',
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,9 +2,9 @@ import { readGlobalConfig, writeGlobalConfig } from '../config.js'
|
||||
import { discoverRegistryFromSite } from '../discovery.js'
|
||||
import type { GlobalOpts } from './types.js'
|
||||
|
||||
export const DEFAULT_SITE = 'https://clawdhub.com'
|
||||
export const DEFAULT_REGISTRY = 'https://clawdhub.com'
|
||||
const LEGACY_REGISTRY_HOSTS = new Set(['auth.clawdhub.com'])
|
||||
export const DEFAULT_SITE = 'https://clawhub.ai'
|
||||
export const DEFAULT_REGISTRY = 'https://clawhub.ai'
|
||||
const LEGACY_REGISTRY_HOSTS = new Set(['auth.clawdhub.com', 'auth.clawhub.com', 'auth.clawhub.ai'])
|
||||
|
||||
export async function resolveRegistry(opts: GlobalOpts) {
|
||||
const explicit = opts.registrySource !== 'default' ? opts.registry.trim() : ''
|
||||
|
||||
@@ -7,7 +7,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import { findSkillFolders, getFallbackSkillRoots } from './scanSkills'
|
||||
|
||||
async function makeTmpDir() {
|
||||
return mkdtemp(join(tmpdir(), 'clawdhub-scan-'))
|
||||
return mkdtemp(join(tmpdir(), 'clawhub-scan-'))
|
||||
}
|
||||
|
||||
describe('scanSkills', () => {
|
||||
@@ -60,5 +60,7 @@ describe('scanSkills', () => {
|
||||
expect(roots.some((p) => p.endsWith('/clawdis/skills'))).toBe(true)
|
||||
expect(roots.some((p) => p.endsWith('/clawd/skills'))).toBe(true)
|
||||
expect(roots.some((p) => p.endsWith('/clawdbot/skills'))).toBe(true)
|
||||
expect(roots.some((p) => p.endsWith('/openclaw/skills'))).toBe(true)
|
||||
expect(roots.some((p) => p.endsWith('/moltbot/skills'))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -37,6 +37,10 @@ export function getFallbackSkillRoots(workdir: string) {
|
||||
resolve(workdir, '..', 'clawdis', 'Skills'),
|
||||
resolve(workdir, '..', 'clawdbot', 'skills'),
|
||||
resolve(workdir, '..', 'clawdbot', 'Skills'),
|
||||
resolve(workdir, '..', 'openclaw', 'skills'),
|
||||
resolve(workdir, '..', 'openclaw', 'Skills'),
|
||||
resolve(workdir, '..', 'moltbot', 'skills'),
|
||||
resolve(workdir, '..', 'moltbot', 'Skills'),
|
||||
|
||||
// legacy locations
|
||||
resolve(home, 'clawd', 'skills'),
|
||||
@@ -54,11 +58,25 @@ export function getFallbackSkillRoots(workdir: string) {
|
||||
resolve(home, '.clawdis', 'skills'),
|
||||
resolve(home, '.clawdis', 'Skills'),
|
||||
|
||||
resolve(home, 'openclaw', 'skills'),
|
||||
resolve(home, 'openclaw', 'Skills'),
|
||||
resolve(home, '.openclaw', 'skills'),
|
||||
resolve(home, '.openclaw', 'Skills'),
|
||||
|
||||
resolve(home, 'moltbot', 'skills'),
|
||||
resolve(home, 'moltbot', 'Skills'),
|
||||
resolve(home, '.moltbot', 'skills'),
|
||||
resolve(home, '.moltbot', 'Skills'),
|
||||
|
||||
// macOS App Support legacy
|
||||
resolve(home, 'Library', 'Application Support', 'clawdbot', 'skills'),
|
||||
resolve(home, 'Library', 'Application Support', 'clawdbot', 'Skills'),
|
||||
resolve(home, 'Library', 'Application Support', 'clawdis', 'skills'),
|
||||
resolve(home, 'Library', 'Application Support', 'clawdis', 'Skills'),
|
||||
resolve(home, 'Library', 'Application Support', 'openclaw', 'skills'),
|
||||
resolve(home, 'Library', 'Application Support', 'openclaw', 'Skills'),
|
||||
resolve(home, 'Library', 'Application Support', 'moltbot', 'skills'),
|
||||
resolve(home, 'Library', 'Application Support', 'moltbot', 'Skills'),
|
||||
]
|
||||
return Array.from(new Set(roots))
|
||||
}
|
||||
|
||||
@@ -1,22 +1,44 @@
|
||||
import { existsSync } from 'node:fs'
|
||||
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
||||
import { homedir } from 'node:os'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { type GlobalConfig, GlobalConfigSchema, parseArk } from './schema/index.js'
|
||||
|
||||
export function getGlobalConfigPath() {
|
||||
const override = process.env.CLAWDHUB_CONFIG_PATH?.trim()
|
||||
const override =
|
||||
process.env.CLAWHUB_CONFIG_PATH?.trim() ?? process.env.CLAWDHUB_CONFIG_PATH?.trim()
|
||||
if (override) return resolve(override)
|
||||
const home = homedir()
|
||||
if (process.platform === 'darwin') {
|
||||
return join(home, 'Library', 'Application Support', 'clawdhub', 'config.json')
|
||||
const clawhubPath = join(home, 'Library', 'Application Support', 'clawhub', 'config.json')
|
||||
const clawdhubPath = join(home, 'Library', 'Application Support', 'clawdhub', 'config.json')
|
||||
if (existsSync(clawhubPath)) return clawhubPath
|
||||
if (existsSync(clawdhubPath)) return clawdhubPath
|
||||
return clawhubPath
|
||||
}
|
||||
const xdg = process.env.XDG_CONFIG_HOME
|
||||
if (xdg) return join(xdg, 'clawdhub', 'config.json')
|
||||
if (xdg) {
|
||||
const clawhubPath = join(xdg, 'clawhub', 'config.json')
|
||||
const clawdhubPath = join(xdg, 'clawdhub', 'config.json')
|
||||
if (existsSync(clawhubPath)) return clawhubPath
|
||||
if (existsSync(clawdhubPath)) return clawdhubPath
|
||||
return clawhubPath
|
||||
}
|
||||
if (process.platform === 'win32') {
|
||||
const appData = process.env.APPDATA
|
||||
if (appData) return join(appData, 'clawdhub', 'config.json')
|
||||
if (appData) {
|
||||
const clawhubPath = join(appData, 'clawhub', 'config.json')
|
||||
const clawdhubPath = join(appData, 'clawdhub', 'config.json')
|
||||
if (existsSync(clawhubPath)) return clawhubPath
|
||||
if (existsSync(clawdhubPath)) return clawdhubPath
|
||||
return clawhubPath
|
||||
}
|
||||
}
|
||||
return join(home, '.config', 'clawdhub', 'config.json')
|
||||
const clawhubPath = join(home, '.config', 'clawhub', 'config.json')
|
||||
const clawdhubPath = join(home, '.config', 'clawdhub', 'config.json')
|
||||
if (existsSync(clawhubPath)) return clawhubPath
|
||||
if (existsSync(clawdhubPath)) return clawdhubPath
|
||||
return clawhubPath
|
||||
}
|
||||
|
||||
export async function readGlobalConfig(): Promise<GlobalConfig | null> {
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import { parseArk, WellKnownConfigSchema } from './schema/index.js'
|
||||
|
||||
export async function discoverRegistryFromSite(siteUrl: string) {
|
||||
const url = new URL('/.well-known/clawdhub.json', siteUrl)
|
||||
const response = await fetch(url.toString(), {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' },
|
||||
})
|
||||
if (!response.ok) return null
|
||||
const raw = (await response.json()) as unknown
|
||||
const parsed = parseArk(WellKnownConfigSchema, raw, 'WellKnown config')
|
||||
const apiBase = 'apiBase' in parsed ? parsed.apiBase : parsed.registry
|
||||
if (!apiBase) return null
|
||||
return {
|
||||
apiBase,
|
||||
authBase: parsed.authBase,
|
||||
minCliVersion: parsed.minCliVersion,
|
||||
const paths = ['/.well-known/clawhub.json', '/.well-known/clawdhub.json']
|
||||
for (const path of paths) {
|
||||
const url = new URL(path, siteUrl)
|
||||
const response = await fetch(url.toString(), {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' },
|
||||
})
|
||||
if (!response.ok) continue
|
||||
const raw = (await response.json()) as unknown
|
||||
const parsed = parseArk(WellKnownConfigSchema, raw, 'WellKnown config')
|
||||
const apiBase = 'apiBase' in parsed ? parsed.apiBase : parsed.registry
|
||||
if (!apiBase) return null
|
||||
return {
|
||||
apiBase,
|
||||
authBase: parsed.authBase,
|
||||
minCliVersion: parsed.minCliVersion,
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -202,7 +202,7 @@ async function fetchJsonFormViaCurl(url: string, args: FormRequestArgs) {
|
||||
headers.push('-H', `Authorization: Bearer ${args.token}`)
|
||||
}
|
||||
|
||||
const tempDir = await mkdtemp(join(tmpdir(), 'clawdhub-upload-'))
|
||||
const tempDir = await mkdtemp(join(tmpdir(), 'clawhub-upload-'))
|
||||
try {
|
||||
const formArgs: string[] = []
|
||||
for (const [key, value] of args.form.entries()) {
|
||||
@@ -255,7 +255,7 @@ async function fetchJsonFormViaCurl(url: string, args: FormRequestArgs) {
|
||||
}
|
||||
|
||||
async function fetchBinaryViaCurl(url: string) {
|
||||
const tempDir = await mkdtemp(join(tmpdir(), 'clawdhub-download-'))
|
||||
const tempDir = await mkdtemp(join(tmpdir(), 'clawhub-download-'))
|
||||
const filePath = join(tempDir, 'payload.bin')
|
||||
try {
|
||||
const curlArgs = [
|
||||
|
||||
@@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import * as schema from '.'
|
||||
import { isTextContentType, TEXT_FILE_EXTENSION_SET } from './textFiles'
|
||||
|
||||
describe('packages/clawdhub schema textFiles', () => {
|
||||
describe('packages/clawhub 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)
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
|
||||
describe('skills', () => {
|
||||
it('extracts zip into directory and skips traversal', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'clawdhub-'))
|
||||
const dir = await mkdtemp(join(tmpdir(), 'clawhub-'))
|
||||
const zip = zipSync({
|
||||
'SKILL.md': strToU8('hello'),
|
||||
'../evil.txt': strToU8('nope'),
|
||||
@@ -32,7 +32,7 @@ describe('skills', () => {
|
||||
})
|
||||
|
||||
it('writes and reads lockfile', async () => {
|
||||
const workdir = await mkdtemp(join(tmpdir(), 'clawdhub-work-'))
|
||||
const workdir = await mkdtemp(join(tmpdir(), 'clawhub-work-'))
|
||||
await writeLockfile(workdir, {
|
||||
version: 1,
|
||||
skills: { demo: { version: '1.0.0', installedAt: 1 } },
|
||||
@@ -42,18 +42,18 @@ describe('skills', () => {
|
||||
})
|
||||
|
||||
it('returns empty lockfile on invalid json', async () => {
|
||||
const workdir = await mkdtemp(join(tmpdir(), 'clawdhub-work-bad-'))
|
||||
await mkdir(join(workdir, '.clawdhub'), { recursive: true })
|
||||
await writeFile(join(workdir, '.clawdhub', 'lock.json'), '{', 'utf8')
|
||||
const workdir = await mkdtemp(join(tmpdir(), 'clawhub-work-bad-'))
|
||||
await mkdir(join(workdir, '.clawhub'), { recursive: true })
|
||||
await writeFile(join(workdir, '.clawhub', 'lock.json'), '{', 'utf8')
|
||||
const read = await readLockfile(workdir)
|
||||
expect(read).toEqual({ version: 1, skills: {} })
|
||||
})
|
||||
|
||||
it('returns empty lockfile on schema mismatch', async () => {
|
||||
const workdir = await mkdtemp(join(tmpdir(), 'clawdhub-work-schema-'))
|
||||
await mkdir(join(workdir, '.clawdhub'), { recursive: true })
|
||||
const workdir = await mkdtemp(join(tmpdir(), 'clawhub-work-schema-'))
|
||||
await mkdir(join(workdir, '.clawhub'), { recursive: true })
|
||||
await writeFile(
|
||||
join(workdir, '.clawdhub', 'lock.json'),
|
||||
join(workdir, '.clawhub', 'lock.json'),
|
||||
JSON.stringify({ version: 1, skills: 'nope' }),
|
||||
'utf8',
|
||||
)
|
||||
@@ -62,7 +62,7 @@ describe('skills', () => {
|
||||
})
|
||||
|
||||
it('skips dotfiles and node_modules when listing text files', async () => {
|
||||
const workdir = await mkdtemp(join(tmpdir(), 'clawdhub-files-'))
|
||||
const workdir = await mkdtemp(join(tmpdir(), 'clawhub-files-'))
|
||||
await writeFile(join(workdir, 'SKILL.md'), 'hi', 'utf8')
|
||||
await writeFile(join(workdir, '.secret.txt'), 'no', 'utf8')
|
||||
await mkdir(join(workdir, 'node_modules'), { recursive: true })
|
||||
@@ -71,10 +71,10 @@ describe('skills', () => {
|
||||
expect(files.map((file) => file.relPath)).toEqual(['SKILL.md'])
|
||||
})
|
||||
|
||||
it('respects .gitignore and .clawdhubignore', async () => {
|
||||
const workdir = await mkdtemp(join(tmpdir(), 'clawdhub-ignore-'))
|
||||
it('respects .gitignore and .clawhubignore', async () => {
|
||||
const workdir = await mkdtemp(join(tmpdir(), 'clawhub-ignore-'))
|
||||
await writeFile(join(workdir, '.gitignore'), 'ignored.md\n', 'utf8')
|
||||
await writeFile(join(workdir, '.clawdhubignore'), 'private.md\n', 'utf8')
|
||||
await writeFile(join(workdir, '.clawhubignore'), 'private.md\n', 'utf8')
|
||||
await writeFile(join(workdir, 'SKILL.md'), 'hi', 'utf8')
|
||||
await writeFile(join(workdir, 'ignored.md'), 'no', 'utf8')
|
||||
await writeFile(join(workdir, 'private.md'), 'no', 'utf8')
|
||||
@@ -90,7 +90,7 @@ describe('skills', () => {
|
||||
})
|
||||
|
||||
it('falls back to text/plain for unknown text extensions', async () => {
|
||||
const workdir = await mkdtemp(join(tmpdir(), 'clawdhub-env-'))
|
||||
const workdir = await mkdtemp(join(tmpdir(), 'clawhub-env-'))
|
||||
await writeFile(join(workdir, 'SKILL.md'), 'hi', 'utf8')
|
||||
await writeFile(join(workdir, 'config.env'), 'TOKEN=demo', 'utf8')
|
||||
const files = await listTextFiles(workdir)
|
||||
@@ -147,26 +147,26 @@ describe('skills', () => {
|
||||
})
|
||||
|
||||
it('returns null for invalid skill origin metadata', async () => {
|
||||
const workdir = await mkdtemp(join(tmpdir(), 'clawdhub-origin-'))
|
||||
const workdir = await mkdtemp(join(tmpdir(), 'clawhub-origin-'))
|
||||
expect(await readSkillOrigin(workdir)).toBeNull()
|
||||
|
||||
await mkdir(join(workdir, '.clawdhub'), { recursive: true })
|
||||
await mkdir(join(workdir, '.clawhub'), { recursive: true })
|
||||
await writeFile(
|
||||
join(workdir, '.clawdhub', 'origin.json'),
|
||||
join(workdir, '.clawhub', 'origin.json'),
|
||||
JSON.stringify({ version: 2 }),
|
||||
'utf8',
|
||||
)
|
||||
expect(await readSkillOrigin(workdir)).toBeNull()
|
||||
|
||||
await writeFile(
|
||||
join(workdir, '.clawdhub', 'origin.json'),
|
||||
join(workdir, '.clawhub', 'origin.json'),
|
||||
JSON.stringify({ version: 1, registry: 'demo', slug: 'x', installedAt: 1 }),
|
||||
'utf8',
|
||||
)
|
||||
expect(await readSkillOrigin(workdir)).toBeNull()
|
||||
|
||||
await writeFile(
|
||||
join(workdir, '.clawdhub', 'origin.json'),
|
||||
join(workdir, '.clawhub', 'origin.json'),
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
registry: 'demo',
|
||||
|
||||
@@ -6,6 +6,11 @@ import ignore from 'ignore'
|
||||
import mime from 'mime'
|
||||
import { type Lockfile, LockfileSchema, parseArk, TEXT_FILE_EXTENSION_SET } from './schema/index.js'
|
||||
|
||||
const DOT_DIR = '.clawhub'
|
||||
const LEGACY_DOT_DIR = '.clawdhub'
|
||||
const DOT_IGNORE = '.clawhubignore'
|
||||
const LEGACY_DOT_IGNORE = '.clawdhubignore'
|
||||
|
||||
export type SkillOrigin = {
|
||||
version: 1
|
||||
registry: string
|
||||
@@ -30,9 +35,10 @@ export async function listTextFiles(root: string) {
|
||||
const files: Array<{ relPath: string; bytes: Uint8Array; contentType?: string }> = []
|
||||
const absRoot = resolve(root)
|
||||
const ig = ignore()
|
||||
ig.add(['.git/', 'node_modules/', '.clawdhub/'])
|
||||
ig.add(['.git/', 'node_modules/', `${DOT_DIR}/`, `${LEGACY_DOT_DIR}/`])
|
||||
await addIgnoreFile(ig, join(absRoot, '.gitignore'))
|
||||
await addIgnoreFile(ig, join(absRoot, '.clawdhubignore'))
|
||||
await addIgnoreFile(ig, join(absRoot, DOT_IGNORE))
|
||||
await addIgnoreFile(ig, join(absRoot, LEGACY_DOT_IGNORE))
|
||||
|
||||
await walk(absRoot, async (absPath) => {
|
||||
const relPath = normalizePath(relative(absRoot, absPath))
|
||||
@@ -87,44 +93,55 @@ export function hashSkillZip(zipBytes: Uint8Array) {
|
||||
}
|
||||
|
||||
export async function readLockfile(workdir: string): Promise<Lockfile> {
|
||||
const path = join(workdir, '.clawdhub', 'lock.json')
|
||||
try {
|
||||
const raw = await readFile(path, 'utf8')
|
||||
const parsed = JSON.parse(raw) as unknown
|
||||
return parseArk(LockfileSchema, parsed, 'Lockfile')
|
||||
} catch {
|
||||
return { version: 1, skills: {} }
|
||||
const paths = [join(workdir, DOT_DIR, 'lock.json'), join(workdir, LEGACY_DOT_DIR, 'lock.json')]
|
||||
for (const path of paths) {
|
||||
try {
|
||||
const raw = await readFile(path, 'utf8')
|
||||
const parsed = JSON.parse(raw) as unknown
|
||||
return parseArk(LockfileSchema, parsed, 'Lockfile')
|
||||
} catch {
|
||||
// try next
|
||||
}
|
||||
}
|
||||
return { version: 1, skills: {} }
|
||||
}
|
||||
|
||||
export async function writeLockfile(workdir: string, lock: Lockfile) {
|
||||
const path = join(workdir, '.clawdhub', 'lock.json')
|
||||
const path = join(workdir, DOT_DIR, 'lock.json')
|
||||
await mkdir(dirname(path), { recursive: true })
|
||||
await writeFile(path, `${JSON.stringify(lock, null, 2)}\n`, 'utf8')
|
||||
}
|
||||
|
||||
export async function readSkillOrigin(skillFolder: string): Promise<SkillOrigin | null> {
|
||||
const path = join(skillFolder, '.clawdhub', 'origin.json')
|
||||
try {
|
||||
const raw = await readFile(path, 'utf8')
|
||||
const parsed = JSON.parse(raw) as Partial<SkillOrigin>
|
||||
if (parsed.version !== 1) return null
|
||||
if (!parsed.registry || !parsed.slug || !parsed.installedVersion) return null
|
||||
if (typeof parsed.installedAt !== 'number' || !Number.isFinite(parsed.installedAt)) return null
|
||||
return {
|
||||
version: 1,
|
||||
registry: String(parsed.registry),
|
||||
slug: String(parsed.slug),
|
||||
installedVersion: String(parsed.installedVersion),
|
||||
installedAt: parsed.installedAt,
|
||||
const paths = [
|
||||
join(skillFolder, DOT_DIR, 'origin.json'),
|
||||
join(skillFolder, LEGACY_DOT_DIR, 'origin.json'),
|
||||
]
|
||||
for (const path of paths) {
|
||||
try {
|
||||
const raw = await readFile(path, 'utf8')
|
||||
const parsed = JSON.parse(raw) as Partial<SkillOrigin>
|
||||
if (parsed.version !== 1) return null
|
||||
if (!parsed.registry || !parsed.slug || !parsed.installedVersion) return null
|
||||
if (typeof parsed.installedAt !== 'number' || !Number.isFinite(parsed.installedAt)) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
version: 1,
|
||||
registry: String(parsed.registry),
|
||||
slug: String(parsed.slug),
|
||||
installedVersion: String(parsed.installedVersion),
|
||||
installedAt: parsed.installedAt,
|
||||
}
|
||||
} catch {
|
||||
// try next
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export async function writeSkillOrigin(skillFolder: string, origin: SkillOrigin) {
|
||||
const path = join(skillFolder, '.clawdhub', 'origin.json')
|
||||
const path = join(skillFolder, DOT_DIR, 'origin.json')
|
||||
await mkdir(dirname(path), { recursive: true })
|
||||
await writeFile(path, `${JSON.stringify(origin, null, 2)}\n`, 'utf8')
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# clawdhub-schema
|
||||
# clawhub-schema
|
||||
|
||||
Shared runtime schemas (ArkType) for ClawdHub.
|
||||
Shared runtime schemas (ArkType) for OpenClaw.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "clawdhub-schema",
|
||||
"name": "clawhub-schema",
|
||||
"version": "0.0.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
WellKnownConfigSchema,
|
||||
} from './schemas'
|
||||
|
||||
describe('clawdhub-schema', () => {
|
||||
describe('clawhub-schema', () => {
|
||||
it('parses lockfile records', () => {
|
||||
const lock = parseArk(
|
||||
LockfileSchema,
|
||||
@@ -68,10 +68,10 @@ describe('clawdhub-schema', () => {
|
||||
expect(
|
||||
parseArk(
|
||||
WellKnownConfigSchema,
|
||||
{ registry: 'https://example.convex.site', authBase: 'https://clawdhub.com' },
|
||||
{ registry: 'https://example.convex.site', authBase: 'https://clawhub.ai' },
|
||||
'WellKnown',
|
||||
),
|
||||
).toEqual({ registry: 'https://example.convex.site', authBase: 'https://clawdhub.com' })
|
||||
).toEqual({ registry: 'https://example.convex.site', authBase: 'https://clawhub.ai' })
|
||||
|
||||
expect(
|
||||
parseArk(
|
||||
@@ -84,14 +84,14 @@ describe('clawdhub-schema', () => {
|
||||
const combined = parseArk(
|
||||
WellKnownConfigSchema,
|
||||
{
|
||||
apiBase: 'https://clawdhub.com',
|
||||
registry: 'https://clawdhub.com',
|
||||
authBase: 'https://clawdhub.com',
|
||||
apiBase: 'https://clawhub.ai',
|
||||
registry: 'https://clawhub.ai',
|
||||
authBase: 'https://clawhub.ai',
|
||||
},
|
||||
'WellKnown',
|
||||
) as unknown as Record<string, unknown>
|
||||
expect(combined.apiBase).toBe('https://clawdhub.com')
|
||||
expect(combined.registry).toBe('https://clawdhub.com')
|
||||
expect(combined.apiBase).toBe('https://clawhub.ai')
|
||||
expect(combined.registry).toBe('https://clawhub.ai')
|
||||
})
|
||||
|
||||
it('throws labeled errors', () => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import * as schema from '.'
|
||||
import { isTextContentType, TEXT_FILE_EXTENSION_SET } from './textFiles'
|
||||
|
||||
describe('clawdhub-schema textFiles', () => {
|
||||
describe('clawhub-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)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"apiBase": "https://clawdhub.com",
|
||||
"authBase": "https://clawdhub.com",
|
||||
"apiBase": "https://clawhub.ai",
|
||||
"authBase": "https://clawhub.ai",
|
||||
"minCliVersion": "0.1.0",
|
||||
"registry": "https://clawdhub.com"
|
||||
"registry": "https://clawhub.ai"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"apiBase": "https://clawhub.ai",
|
||||
"authBase": "https://clawhub.ai",
|
||||
"minCliVersion": "0.1.0",
|
||||
"registry": "https://clawhub.ai"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user