Skill: vinlow/web2labs-studio (version 1.0.1)

--- name: web2labs-studio description: Edit my recording, turn a long video into shorts, generate captions and thumbnails, estimate cost before processing. Upload local files or YouTube/Twitch URLs and get back edited jump-cut videos, vertical shorts, subtitles, and thumbnail variants. metadata: {"openclaw":{"emoji":"video","homepage":"https://web2labs.com/openclaw","primaryEnv":"WEB2LABS_API_KEY","skillKey":"@web2labs/studio","requires":{"anyBins":["node"]}}} --- # Web2Labs Studio AI-powered video editor for creators. Process recordings into jump-cut videos, automatic subtitles, and shorts. ## Available Tools - `studio_setup`: Send magic link, complete setup, or save an existing API key. - `studio_upload`: Upload local file or supported URL for processing. - supports optional `webhook_url` + `webhook_secret` for `project.completed` callbacks. - `studio_status`: Check current project status. - `studio_poll`: Wait for completion with real-time WebSocket progress (falls back to HTTP polling). - `studio_results`: Get output URLs and metadata. - `studio_download`: Download outputs to local filesystem. - `studio_credits`: Check API/subscription balances. - `studio_pricing`: Get current pricing metadata for API and Creator Credit features. - `studio_estimate`: Estimate API and Creator Credit cost before upload. - `studio_thumbnails`: Generate A/B/C thumbnail variants for a completed project. - `studio_rerender`: Re-render a completed project with configuration overrides. First re-render per project is free; subsequent re-renders cost 15 Creator Credits. - `studio_analytics`: Get usage and value analytics. - `studio_brand`: Get or update brand kit settings (colors, identity, fonts, defaults). - `studio_brand_import`: Import brand colors and identity from a YouTube/Twitch profile URL. - `studio_assets`: Upload/list/delete reusable intro/outro/watermark assets. - `studio_projects`: List recent projects. - `studio_delete`: Delete a project. - `studio_feedback`: Report bugs/suggestions/questions. - `studio_referral`: Get or apply referral codes for bonus credits. - `studio_watch`: Watch a YouTube or Twitch channel for new videos and auto-process them. ## Presets - `quick`: Fast cleanup, no extras. - `youtube`: Subtitles + shorts + music. - `shorts-only`: Generate only vertical shorts. - `podcast`: Soft cuts, subtitles, no zoom. - `gaming`: Dynamic zoom and gaming-style pacing. - `tutorial`: Gentle edits for educational content. - `vlog`: Balanced vlog workflow. - `cinematic`: High-production settings. ## Standard Workflow 1. If auth is missing, run `studio_setup`. 2. Run `studio_credits` first. 3. If the workflow may use premium features, run `studio_estimate`. 4. Run `studio_upload` with a preset. 5. If no webhook is configured, run `studio_poll` for progress until completion. 6. Run `studio_results` and optionally `studio_thumbnails`. 7. Use `studio_rerender` if the user wants output changes without re-uploading. 8. Run `studio_download` to save outputs. 9. Use `studio_brand` when the user asks for brand color/font consistency across future outputs. 10. Use `studio_brand_import` when the user provides a YouTube/Twitch profile URL for one-click brand setup. 11. Use `studio_assets` when the user wants reusable intro/outro/watermark media. ## Cost-Aware Workflow - Use `studio_pricing` when the user asks "how much will this cost?" or wants bundle guidance. - Use `studio_estimate` before upload when configuration enables thumbnails/B-roll/audio polish. - Use `studio_rerender` for post-processing adjustments when analysis is already complete. - If estimate is non-trivial, confirm expected cost with the user before upload. - If the user asks for urgent/rush processing, set `priority: "rush"` and confirm the 2x API-credit cost before upload. - Paid tools support `confirm_spend: true` for explicit approval when required by spend policy. ## Spend Policy Spend policy is controlled by env var `WEB2LABS_SPEND_POLICY`: - `auto` (default): proceed without prompt unless auto-spend caps are exceeded. Best for most users who want a frictionless workflow. - `smart`: confirm higher-risk or higher-cost spends (rush uploads, low balance, large creator credit spend). - `explicit`: confirm every credit-spending action. Use for strict budget control. Auto mode caps (all tunable via env vars): - [REDACTED_SECRET] (default: 2) - [REDACTED_SECRET] (default: 40) - [REDACTED_SECRET] (default: 80) - [REDACTED_SECRET] (default: 400) When a tool returns `spend_confirmation_required`, ask the user and re-run with `confirm_spend: true`. ## URL Input If `file_path` is a URL, use local yt-dlp download first. If yt-dlp is missing, provide install help: - macOS: `brew install yt-dlp` - Linux: `pip install yt-dlp` - Windows: `winget install yt-dlp` Only process content the user has rights to edit. ## Webhooks - `studio_upload` accepts `webhook_url` and optional `webhook_secret`. - If provided, Studio sends `project.completed` to the callback URL when rendering finishes. - Signed callbacks include: - `X-Web2Labs-Webhook-Timestamp` - `X-Web2Labs-Webhook-Signature` (`sha256=<hmac(timestamp + "." + payload)>`) - With webhook enabled, polling is optional and can be skipped for fire-and-forget automation. ## Batch Workflow Before batch processing, confirm total credit cost: "This will process N videos and consume N credits. Proceed?" ## Guardrails 1. Always call `studio_credits` before upload. 2. Never expose API keys in logs or messages. 3. Respect spend policy and confirmation requirements before spending credits. 4. Remind users about retention after download. 5. Suggest presets when users do not provide settings. 6. For URLs, verify yt-dlp availability before proceeding. 7. Respect user rights and content ownership. 8. For `priority: "rush"`, explicitly confirm "2 API credits instead of 1" before proceeding. 9. For watch mode, only monitor channels the user owns or has permission to process. ## Upsell Moments - After successful processing: suggest thumbnails if missing. - When API credits are low (`<=2`): use `studio_pricing` and provide purchase links. - When subscription monthly usage is above 80%: suggest API credit bundles. - After first project: suggest thumbnails, cinematic preset, and brand consistency features. - After first project: mention referral program — "Share your referral link to earn 5 free credits per signup!" ## Brand Kit Workflow - If user asks to set brand colors, fonts, or identity, call `studio_brand` with `action: "update"`. - Use snake_case fields for convenience (`primary_color`, `secondary_color`, `channel_name`) or `updates` object. - Confirm that changes apply to future thumbnail/subtitle outputs. - If user shares a YouTube/Twitch profile URL, offer `studio_brand_import` first, then review/apply suggestions. ## Assets Workflow - Use `studio_assets` with: - `action: "list"` to inspect available intro/outro/watermark assets. - `action: "upload"` + `asset_type` + `file_path` to add reusable media. - `action: "delete"` + `asset_type` to remove old assets. - If user says "use this intro/outro on future videos", upload via `studio_assets` first, then ensure brand defaults are configured with `studio_brand`. ## Watch Mode Watch mode monitors YouTube or Twitch channels for new videos and auto-processes them through Studio. ### Setting up a watcher 1. Run `studio_watch` with `action: "add"` and the channel `url` (e.g. `https://youtube.com/@username`). 2. Optionally set `preset`, `max_duration_minutes`, `max_daily_uploads`, and `poll_interval_minutes`. 3. The watcher is saved and ready. Run `studio_watch` with `action: "check"` to poll for new videos. Only channel/user URLs are accepted, not individual video URLs. ### How check works `action: "check"` does a single poll cycle: - Lists recent videos from each enabled watcher's channel via yt-dlp. - Filters out already-processed videos (tracked by video ID). - Filters out videos exceeding `max_duration_minutes`. - Respects the `max_daily_uploads` cap per watcher. - Downloads and uploads each new video to Studio with the watcher's preset. - Returns a summary of what was processed. Pass `id` to check a specific watcher, or omit to check all enabled watchers. ### Managing watchers - `action: "list"` — show all watchers and their status. - `action: "status"` with `id` — detailed status for one watcher. - `action: "pause"` / `action: "resume"` with `id` — disable/enable a watcher. - `action: "remove"` with `id` — delete a watcher. ### Automation Run `studio_watch` with `action: "check"` on a schedule. Examples: - Have your AI agent call it periodically. - Use a system cron job: `*/30 * * * * node /path/to/check-watchers.mjs` - Use a simple loop script with a sleep interval. ### Content rights Only watch channels you own or have explicit permission to process. This aligns with the existing guardrail about respecting user rights and content ownership. ## Environment Variables - `WEB2LABS_API_KEY`: API key for authentication. - `WEB2LABS_BEARER_TOKEN`: Bearer token for authentication (alternative to API key). - `WEB2LABS_API_ENDPOINT`: API endpoint URL (default: `https://web2labs.com`). - `WEB2LABS_SOCKET_URL`: WebSocket server URL for real-time progress (default: same as API endpoint). Override for local dev when the socket server runs on a different port. - `WEB2LABS_SPEND_POLICY`: Spend confirmation policy (`smart`, `explicit`, `auto`). - `WEB2LABS_TEST_MODE`: Set to `true` to target the test instance (`https://test.web2labs.com`). Changes the default API endpoint and enables test-mode behavior. - `WEB2LABS_BASIC_AUTH`: HTTP Basic Auth credentials in `user:password` format. Required when the target instance is behind HTTP Basic Auth (e.g. the test instance). ### Sandbox mode When OpenClaw runs in sandbox mode, tool processes execute inside Docker and do **not** inherit host `process.env`. Environment variables set via `skills.entries.@web2labs/studio.env` in `~/.openclaw/openclaw.json` are only injected in host mode. For sandbox sessions, configure environment variables through the sandbox environment configuration or a custom Docker image. Alternatively, use `studio_setup` with `action: "save_api_key"` after starting the session — the key is written to a config file inside the container. ## Test Mode Test mode targets the isolated test instance at `https://test.web2labs.com`. The test instance has its own database, storage, and configuration — nothing affects production. ### Enabling test mode Set these environment variables: ``` WEB2LABS_TEST_MODE=true WEB2LABS_BASIC_AUTH=web2labs:<password> WEB2LABS_API_KEY=<test-instance-api-key> ``` `WEB2LABS_TEST_MODE=true` changes the default API endpoint to `https://test.web2labs.com`. You can also set `WEB2LABS_API_ENDPOINT` explicitly to override the URL. `WEB2LABS_BASIC_AUTH` provides the HTTP Basic Auth credentials that the test instance's nginx reverse proxy requires on all requests (format: `user:password`). ### Getting a test API key The test instance is password-protected at the nginx level, so the magic-link setup flow (`send_magic_link` / `complete_setup`) may not complete fully. Instead: 1. Open `https://test.web2labs.com` in a browser and enter the HTTP Basic Auth credentials when prompted. 2. Create an account or log in. 3. Navigate to `/user/api` and generate an API key. 4. Save it via `studio_setup` with `action: "save_api_key"` and `[REDACTED_SECRET]"`, or set `WEB2LABS_API_KEY` directly. ### Test instance differences - No real billing (`BILLING_MODE=test`), no real emails, no analytics. - Local disk storage instead of GCS. - Separate database (`web2labs_test`). - Lower resource limits than production. - Projects and data are fully isolated from production. ## Error Handling - Network timeout: retry up to 3 times with backoff. - 429 rate-limited: wait `retryAfter` then retry. - Worker unavailable: surface clear retry guidance. - Invalid file format: explain supported formats. - Failed processing: include `project_id` in support guidance. ## Setup If API key is missing, run onboarding with `studio_setup`: 1. Send magic link: `studio_setup` with `action: "send_magic_link"` and `email`. 2. Ask user for the code from email (or auto-read via email skill). 3. Complete setup: `studio_setup` with `action: "complete_setup"`, `email`, and `code`. 4. API key is generated and saved to `~/.openclaw/openclaw.json`. If user already has an API key: - `studio_setup` with `action: "save_api_key"` and `api_key`. Manual config example: ```json { "skills": { "entries": { "@web2labs/studio": { "apiKey": "w2l_xxxxx" } } } } ``` ## Feedback Use `studio_feedback` when users want to report: - `bug` - `suggestion` - `question` Include `project_id` when applicable. ## Referral Program - `studio_referral`: Get the user's referral code/link/stats, or apply a friend's referral code. Every user gets a unique referral code (format: `STUDIO-XXXX`). When someone signs up using a code, both parties get **5 free API credits** (60-day expiry). ### When to mention referrals - **After first successful project**: "Want 5 free credits? Share your referral link!" - **When asked about free credits**: Explain the referral program. - **During onboarding**: "Have a referral code from a friend? Use `studio_referral` with `action: apply`." - **When credits are low**: "You can earn 5 credits per referral — up to 50 total." ### How to use - Get code/link/stats: `studio_referral` with `action: "get"`. - Apply a friend's code: `studio_referral` with `action: "apply"` and `code: "STUDIO-XXXX"`. - Codes can only be applied within 24 hours of account creation. - Users cannot apply their own code. - Maximum 50 credits earned per user from referrals (10 referrals).

path: CHANGELOG.md
content: # Changelog

## Unreleased

## 1.0.1 - 2026-02-23

### Security
- Prevent leaking `WEB2LABS_API_KEY` to third-party artifact URLs by stripping auth headers on non-Web2Labs domains.
- Sanitize remote filenames to prevent path traversal and unsafe writes during `studio_download`.

### Fixed
- Align OpenClaw `skillKey` with setup writer so saved API key is correctly injected on future runs.
- Use deep merge for watch mode configuration overrides instead of shallow `Object.assign`.
- Collapse SKILL.md metadata to single-line JSON for OpenClaw parser compatibility.

### Added
- `studio_setup` tool for zero-touch onboarding (`send_magic_link`, `complete_setup`, `save_api_key`).
- Hardened auth-flow error handling (`invalid_code`, `key_already_exists`, rate-limit messaging).
- Revenue-aware tools: `studio_pricing`, `studio_estimate`, `studio_thumbnails`, and `studio_analytics`.
- `studio_rerender` for configuration-only rerenders of completed projects.
- Spend-policy enforcement for paid actions (`smart|explicit|auto`) with `confirm_spend` support.
- Checkout deep links (`ref=openclaw`) and recommendations in `studio_pricing` and `studio_credits` outputs.
- Preflight insufficient-credit detection with purchase-link hints for paid actions.
- `studio_brand` tool and API client support for `/api/v1/brand` (get/update brand kit settings).
- `studio_brand_import` tool and API client support for `/api/v1/brand/import` (preview/apply from YouTube/Twitch/X profile URLs).
- Upload webhook support (`webhook_url`, `webhook_secret`) for `project.completed` callbacks.
- `studio_assets` tool and API client support for `/api/v1/assets` (intro/outro/watermark list/upload/delete).
- `studio_watch` tool for monitoring YouTube/Twitch channels with auto-processing, retry tracking, and rate-limited uploads.
- `studio_referral` tool for referral code management and bonus credits.
- Contextual `next_steps` hints in tool responses for download, results, upload, credits, and setup.
- Dependency checks (Node.js version, yt-dlp availability) and trust/privacy block in `studio_setup` responses.
- Sandbox mode documentation in SKILL.md.

## 1.0.0 - 2026-02-21
- Initial OpenClaw Studio skill release.
- Added 9 core tools for upload, polling, results, download, credits, project management, and feedback.
- Added preset catalog and URL download support via yt-dlp.


path: claw.json
content: {
  "name": "@web2labs/studio",
  "version": "1.0.1",
  "description": "Edit my recording, turn a long video into shorts, generate captions and thumbnails, estimate cost before processing. Upload local files or YouTube/Twitch URLs and get back edited jump-cut videos, vertical shorts, subtitles, and thumbnail variants.",
  "author": "web2labs",
  "license": "MIT",
  "homepage": "https://web2labs.com/openclaw",
  "repository": "https://github.com/web2labs/web2labs-studio-skill",
  "entry": "SKILL.md",
  "tags": [
    "video-editing",
    "ai",
    "youtube",
    "shorts",
    "subtitles",
    "tiktok",
    "content-creation",
    "media",
    "automation",
    "twitch",
    "jump-cuts",
    "captions",
    "thumbnails",
    "podcast",
    "batch",
    "webhook",
    "brand-kit",
    "rerender",
    "creator"
  ],
  "models": ["*"],
  "minOpenClawVersion": "1.0.0",
  "permissions": ["network", "filesystem"],
  "mcp": {
    "server": "./src/server.mjs",
    "tools": [
      "studio_setup",
      "studio_upload",
      "studio_status",
      "studio_poll",
      "studio_results",
      "studio_download",
      "studio_credits",
      "studio_pricing",
      "studio_estimate",
      "studio_thumbnails",
      "studio_rerender",
      "studio_analytics",
      "studio_brand",
      "studio_brand_import",
      "studio_assets",
      "studio_projects",
      "studio_delete",
      "studio_feedback",
      "studio_referral",
      "studio_watch"
    ]
  }
}


path: CONTRIBUTING.md
content: # Contributing

Thanks for your interest in improving the Web2Labs Studio skill. This document covers setup, testing, and pull request expectations.

## Prerequisites

- Node.js 18+
- npm
- Optional: `yt-dlp` (for URL-based workflows and watch mode)

## Setup

```bash
git clone https://github.com/web2labs/web2labs-studio-skill.git
cd web2labs-studio-skill
npm install
```

## Running Tests

```bash
# Unit tests (mocked, no network)
npm test

# Integration tests (mocked API flows)
npm run test:integration

# End-to-end tests (mocked full workflows)
npm run test:e2e

# All quality gates
npm run test:hardening
```

Unit tests use Node.js built-in test runner (`node --test`) with no external test framework.

## Code Style

- ES modules (`.mjs` extension).
- Classes with static methods for tools and utilities.
- No TypeScript — plain JavaScript with JSDoc where helpful.
- No comments that narrate what the code does. Comments explain non-obvious intent only.

## Pull Request Guidelines

1. **One concern per PR.** Security fixes, features, and refactors should be separate.
2. **Run `npm test` before submitting.** All tests must pass.
3. **Fill out the PR template.** The security checklist is mandatory for any PR that touches network, filesystem, or auth logic.
4. **Update CHANGELOG.md** under `## Unreleased` with a line describing your change.
5. **Do not commit secrets.** No API keys, tokens, or credentials in code or tests.

## Security Contributions

If your contribution involves security-sensitive changes (auth, network destinations, file paths, spend confirmations), call it out explicitly in the PR description and verify:

- Auth headers are not sent to non-Web2Labs domains.
- File paths from external sources are sanitized via `safeFilename()`.
- Paid actions go through spend policy enforcement.
- No secrets appear in logs or tool output.

For vulnerability reports, see [SECURITY.md](SECURITY.md).

## Versioning

This skill follows [semver](https://semver.org/). ClawHub expects versioned releases with changelogs.

- **Patch** (1.0.x): bug fixes, security patches, doc improvements.
- **Minor** (1.x.0): new tools, new features, non-breaking changes.
- **Major** (x.0.0): breaking changes to tool schemas or behavior.

## License

By contributing, you agree that your contributions will be licensed under the [MIT License](LICENSE).


path: examples/batch-processing.md
content: # Batch Processing

Prompt:
`Process all videos in ~/recordings/ as youtube videos.`

Guardrail:
Always confirm total credit cost before starting batch jobs.


path: examples/gaming-montage.md
content: # Gaming Montage

Prompt:
`Edit ~/Videos/match.mp4 with the gaming preset.`

Highlights:
- Dynamic zoom
- Gaming pacing
- Split shorts layout


path: examples/podcast-cleanup.md
content: # Podcast Cleanup

Prompt:
`Clean up ~/Podcasts/episode.mp4 with the podcast preset.`

Highlights:
- Soft cuts
- Subtitles on
- No shorts
- No background music


path: examples/quick-edit.md
content: # Quick Edit

Prompt:
`Edit ~/Desktop/recording.mp4 with the quick preset.`

Flow:
- studio_credits
- studio_upload (preset=quick)
- studio_poll
- studio_download


path: examples/shorts-machine.md
content: # Shorts Machine

Prompt:
`Make 5 shorts from ~/Desktop/stream.mp4.`

Use preset: `shorts-only`.


path: examples/twitch-automation.md
content: # Twitch Automation

Prompt:
`Watch my channel https://twitch.tv/my_channel and process new VODs with the gaming preset.`

Requires yt-dlp.
State file suggestion: `~/.openclaw/web2labs-studio/watched.json`


path: examples/url-input.md
content: # URL Input

Prompt:
`Edit https://youtube.com/watch?v=abc123 as a tutorial video.`

Behavior:
- Skill checks for yt-dlp
- Downloads locally
- Uploads resulting file to Studio
- Cleans temporary files


path: examples/youtube-ready.md
content: # YouTube Ready

Prompt:
`Edit ~/Desktop/episode.mp4 as a youtube video.`

If spend confirmation is requested by policy, approve and re-run with:
`confirm_spend: true`

Optional brand consistency step:
`studio_brand` with `action: "update"` and colors (for example `primary_color: "#1a73e8"`).

One-click brand setup alternative:
`studio_brand_import` with `url: "https://youtube.com/@yourchannel"` (preview), then repeat with `apply: true`.

Optional reusable media step:
`studio_assets` with `action: "upload"` and `asset_type: "intro"` (or `outro` / `watermark`) before upload.

Optional automation step:
include `webhook_url: "https://example.com/studio-callback"` on upload to skip polling.

Expected outputs:
- Main edited video
- 3 shorts
- Subtitles
- Transcription


path: package-lock.json
content: {
  "name": "@web2labs/studio-skill",
  "version": "1.0.1",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "@web2labs/studio-skill",
      "version": "1.0.1",
      "license": "MIT",
      "dependencies": {
        "@modelcontextprotocol/sdk": "^1.26.0",
        "form-data": "^4.0.5",
        "node-fetch": "^3.3.2",
        "socket.io-client": "^4.8.3",
        "zod": "^4.3.6"
      },
      "engines": {
        "node": ">=18.0.0"
      }
    },
    "node_modules/@hono/node-server": {
      "version": "1.19.9",
      "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=18.14.1"
      },
      "peerDependencies": {
        "hono": "^4"
      }
    },
    "node_modules/@modelcontextprotocol/sdk": {
      "version": "1.26.0",
      "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "@hono/node-server": "^1.19.9",
        "ajv": "^8.17.1",
        "ajv-formats": "^3.0.1",
        "content-type": "^1.0.5",
        "cors": "^2.8.5",
        "cross-spawn": "^7.0.5",
        "eventsource": "^3.0.2",
        "eventsource-parser": "^3.0.0",
        "express": "^5.2.1",
        "express-rate-limit": "^8.2.1",
        "hono": "^4.11.4",
        "jose": "^6.1.3",
        "json-schema-typed": "^8.0.2",
        "pkce-challenge": "^5.0.0",
        "raw-body": "^3.0.0",
        "zod": "^3.25 || ^4.0",
        "zod-to-json-schema": "^3.25.1"
      },
      "engines": {
        "node": ">=18"
      },
      "peerDependencies": {
        "@cfworker/json-schema": "^4.1.1",
        "zod": "^3.25 || ^4.0"
      },
      "peerDependenciesMeta": {
        "@cfworker/json-schema": {
          "optional": true
        },
        "zod": {
          "optional": false
        }
      }
    },
    "node_modules/@socket.io/component-emitter": {
      "version": "3.1.2",
      "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/accepts": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "mime-types": "^3.0.0",
        "negotiator": "^1.0.0"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/ajv": {
      "version": "8.18.0",
      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "fast-deep-equal": "^3.1.3",
        "fast-uri": "^3.0.1",
        "json-schema-traverse": "^1.0.0",
        "require-from-string": "^2.0.2"
      },
      "funding": {
        "type": "github",
        "url": "https://github.com/sponsors/epoberezkin"
      }
    },
    "node_modules/ajv-formats": {
      "version": "3.0.1",
      "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ajv": "^8.0.0"
      },
      "peerDependencies": {
        "ajv": "^8.0.0"
      },
      "peerDependenciesMeta": {
        "ajv": {
          "optional": true
        }
      }
    },
    "node_modules/asynckit": {
      "version": "0.4.0",
      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/body-parser": {
      "version": "2.2.2",
      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "bytes": "^3.1.2",
        "content-type": "^1.0.5",
        "debug": "^4.4.3",
        "http-errors": "^2.0.0",
        "iconv-lite": "^0.7.0",
        "on-finished": "^2.4.1",
        "qs": "^6.14.1",
        "raw-body": "^3.0.1",
        "type-is": "^2.0.1"
      },
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/bytes": {
      "version": "3.1.2",
      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/call-bind-apply-helpers": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/call-bound": {
      "version": "1.0.4",
      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.2",
        "get-intrinsic": "^1.3.0"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/combined-stream": {
      "version": "1.0.8",
      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "delayed-stream": "~1.0.0"
      },
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/content-disposition": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/content-type": {
      "version": "1.0.5",
      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/cookie": {
      "version": "0.7.2",
      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/cookie-signature": {
      "version": "1.2.2",
      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=6.6.0"
      }
    },
    "node_modules/cors": {
      "version": "2.8.6",
      "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "object-assign": "^4",
        "vary": "^1"
      },
      "engines": {
        "node": ">= 0.10"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/cross-spawn": {
      "version": "7.0.6",
      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "path-key": "^3.1.0",
        "shebang-command": "^2.0.0",
        "which": "^2.0.1"
      },
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/data-uri-to-buffer": {
      "version": "4.0.1",
      "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 12"
      }
    },
    "node_modules/debug": {
      "version": "4.4.3",
      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ms": "^2.1.3"
      },
      "engines": {
        "node": ">=6.0"
      },
      "peerDependenciesMeta": {
        "supports-color": {
          "optional": true
        }
      }
    },
    "node_modules/delayed-stream": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.4.0"
      }
    },
    "node_modules/depd": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/dunder-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.1",
        "es-errors": "^1.3.0",
        "gopd": "^1.2.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/ee-first": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/encodeurl": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/engine.io-client": {
      "version": "6.6.4",
      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "@socket.io/component-emitter": "~3.1.0",
        "debug": "~4.4.1",
        "engine.io-parser": "~5.2.1",
        "ws": "~8.18.3",
        "xmlhttprequest-ssl": "~2.1.1"
      }
    },
    "node_modules/engine.io-parser": {
      "version": "5.2.3",
      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=10.0.0"
      }
    },
    "node_modules/es-define-property": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-errors": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-object-atoms": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-set-tostringtag": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.6",
        "has-tostringtag": "^1.0.2",
        "hasown": "^2.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/escape-html": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/etag": {
      "version": "1.8.1",
      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/eventsource": {
      "version": "3.0.7",
      "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "eventsource-parser": "^3.0.1"
      },
      "engines": {
        "node": ">=18.0.0"
      }
    },
    "node_modules/eventsource-parser": {
      "version": "3.0.6",
      "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=18.0.0"
      }
    },
    "node_modules/express": {
      "version": "5.2.1",
      "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "accepts": "^2.0.0",
        "body-parser": "^2.2.1",
        "content-disposition": "^1.0.0",
        "content-type": "^1.0.5",
        "cookie": "^0.7.1",
        "cookie-signature": "^1.2.1",
        "debug": "^4.4.0",
        "depd": "^2.0.0",
        "encodeurl": "^2.0.0",
        "escape-html": "^1.0.3",
        "etag": "^1.8.1",
        "finalhandler": "^2.1.0",
        "fresh": "^2.0.0",
        "http-errors": "^2.0.0",
        "merge-descriptors": "^2.0.0",
        "mime-types": "^3.0.0",
        "on-finished": "^2.4.1",
        "once": "^1.4.0",
        "parseurl": "^1.3.3",
        "proxy-addr": "^2.0.7",
        "qs": "^6.14.0",
        "range-parser": "^1.2.1",
        "router": "^2.2.0",
        "send": "^1.1.0",
        "serve-static": "^2.2.0",
        "statuses": "^2.0.1",
        "type-is": "^2.0.1",
        "vary": "^1.1.2"
      },
      "engines": {
        "node": ">= 18"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/express-rate-limit": {
      "version": "8.2.1",
      "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ip-address": "10.0.1"
      },
      "engines": {
        "node": ">= 16"
      },
      "funding": {
        "url": "https://github.com/sponsors/express-rate-limit"
      },
      "peerDependencies": {
        "express": ">= 4.11"
      }
    },
    "node_modules/fast-deep-equal": {
      "version": "3.1.3",
      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/fast-uri": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/fastify"
        },
        {
          "type": "opencollective",
          "url": "https://opencollective.com/fastify"
        }
      ],
      "license": "BSD-3-Clause"
    },
    "node_modules/fetch-blob": {
      "version": "3.2.0",
      "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/jimmywarting"
        },
        {
          "type": "paypal",
          "url": "https://paypal.me/jimmywarting"
        }
      ],
      "license": "MIT",
      "dependencies": {
        "node-domexception": "^1.0.0",
        "web-streams-polyfill": "^3.0.3"
      },
      "engines": {
        "node": "^12.20 || >= 14.13"
      }
    },
    "node_modules/finalhandler": {
      "version": "2.1.1",
      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "debug": "^4.4.0",
        "encodeurl": "^2.0.0",
        "escape-html": "^1.0.3",
        "on-finished": "^2.4.1",
        "parseurl": "^1.3.3",
        "statuses": "^2.0.1"
      },
      "engines": {
        "node": ">= 18.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/form-data": {
      "version": "4.0.5",
      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "asynckit": "^0.4.0",
        "combined-stream": "^1.0.8",
        "es-set-tostringtag": "^2.1.0",
        "hasown": "^2.0.2",
        "mime-types": "^2.1.12"
      },
      "engines": {
        "node": ">= 6"
      }
    },
    "node_modules/form-data/node_modules/mime-db": {
      "version": "1.52.0",
      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/form-data/node_modules/mime-types": {
      "version": "2.1.35",
      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "mime-db": "1.52.0"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/formdata-polyfill": {
      "version": "4.0.10",
      "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "fetch-blob": "^3.1.2"
      },
      "engines": {
        "node": ">=12.20.0"
      }
    },
    "node_modules/forwarded": {
      "version": "0.2.0",
      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/fresh": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/function-bind": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/get-intrinsic": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.2",
        "es-define-property": "^1.0.1",
        "es-errors": "^1.3.0",
        "es-object-atoms": "^1.1.1",
        "function-bind": "^1.1.2",
        "get-proto": "^1.0.1",
        "gopd": "^1.2.0",
        "has-symbols": "^1.1.0",
        "hasown": "^2.0.2",
        "math-intrinsics": "^1.1.0"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/get-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "dunder-proto": "^1.0.1",
        "es-object-atoms": "^1.0.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/gopd": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/has-symbols": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/has-tostringtag": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "has-symbols": "^1.0.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/hasown": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/hono": {
      "version": "4.12.1",
      "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=16.9.0"
      }
    },
    "node_modules/http-errors": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "depd": "~2.0.0",
        "inherits": "~2.0.4",
        "setprototypeof": "~1.2.0",
        "statuses": "~2.0.2",
        "toidentifier": "~1.0.1"
      },
      "engines": {
        "node": ">= 0.8"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/iconv-lite": {
      "version": "0.7.2",
      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "safer-buffer": ">= 2.1.2 < 3.0.0"
      },
      "engines": {
        "node": ">=0.10.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/inherits": {
      "version": "2.0.4",
      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/ip-address": {
      "version": "10.0.1",
      "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 12"
      }
    },
    "node_modules/ipaddr.js": {
      "version": "1.9.1",
      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.10"
      }
    },
    "node_modules/is-promise": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/isexe": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/jose": {
      "version": "6.1.3",
      "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/panva"
      }
    },
    "node_modules/json-schema-traverse": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/json-schema-typed": {
      "version": "8.0.2",
      "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause"
    },
    "node_modules/math-intrinsics": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/media-typer": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/merge-descriptors": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/mime-db": {
      "version": "1.54.0",
      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/mime-types": {
      "version": "3.0.2",
      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "mime-db": "^1.54.0"
      },
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/ms": {
      "version": "2.1.3",
      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/negotiator": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/node-domexception": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "deprecated": "Use your platform's native DOMException instead",
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/jimmywarting"
        },
        {
          "type": "github",
          "url": "https://paypal.me/jimmywarting"
        }
      ],
      "license": "MIT",
      "engines": {
        "node": ">=10.5.0"
      }
    },
    "node_modules/node-fetch": {
      "version": "3.3.2",
      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "data-uri-to-buffer": "^4.0.0",
        "fetch-blob": "^3.1.4",
        "formdata-polyfill": "^4.0.10"
      },
      "engines": {
        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/node-fetch"
      }
    },
    "node_modules/object-assign": {
      "version": "4.1.1",
      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/object-inspect": {
      "version": "1.13.4",
      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/on-finished": {
      "version": "2.4.1",
      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ee-first": "1.1.1"
      },
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/once": {
      "version": "1.4.0",
      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "dependencies": {
        "wrappy": "1"
      }
    },
    "node_modules/parseurl": {
      "version": "1.3.3",
      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/path-key": {
      "version": "3.1.1",
      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/path-to-regexp": {
      "version": "8.3.0",
      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/pkce-challenge": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=16.20.0"
      }
    },
    "node_modules/proxy-addr": {
      "version": "2.0.7",
      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "forwarded": "0.2.0",
        "ipaddr.js": "1.9.1"
      },
      "engines": {
        "node": ">= 0.10"
      }
    },
    "node_modules/qs": {
      "version": "6.15.0",
      "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-3-Clause",
      "dependencies": {
        "side-channel": "^1.1.0"
      },
      "engines": {
        "node": ">=0.6"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/range-parser": {
      "version": "1.2.1",
      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/raw-body": {
      "version": "3.0.2",
      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "bytes": "~3.1.2",
        "http-errors": "~2.0.1",
        "iconv-lite": "~0.7.0",
        "unpipe": "~1.0.0"
      },
      "engines": {
        "node": ">= 0.10"
      }
    },
    "node_modules/require-from-string": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/router": {
      "version": "2.2.0",
      "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "debug": "^4.4.0",
        "depd": "^2.0.0",
        "is-promise": "^4.0.0",
        "parseurl": "^1.3.3",
        "path-to-regexp": "^8.0.0"
      },
      "engines": {
        "node": ">= 18"
      }
    },
    "node_modules/safer-buffer": {
      "version": "2.1.2",
      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/send": {
      "version": "1.2.1",
      "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "debug": "^4.4.3",
        "encodeurl": "^2.0.0",
        "escape-html": "^1.0.3",
        "etag": "^1.8.1",
        "fresh": "^2.0.0",
        "http-errors": "^2.0.1",
        "mime-types": "^3.0.2",
        "ms": "^2.1.3",
        "on-finished": "^2.4.1",
        "range-parser": "^1.2.1",
        "statuses": "^2.0.2"
      },
      "engines": {
        "node": ">= 18"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/serve-static": {
      "version": "2.2.1",
      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "encodeurl": "^2.0.0",
        "escape-html": "^1.0.3",
        "parseurl": "^1.3.3",
        "send": "^1.2.0"
      },
      "engines": {
        "node": ">= 18"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/setprototypeof": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/shebang-command": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "shebang-regex": "^3.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/shebang-regex": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/side-channel": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "object-inspect": "^1.13.3",
        "side-channel-list": "^1.0.0",
        "side-channel-map": "^1.0.1",
        "side-channel-weakmap": "^1.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-list": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "object-inspect": "^1.13.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-map": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.2",
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.5",
        "object-inspect": "^1.13.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-weakmap": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.2",
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.5",
        "object-inspect": "^1.13.3",
        "side-channel-map": "^1.0.1"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/socket.io-client": {
      "version": "4.8.3",
      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "@socket.io/component-emitter": "~3.1.0",
        "debug": "~4.4.1",
        "engine.io-client": "~6.6.1",
        "socket.io-parser": "~4.2.4"
      },
      "engines": {
        "node": ">=10.0.0"
      }
    },
    "node_modules/socket.io-parser": {
      "version": "4.2.5",
      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "@socket.io/component-emitter": "~3.1.0",
        "debug": "~4.4.1"
      },
      "engines": {
        "node": ">=10.0.0"
      }
    },
    "node_modules/statuses": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/toidentifier": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.6"
      }
    },
    "node_modules/type-is": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "content-type": "^1.0.5",
        "media-typer": "^1.1.0",
        "mime-types": "^3.0.0"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/unpipe": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/vary": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/web-streams-polyfill": {
      "version": "3.3.3",
      "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/which": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "dependencies": {
        "isexe": "^2.0.0"
      },
      "bin": {
        "node-which": "bin/node-which"
      },
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/wrappy": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/ws": {
      "version": "8.18.3",
      "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=10.0.0"
      },
      "peerDependencies": {
        "bufferutil": "^4.0.1",
        "utf-8-validate": ">=5.0.2"
      },
      "peerDependenciesMeta": {
        "bufferutil": {
          "optional": true
        },
        "utf-8-validate": {
          "optional": true
        }
      }
    },
    "node_modules/xmlhttprequest-ssl": {
      "version": "2.1.2",
      "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": ">=0.4.0"
      }
    },
    "node_modules/zod": {
      "version": "4.3.6",
      "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/colinhacks"
      }
    },
    "node_modules/zod-to-json-schema": {
      "version": "3.25.1",
      "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "peerDependencies": {
        "zod": "^3.25 || ^4"
      }
    }
  }
}


path: package.json
content: {
  "name": "@web2labs/studio-skill",
  "version": "1.0.1",
  "description": "OpenClaw skill for Web2Labs Studio - AI-powered video editing",
  "type": "module",
  "main": "src/server.mjs",
  "scripts": {
    "start": "node src/server.mjs",
    "test": "node --test test/*.test.mjs",
    "test:integration": "node --test ../tests/integration/*.test.mjs",
    "test:e2e": "node --test ../tests/e2e/*.test.mjs",
    "test:live": "node --test ../tests/live/*.test.mjs",
    "test:hardening": "npm test && npm run test:integration && npm run test:e2e",
    "lint": "node --check src/server.mjs src/lib/*.mjs src/tools/*.mjs",
    "sync": "../scripts/sync-skill-repo.sh",
    "publish:clawhub": "../scripts/publish-to-clawhub.sh"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.26.0",
    "form-data": "^4.0.5",
    "node-fetch": "^3.3.2",
    "socket.io-client": "^4.8.3",
    "zod": "^4.3.6"
  },
  "engines": {
    "node": ">=18.0.0"
  },
  "keywords": [
    "openclaw",
    "video-editing",
    "ai",
    "youtube",
    "shorts"
  ],
  "author": "Web2Labs <[REDACTED_SECRET]>",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/web2labs/web2labs-studio-skill"
  }
}


path: README.md
content: <p align="center">
  <a href="https://www.web2labs.com">
    <img src="https://www.web2labs.com/assets/images/logo.svg" alt="Web2Labs Studio" width="320" />
  </a>
</p>

<h3 align="center">AI video editing - turns videos & streams into<br/>complete YouTube videos & shorts</h3>

<p align="center">
  <a href="https://www.web2labs.com/openclaw"><strong>Landing Page</strong></a> · 
  <a href="https://www.web2labs.com"><strong>Web App</strong></a> · 
  <a href="https://www.web2labs.com/api/v1/docs"><strong>API Docs</strong></a> · 
  <a href="https://github.com/web2labs/web2labs-studio-examples"><strong>Examples</strong></a>
</p>

<p align="center">
  <img src="https://img.shields.io/badge/OpenClaw-skill-blue?style=flat-square" alt="OpenClaw Skill" />
  <img src="https://img.shields.io/badge/node-%3E%3D18-brightgreen?style=flat-square" alt="Node.js 18+" />
  <img src="https://img.shields.io/badge/tools-20-orange?style=flat-square" alt="20 MCP Tools" />
  <img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="MIT License" />
</p>

---

## What is Web2Labs Studio?

**Web2Labs Studio** is a cloud video editing platform that turns raw recordings into publish-ready content - automatically. No timeline. No editing software. Just upload and get back:

<table>
<tr>
<td width="33%" align="center">

**Edited Video**

Dead air removed. Pacing tightened. Dynamic zoom punch-ins. Animated captions. Audio normalized. Every second earns its place.

</td>
<td width="33%" align="center">

**25+ Shorts**

Vertical clips auto-extracted from your recording, ready to post on YouTube Shorts, TikTok, Instagram Reels, and X.

</td>
<td width="33%" align="center">

**Full Meta Package**

AI-generated titles, descriptions, tags, chapters, pinned comments, and A/B/C thumbnail variants - everything you need to publish.

</td>
</tr>
</table>



### High Retention Cut vs. Normal Video

<table>
<tr>
<td align="center"><strong>Studio Output</strong></td>
<td align="center"><strong>Raw Recording</strong></td>
</tr>
<tr>
<td align="center">
<img src="assets/retention-cut-sample.gif" alt="High retention cut by Studio" width="400" />
<br/><em>Dead air cut. Pacing tightened. Every second earns its place.</em>
</td>
<td align="center">
<img src="assets/normal-cut-sample.gif" alt="Normal unedited video" width="400" />
<br/><em>Silences, filler, and rambles. Viewers scroll past.</em>
</td>
</tr>
</table>

### See the difference

<table>
<tr>
<td align="center"><strong>Studio Thumbnail</strong></td>
<td align="center"><strong>Amateur Thumbnail</strong></td>
</tr>
<tr>
<td align="center">
<img src="https://www.web2labs.com/assets/studio/thumbnails/thumbnail_1.webp" alt="Studio-generated thumbnail" width="400" />
<br/><em>AI-generated, brand-colored, click-worthy.</em>
</td>
<td align="center">
<img src="https://www.web2labs.com/assets/studio/thumbnails/thumbnail-amateur.webp" alt="Amateur thumbnail" width="400" />
<br/><em>Generic frame grab. Viewers scroll right past.</em>
</td>
</tr>
</table>

> [See the full interactive comparison on web2labs.com →](https://www.web2labs.com)

### Who is it for?

YouTube creators, Twitch streamers, podcasters, educators, coaches - anyone who records video and wants it edited without the grind. Studio handles the tedious parts (cutting silence, adding captions, generating shorts) so you can focus on creating.

---

## This Skill: Studio in Your Terminal

This OpenClaw skill gives you the **full power of Studio** from your AI assistant. Talk to your agent, and it handles everything:

```
You:   "Edit my latest recording as a YouTube video"
Agent: Uploading ~/Desktop/recording.mp4... Processing with 'youtube' preset.
       Transcribing → Cutting → Rendering... Done in 3m 42s.
       
       Downloaded to ~/studio-exports/my-recording/:
       ├── main.mp4              (edited video, 18:24)
       ├── shorts/
       │   ├── short-1.mp4       (0:58)
       │   ├── short-2.mp4       (0:45)
       │   └── ... 23 more
       ├── subtitles.srt
       └── transcription.json
       
       Want me to generate thumbnail variants? (10 Creator Credits)
```

### From URL - no download needed

```
You:   "Make shorts from https://youtube.com/watch?v=dQw4w9WgXcQ"
Agent: Downloading via yt-dlp... Uploading to Studio...
       Processing with 'shorts-only' preset... Done.
       27 shorts ready for download.
```

---

## Quick Start

**3 commands. 2 free credits. No credit card.**

```bash
# 1. Install the skill
clawhub install @web2labs/studio

# 2. Set up your account (sends a magic link to your email)
"Run studio_setup with my email [REDACTED_SECRET]"

# 3. Edit your first video
"Edit ~/Desktop/my-recording.mp4 as a youtube video"
```

That's it. Your first two videos are free.

---

## What You Get: The Full Package

Every video processed through Studio returns a complete content package:

### Edited Video
- **Smart jump-cuts** - AI removes dead air, filler words, and awkward pauses while preserving meaning
- **Dynamic zoom punch-ins** - automatic Ken Burns-style zooms at the right moments to hold attention
- **Animated captions** - word-level subtitles styled to your brand, because most viewers watch on mute
- **Tighter pacing** - rambles tightened, momentum maintained, every minute feels intentional
- **Audio normalization** - broadcast-standard loudness (-14 LUFS) so you sound professional
- **High-quality render** - CRF 18, ready to upload directly

### Vertical Shorts
- **25+ shorts per recording** - auto-extracted highlight clips with clean hooks and payoffs
- **Multi-platform ready** - formatted for YouTube Shorts, TikTok, Instagram Reels, and X
- **One recording → one week of content** - never go dark on social media again

### Meta Package
- **A/B/C thumbnail variants** - AI-generated, brand-colored thumbnails you can test
- **Titles, descriptions, tags** - optimized for search and click-through
- **Chapters** - auto-generated from your transcript so viewers can jump in
- **Pinned comment** - ready to copy-paste for engagement

---

## 8 Presets for Every Content Style

| Preset | Best For | What It Does |
|--------|----------|-------------|
| `youtube` | Standard YouTube videos | Subtitles + shorts + music + full meta |
| `quick` | Fast turnaround | Clean cuts, no extras, fastest render |
| `shorts-only` | Social media blitz | Only generates vertical shorts |
| `podcast` | Talking-head podcasts | Soft cuts, subtitles, no zoom |
| `gaming` | Streams & gameplay | Dynamic zoom, gaming-style pacing |
| `tutorial` | Educational content | Gentle edits, preserves explanations |
| `vlog` | Daily vlogs | Balanced pacing, natural feel |
| `cinematic` | High production | Premium settings, slower pacing |

Mix presets with custom overrides for full control:

```
"Edit my video with the youtube preset but disable music and set subtitle style to minimal"
```

---

## 20 Tools at Your Fingertips

<details>
<summary><strong>Core Workflow</strong></summary>

| Tool | What It Does |
|------|-------------|
| `studio_setup` | One-time account setup via magic link or existing API key |
| `studio_upload` | Upload local files or URLs (YouTube, Twitch, Vimeo) for processing |
| `studio_status` | Check current project processing status |
| `studio_poll` | Smart polling - waits for completion with progress updates |
| `studio_results` | Get output URLs and metadata for a completed project |
| `studio_download` | Download all outputs to your local filesystem |
| `studio_projects` | List your recent projects |
| `studio_delete` | Delete a project and free up storage |
| `studio_watch` | Watch YouTube/Twitch channels and auto-process new uploads |

</details>

<details>
<summary><strong>Premium Features</strong></summary>

| Tool | What It Does |
|------|-------------|
| `studio_thumbnails` | Generate A/B/C thumbnail variants for any completed project |
| `studio_rerender` | Re-render with different settings - no re-upload needed (first free, then 15 CC) |
| `studio_brand` | Save your brand colors, fonts, and identity for consistent output |
| `studio_brand_import` | Auto-import brand settings from your YouTube or Twitch channel |
| `studio_assets` | Upload reusable intros, outros, and watermarks |
| `studio_analytics` | See your usage stats and time saved |

</details>

<details>
<summary><strong>Credits & Pricing</strong></summary>

| Tool | What It Does |
|------|-------------|
| `studio_credits` | Check your API credit and Creator Credit balances |
| `studio_pricing` | Get current pricing for all features and bundles |
| `studio_estimate` | Estimate exact cost before uploading |
| `studio_feedback` | Report bugs, request features, or ask questions |
| `studio_referral` | Get your referral code - earn 5 free credits per signup |

</details>

---

## Pricing

**2 free credits on signup. No credit card required.**

Every video costs **1 API credit** (or 2 for rush priority). Premium features like thumbnails use Creator Credits.

| Bundle | Credits | Price | Per Video |
|--------|---------|-------|-----------|
| **Free** | 2 | €0 | €0 |
| Casual | 10 | €22.99 | €2.30 |
| Starter | 20 | €39.99 | €2.00 |
| Pro | 100 | €199.99 | €2.00 |
| Studio | 250 | €449.99 | €1.80 |
| Agency | 1000 | €1,699.99 | €1.70 |

**Subscribers** (Starter €20/mo, Creator €50/mo, Legend €800 lifetime) can use their monthly project allocation through the skill - as low as €0.50/video on Creator tier.

[View full pricing →](https://www.web2labs.com/pricing)

---

## Referral Program

Every user gets a unique referral code. Share it and both you and your friend get **5 free API credits** (60-day expiry).

```
You:   "What's my referral code?"
Agent: Your code is STUDIO-K7M3
       Share: https://www.web2labs.com/ref/STUDIO-K7M3
       
       Stats: 3 referrals, 15 credits earned, 7 slots remaining
```

Earn up to **50 free credits** (10 referrals). That's 50 fully edited videos - for free.

---

## Advanced Workflows

### Brand Consistency

Set up your brand once, and every video matches your style:

```
You:   "Import my brand from https://youtube.com/@mychannel"
Agent: Imported from YouTube: primary #1a73e8, secondary #ff6f00,
       identity: "Tech educator focused on practical tutorials"
       Applied to your brand kit. All future videos will use these settings.
```

### Batch Processing

```
You:   "Process all .mp4 files in ~/recordings/ as youtube videos"
Agent: Found 5 files. This will consume 5 API credits. Proceed?
You:   "Yes"
Agent: Processing 5 videos... [████████░░] 3/5 complete
```

### Fire-and-Forget with Webhooks

```
You:   "Upload this video and send results to https://my-server.com/callback"
Agent: Uploaded with webhook. You'll receive a signed callback at
       project.completed - no need to poll.
```

### Re-render Without Re-upload

```
You:   "Add subtitles to that last video"
Agent: Re-rendering with subtitles enabled. First re-render is free!
       No re-upload needed - reusing existing analysis.
```

### Watch Mode - Auto-Process New Uploads

Set up a watcher on your channel and Studio processes every new upload automatically:

```
You:   "Watch my YouTube channel https://youtube.com/@mychannel for new videos"
Agent: Watcher created for 'mychannel' with youtube preset.
       Max 5 uploads/day, skipping videos over 2 hours.
       
       Run "check my watchers" to poll for new videos, or set up a cron job.
```

```
You:   "Check my watchers"
Agent: Checking 1 watcher...
       
       mychannel: 2 new videos found
       ├── "Building a REST API" (24:31) → project p_abc123, uploading...
       └── "React Server Components" (18:07) → project p_def456, uploading...
       
       2 videos uploaded and processing. Use studio_poll to track progress.
```

Works with YouTube channels, YouTube live VODs, and Twitch channels. Requires `yt-dlp`.

---

## Spend Control

You're always in control of what gets spent. Configure your spend policy:

| Mode | Behavior |
|------|----------|
| `auto` (default) | Proceeds without prompts unless auto-caps are exceeded |
| `smart` | Confirms high-cost or low-balance actions |
| `explicit` | Confirms every credit-spending action |

```bash
# Set in your environment
export WEB2LABS_SPEND_POLICY=explicit
```

---

## Requirements

- **Node.js 18+**
- **Web2Labs account** - [sign up free](https://www.web2labs.com/openclaw) (2 free credits, no card)
- **Optional:** `yt-dlp` for URL-based workflows and watch mode (`brew install yt-dlp`)

---

## Configuration

The skill reads from environment variables or OpenClaw config (`~/.openclaw/openclaw.json`):

| Variable | Default | Description |
|----------|---------|-------------|
| `WEB2LABS_API_KEY` | - | Your API key (starts with `sk_live_`) |
| `WEB2LABS_API_ENDPOINT` | `https://www.web2labs.com` | API base URL |
| `WEB2LABS_DEFAULT_PRESET` | `youtube` | Default preset when none specified |
| `WEB2LABS_DOWNLOAD_DIR` | `~/studio-exports` | Where downloads are saved |
| `WEB2LABS_SPEND_POLICY` | `auto` | Spend confirmation mode |

---

## Security

- API keys are stored locally in your OpenClaw config - never sent to third parties
- Auth headers are stripped for non-Web2Labs download URLs - no credential leakage to CDN/S3
- The skill never logs or displays full API key values
- URL downloads happen locally on your machine via yt-dlp - files never touch third-party servers
- All API communication uses HTTPS
- Webhook callbacks are HMAC-SHA256 signed with your secret
- Remote filenames are sanitized to prevent path traversal

For the full security posture and vulnerability reporting, see [SECURITY.md](SECURITY.md).

---

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, testing, and pull request guidelines.

---

## Links

- **Product:** [web2labs.com](https://www.web2labs.com) - see real before/after examples
- **OpenClaw Landing:** [web2labs.com/openclaw](https://www.web2labs.com/openclaw) - skill-specific info + install
- **API Documentation:** [web2labs.com/api/v1/docs](https://www.web2labs.com/api/v1/docs) - Swagger/OpenAPI reference
- **Examples:** [github.com/web2labs/web2labs-studio-examples](https://github.com/web2labs/web2labs-studio-examples)
- **Support:** Use `studio_feedback` or email [[REDACTED_SECRET]](mailto:[REDACTED_SECRET])

---

<p align="center">
  <strong>Record once. Publish everywhere.</strong><br/>
  <a href="https://www.web2labs.com/openclaw">Start free →</a>
</p>


path: SECURITY.md
content: # Security Policy

## Reporting a Vulnerability

If you discover a security vulnerability in this skill, **do not open a public issue**. Instead, email us directly:

**[REDACTED_SECRET]**

Include:
- Description of the vulnerability
- Steps to reproduce
- Affected tool(s) or file(s)
- Potential impact

We will acknowledge receipt within 48 hours and provide a fix timeline within 5 business days.

## Security Posture

This skill is designed to meet OpenClaw's "treat tools as code" security expectations.

### What this skill does

- Communicates with `web2labs.com` over HTTPS to upload videos and retrieve results.
- Stores your API key locally in `~/.openclaw/openclaw.json` with `chmod 600` permissions.
- Downloads URL-based videos locally via `yt-dlp` — files never pass through third-party intermediaries.
- Sends signed webhook callbacks (HMAC-SHA256) when configured.

### What this skill never does

- Executes arbitrary shell commands.
- Uploads data to any service other than Web2Labs.
- Sends API keys or secrets to third-party domains (auth headers are stripped for non-Web2Labs URLs).
- Logs or displays full API key values.
- Uses your content for AI model training.

### Data retention

- **Free users:** uploads deleted 2 hours after processing.
- **Paid users:** retention per subscription plan.
- All processing happens on Web2Labs infrastructure; no data is shared with third parties.

### Dependency audit

This skill uses a minimal dependency set:

| Package | Purpose |
|---------|---------|
| `@modelcontextprotocol/sdk` | MCP protocol (required by OpenClaw) |
| `node-fetch` | HTTP client |
| `form-data` | Multipart file uploads |
| `socket.io-client` | Real-time progress updates |
| `zod` | Input schema validation |

## Supported Versions

| Version | Supported |
|---------|-----------|
| 1.0.x   | Yes       |


path: src/lib/auth-flow.mjs
content: import { chmod, mkdir, readFile, writeFile } from "node:fs/promises"
import { homedir } from "node:os"
import { dirname, join } from "node:path"
import fetch from "node-fetch"

export class AuthFlowError extends Error {
  constructor(message, code = "auth_flow_error", status = 500, details = null) {
    super(message)
    this.name = "AuthFlowError"
    this.code = code
    this.status = status
    this.details = details
  }
}

export class AuthFlow {
  static DEFAULT_TIMEOUT_MS = 20000

  static normalizeApiEndpoint(apiEndpoint) {
    return String(apiEndpoint || "https://www.web2labs.com").replace(/\/$/, "")
  }

  static async parsePayload(response) {
    const raw = await response.text()
    if (!raw) {
      return null
    }

    try {
      return JSON.parse(raw)
    } catch {
      return {
        raw,
      }
    }
  }

  static createApiError(response, payload, fallbackCode, fallbackMessage) {
    const code = payload?.error?.code || fallbackCode
    const message = payload?.error?.message || fallbackMessage
    return new AuthFlowError(message, code, response.status, payload?.error || null)
  }

  static buildBasicAuthHeader(basicAuth) {
    if (!basicAuth) return {}
    const encoded = Buffer.from(basicAuth).toString("base64")
    return { Authorization: `Basic ${encoded}` }
  }

  static async requestJson(url, options = {}) {
    const timeoutMs = Number(options.timeoutMs || AuthFlow.DEFAULT_TIMEOUT_MS)
    const controller = new AbortController()
    const timer = setTimeout(() => controller.abort(), timeoutMs)

    try {
      const response = await fetch(url, {
        ...options,
        signal: controller.signal,
      })
      const payload = await AuthFlow.parsePayload(response)
      return { response, payload }
    } catch (error) {
      if (error?.name === "AbortError") {
        throw new AuthFlowError(
          "Setup request timed out. Please try again.",
          "timeout",
          408
        )
      }
      throw new AuthFlowError(
        error?.message || "Network error during setup.",
        "network_error",
        503
      )
    } finally {
      clearTimeout(timer)
    }
  }

  static getConfigPath() {
    const override = process.env.OPENCLAW_CONFIG_PATH
    if (override && override.trim()) {
      return override.trim()
    }
    return join(homedir(), ".openclaw", "openclaw.json")
  }

  static getConfigDir() {
    return dirname(AuthFlow.getConfigPath())
  }

  static async sendMagicLink(apiEndpoint, email, basicAuth = null) {
    const normalizedEndpoint = AuthFlow.normalizeApiEndpoint(apiEndpoint)
    const { response, payload } = await AuthFlow.requestJson(
      `${normalizedEndpoint}/api/auth/magic/send`,
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          ...AuthFlow.buildBasicAuthHeader(basicAuth),
        },
        body: JSON.stringify({ email }),
      }
    )
    if (!response.ok) {
      if (response.status === 429) {
        const retryIn = Number(payload?.error?.details?.retryIn || 60)
        throw new AuthFlowError(
          `Rate limited. Please wait ${retryIn} seconds and retry.`,
          "rate_limited",
          response.status,
          payload?.error?.details || null
        )
      }
      throw AuthFlow.createApiError(
        response,
        payload,
        "magic_send_failed",
        "Failed to send magic link"
      )
    }

    return {
      sent: true,
      email: payload?.data?.email || email,
      retryInSeconds: null,
    }
  }

  static async completeMagicLinkToken(apiEndpoint, email, code, basicAuth = null) {
    const normalizedEndpoint = AuthFlow.normalizeApiEndpoint(apiEndpoint)
    const { response, payload } = await AuthFlow.requestJson(
      `${normalizedEndpoint}/api/auth/magic/token`,
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          ...AuthFlow.buildBasicAuthHeader(basicAuth),
        },
        body: JSON.stringify({ state: email, code }),
      }
    )
    if (!response.ok) {
      if (payload?.error?.code === "invalid_code") {
        throw new AuthFlowError(
          "Invalid or expired code. Request a new magic link and retry.",
          "invalid_code",
          response.status,
          payload?.error?.details || null
        )
      }
      throw AuthFlow.createApiError(
        response,
        payload,
        "magic_token_failed",
        "Authentication failed"
      )
    }

    return {
      accessToken: payload?.data?.accessToken,
      userId: payload?.data?.userId,
      tier: payload?.data?.tier,
      expiresIn: payload?.data?.expiresIn,
    }
  }

  static async generateApiKey(apiEndpoint, accessToken, basicAuth = null) {
    // Bearer token goes in Authorization header; Basic Auth is intentionally
    // omitted here — the server endpoint should not require it.
    // regenerateIfExists: if the user already has an API key, revoke it and
    // create a new one instead of returning a key_already_exists error.
    const normalizedEndpoint = AuthFlow.normalizeApiEndpoint(apiEndpoint)
    const { response, payload } = await AuthFlow.requestJson(
      `${normalizedEndpoint}/api/user/api-key/generate`,
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${accessToken}`,
        },
        body: JSON.stringify({ regenerateIfExists: true }),
      }
    )
    if (!response.ok) {
      if (payload?.error?.code === "key_already_exists") {
        throw new AuthFlowError(
          "API key already exists. Open https://web2labs.com/user/api to view or regenerate your key.",
          "key_already_exists",
          response.status,
          payload?.error?.details || null
        )
      }
      throw AuthFlow.createApiError(
        response,
        payload,
        "api_key_generate_failed",
        "Failed to generate API key"
      )
    }

    return {
      key: payload?.data?.key,
      keyPrefix: payload?.data?.keyPrefix,
      freeCredits: payload?.data?.freeCredits || 0,
      message: payload?.data?.message || "API key generated",
    }
  }

  static async storeApiKey(apiKey) {
    const configPath = AuthFlow.getConfigPath()
    const configDir = AuthFlow.getConfigDir()

    let config = {}
    try {
      const raw = await readFile(configPath, "utf-8")
      config = JSON.parse(raw)
    } catch (err) {
      if (err?.code === "ENOENT") {
        await mkdir(configDir, { recursive: true })
      } else if (err instanceof SyntaxError) {
        throw new AuthFlowError(
          `Config file ${configPath} contains invalid JSON. Please fix or delete it manually.`,
          "config_corrupt",
          500
        )
      } else {
        throw new AuthFlowError(
          `Unable to read config file ${configPath}: ${err?.message || err}`,
          "config_read_error",
          500
        )
      }
    }

    if (!config.skills) {
      config.skills = {}
    }
    if (!config.skills.entries) {
      config.skills.entries = {}
    }
    if (!config.skills.entries["@web2labs/studio"]) {
      config.skills.entries["@web2labs/studio"] = {}
    }

    config.skills.entries["@web2labs/studio"].enabled = true
    config.skills.entries["@web2labs/studio"].[REDACTED_SECRET]

    await writeFile(configPath, JSON.stringify(config, null, 2), "utf-8")

    try {
      await chmod(configPath, 0o600)
    } catch {
      // Best effort; platform may not support chmod semantics.
    }

    return {
      stored: true,
      path: configPath,
    }
  }
}


path: src/lib/downloader.mjs
content: import { execFile } from "node:child_process"
import { mkdtemp, rm, stat } from "node:fs/promises"
import { readdirSync } from "node:fs"
import { tmpdir } from "node:os"
import { basename, join } from "node:path"
import { promisify } from "node:util"

const execFileAsync = promisify(execFile)

export class VideoDownloader {
  static SUPPORTED_DOMAINS = [
    "youtube.com",
    "youtu.be",
    "www.youtube.com",
    "twitch.tv",
    "www.twitch.tv",
    "vimeo.com",
    "www.vimeo.com",
    "dailymotion.com",
    "www.dailymotion.com",
    "streamable.com",
    "reddit.com",
    "www.reddit.com",
  ]

  static isUrl(input) {
    try {
      const url = new URL(input)
      return url.protocol === "http:" || url.protocol === "https:"
    } catch {
      return false
    }
  }

  static isSupportedUrl(input) {
    try {
      const url = new URL(input)
      const host = url.hostname.toLowerCase()
      return VideoDownloader.SUPPORTED_DOMAINS.some(
        (domain) => host === domain || host.endsWith(`.${domain}`)
      )
    } catch {
      return false
    }
  }

  static async checkYtDlp() {
    try {
      const { stdout } = await execFileAsync("yt-dlp", ["--version"])
      return { installed: true, version: stdout.trim() }
    } catch {
      return { installed: false, version: null }
    }
  }

  static async getVideoInfo(url) {
    const { stdout } = await execFileAsync(
      "yt-dlp",
      ["--dump-json", "--no-download", url],
      { timeout: 30000 }
    )

    const info = JSON.parse(stdout)
    return {
      title: info.title || "Untitled",
      duration: Number(info.duration || 0),
      uploader: info.uploader || null,
      uploadDate: info.upload_date || null,
      description: info.description || null,
      filesize: info.filesize_approx || null,
    }
  }

  static async download(url, options = {}) {
    const tmpPath = await mkdtemp(join(tmpdir(), "w2l-dl-"))
    try {
      const outputTemplate = join(tmpPath, "%(title)s.%(ext)s")

      const args = [
        "-f",
        "bestvideo[height<=1080]+bestaudio/best[height<=1080]/best",
        "--merge-output-format",
        "mp4",
        "--no-playlist",
        "--no-overwrites",
        "--restrict-filenames",
        "--output",
        outputTemplate,
      ]

      if (Number(options.maxDuration) > 0) {
        args.push("--match-filter", `duration<=${Number(options.maxDuration)}`)
      }

      args.push(url)

      const { stdout, stderr } = await execFileAsync("yt-dlp", args, {
        timeout: Number(options.timeout || 600000),
        maxBuffer: 5 * 1024 * 1024,
      })

      const fromMerge = stderr.match(/\[Merger\] Merging formats into \"(.+)\"/)
      const fromExisting = stderr.match(/\[download\] (.+\.mp4) has already been downloaded/)
      const fromDestination = stdout.match(/\[download\] Destination: (.+)/)

      let filePath = null
      if (fromMerge?.[1]) {
        filePath = fromMerge[1]
      } else if (fromExisting?.[1]) {
        filePath = fromExisting[1]
      } else if (fromDestination?.[1]) {
        filePath = fromDestination[1]
      }

      if (!filePath) {
        const candidates = readdirSync(tmpPath)
          .filter((name) => name.toLowerCase().endsWith(".mp4"))
          .map((name) => join(tmpPath, name))
        if (candidates.length > 0) {
          filePath = candidates[0]
        }
      }

      if (!filePath) {
        throw new Error("Download completed but output file was not found")
      }

      const fileStats = await stat(filePath)
      return {
        filePath,
        tmpDir: tmpPath,
        fileSize: Number(fileStats.size || 0),
        fileName: basename(filePath),
      }
    } catch (err) {
      err.tmpDir = tmpPath
      throw err
    }
  }

  static async cleanup(tmpPath) {
    if (!tmpPath) {
      return
    }
    try {
      await rm(tmpPath, { recursive: true, force: true })
    } catch {
      // Best-effort cleanup.
    }
  }

  static async listChannelVods(channelUrl, limit = 10) {
    const args = [
      "--flat-playlist",
      "--print",
      "%(id)s\t%(title)s\t%(upload_date)s\t%(duration)s",
      "--playlist-end",
      String(limit),
      channelUrl,
    ]

    const { stdout } = await execFileAsync("yt-dlp", args, { timeout: 30000 })

    return stdout
      .trim()
      .split("\n")
      .filter(Boolean)
      .map((line) => {
        const [id, title, date, duration] = line.split("\t")
        return {
          id,
          title,
          date,
          duration: Number(duration || 0),
        }
      })
  }
}


path: src/tools/brand-import.mjs
content: export class BrandImportTool {
  static normalizeApply(value) {
    if (value === true) return true
    return String(value || "")
      .trim()
      .toLowerCase() === "true"
  }

  static async execute(context, params = {}) {
    const url = String(params.url || "").trim()
    if (!url) {
      throw new Error("url is required")
    }
    if (url.length > 2048) {
      throw new Error("URL is too long (max 2048 characters)")
    }
    if (!/^https?:\/\/.+/i.test(url)) {
      throw new Error("URL must start with http:// or https://")
    }

    const apply = BrandImportTool.normalizeApply(params.apply)
    const result = await context.apiClient.importBrand({
      url,
      apply,
    })

    return {
      action: apply ? "apply" : "preview",
      ...result,
    }
  }
}



Security scan verdicts — ClawScan: clean | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)