Skill: vibevideo/vibevideo-core (version 1.0.5)

--- name: vibevideoio-ai-script-to-video description: Log into bollo.video or vibevideo.io, list Studio projects, create Studio episodes from prompts or scripts, and log out. Use when users ask to make or generate an AI video, paste a script/story and ask to turn it into a video, or manage Studio login/projects. --- # VibeVideoIO AI Script to Video This package exposes a single canonical skill: - `skills/vibevideoio-ai-script-to-video/SKILL.md` ## Install Notes - Local OpenClaw skill registration is manual via `npm run openclaw:register` - The package does not auto-register itself during `postinstall` ## When To Use - Create / make / generate an AI video - Turn a prompt / script / story into a video - Paste a screenplay or short video script and ask to make it into an AI video - Log into `bollo.video` or `vibevideo.io` - List or manage Studio projects ## Default Site Rule - Chinese input → `bollo.video` - English input → `vibevideo.io` - Explicit user choice overrides the default ## Site Consistency Rule - If the login site is `vibevideo.io`, keep all subsequent Studio operations on the `vibevideo.io` web/API endpoints - If the login site is `bollo.video`, keep all subsequent Studio operations on the `bollo.video` web/API endpoints - Do not mix `vibevideo.io` web pages or APIs with a `bollo.video` login session, and do not mix `bollo.video` web pages or APIs with a `vibevideo.io` login session ## Error And Retry Policy - For login continuation errors, return the exact CLI/API error to the user first - Only retry login when the user explicitly asks, or when the error clearly means the CAPTCHA expired/was incorrect and a fresh CAPTCHA is required - For `create-episode` failures such as `fetch failed`, timeout, HTTP/API errors, or backend generation errors, return the failure result directly and stop - When returning a `create-episode` failure, also include any known generation context such as video ID, title, aspect ratio, style mode, and project information - Do not automatically rerun `create-episode`, do not silently switch sites, and do not change the script/style/aspect ratio/project to retry on the user's behalf - After returning the error, ask the user whether they want to retry manually or adjust inputs ## Source Of Truth For all detailed behavior, follow: - `skills/vibevideoio-ai-script-to-video/SKILL.md` That file is the single source of truth for: - login and CAPTCHA flow - Feishu vs local preview behavior - episode creation questions - CLI command usage and expected results ## Key Files - `skills/vibevideoio-ai-script-to-video/SKILL.md` - `skills/vibevideoio-ai-script-to-video/scripts/vibevideo-studio.mjs` - `agents/openai.yaml` - `references/current-system-map.md`

path: openclaw.plugin.json
content: {
  "id": "vibevideoio-ai-script-to-video",
  "name": "VibeVideoIO AI Script to Video",
  "description": "Create AI videos from prompts or scripts, log into bollo.video or vibevideo.io, list Studio projects, and create Studio episodes. Chinese requests default to bollo.video; English requests default to vibevideo.io unless explicitly overridden.",
  "skills": [
    "./skills"
  ],
  "configSchema": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "defaultSite": {
        "type": "string",
        "enum": [
          "bollo",
          "vibevideo"
        ]
      },
      "defaultAspectRatio": {
        "type": "string"
      },
      "defaultLocale": {
        "type": "string",
        "enum": [
          "zh",
          "en"
        ]
      }
    }
  }
}


path: index.js
content: export default function register(_api) {}


path: package.json
content: {
  "name": "@vibevideo/agent-bridge-skill",
  "version": "0.1.0",
  "private": true,
  "description": "OpenClaw skill package for VibeVideoIO AI Script to Video, including Studio login, project listing, and episode creation from scripts.",
  "type": "module",
  "engines": {
    "node": ">=20"
  },
  "files": [
    "SKILL.md",
    "agents",
    "index.js",
    "index.ts",
    "openclaw.plugin.json",
    "references",
    "scripts",
    "skills",
    "src"
  ],
  "exports": {
    ".": "./src/index.ts"
  },
  "bin": {
    "openclaw-vibevideoio-ai-script-to-video": "skills/vibevideoio-ai-script-to-video/scripts/vibevideo-studio.mjs"
  },
  "scripts": {
    "openclaw:register": "node ./scripts/register-openclaw-skill.mjs",
    "studio": "node ./skills/vibevideoio-ai-script-to-video/scripts/vibevideo-studio.mjs",
    "studio:help": "node ./skills/vibevideoio-ai-script-to-video/scripts/vibevideo-studio.mjs --help",
    "typecheck": "tsc -p tsconfig.json --noEmit"
  },
  "openclaw": {
    "extensions": [
      "./index.js"
    ]
  }
}


path: tsconfig.json
content: {
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "lib": [
      "ES2022"
    ]
  },
  "include": [
    "src/**/*.ts"
  ]
}


path: index.ts
content: export default function register(_api: unknown) {}


path: src/contracts.ts
content: export const HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH"] as const;
export type HttpMethod = (typeof HTTP_METHODS)[number];

export const RESPONSE_CONTRACTS = ["standard", "legacy", "typed", "redirect", "planned"] as const;
export type ResponseContract = (typeof RESPONSE_CONTRACTS)[number];

export const PROVIDER_TRANSPORTS = ["redirect", "embedded", "popup", "server_proxy", "server_only"] as const;
export type ProviderTransport = (typeof PROVIDER_TRANSPORTS)[number];

export const AUTH_STRATEGIES = [
  "oauth_redirect",
  "email_link",
  "email_code",
  "session_handoff",
  "internal_service",
] as const;
export type AgentAuthStrategy = (typeof AUTH_STRATEGIES)[number];

export const CAPABILITY_CATEGORIES = [
  "auth",
  "identity",
  "access",
  "scripts",
  "tools",
  "assets",
  "projects",
  "notifications",
  "agent_bridge",
  "service",
] as const;
export type CapabilityCategory = (typeof CAPABILITY_CATEGORIES)[number];

export const CAPABILITY_STATUSES = ["implemented", "planned", "provider_specific"] as const;
export type CapabilityStatus = (typeof CAPABILITY_STATUSES)[number];

export const AGENT_SCOPES = ["anonymous", "authenticated", "owner", "sub_user", "internal"] as const;
export type AgentScope = (typeof AGENT_SCOPES)[number];

export const AGENT_LAUNCHERS = ["redirect", "embedded", "popup", "server_only"] as const;
export type AgentLauncher = (typeof AGENT_LAUNCHERS)[number];

export const BRIDGE_MODES = ["backend_only", "frontend_callback", "hybrid"] as const;
export type BridgeMode = (typeof BRIDGE_MODES)[number];

export const AUDIT_EVENTS = [
  "session_issued",
  "session_exchanged",
  "capability_denied",
  "provider_callback_received",
  "task_started",
  "task_completed",
] as const;
export type AgentAuditEventName = (typeof AUDIT_EVENTS)[number];

export interface AgentRouteBinding {
  method: HttpMethod;
  path: string;
  responseContract: ResponseContract;
  localeHeader?: boolean;
  notes?: string;
}

export interface AgentCapabilityDescriptor {
  key: string;
  title: string;
  category: CapabilityCategory;
  status: CapabilityStatus;
  scopes: ReadonlyArray<AgentScope>;
  route?: AgentRouteBinding;
  dependsOn?: ReadonlyArray<string>;
  tags?: ReadonlyArray<string>;
  notes?: string;
}

export interface AgentAuthSurface {
  primaryStrategy: AgentAuthStrategy;
  fallbackStrategies?: ReadonlyArray<AgentAuthStrategy>;
  login?: AgentRouteBinding;
  verify?: AgentRouteBinding;
  callback?: AgentRouteBinding;
  currentUser?: AgentRouteBinding;
  cookieSetup?: AgentRouteBinding;
  sessionBrokerPath?: string;
  bridgeTokenTtlSeconds?: number;
  neverSharePrimaryCookie: boolean;
}

export interface AgentProviderUi {
  launcher: AgentLauncher;
  entryPath?: string;
  callbackPath?: string;
}

export interface AgentProviderSecurity {
  bridgeMode: BridgeMode;
  auditEventNamespace: string;
  requiresAuditTrail: boolean;
  allowedOrigins?: ReadonlyArray<string>;
}

export interface AgentProviderManifest {
  providerId: string;
  displayName: string;
  providerUrl?: string;
  transport: ProviderTransport;
  auth: AgentAuthSurface;
  capabilities: ReadonlyArray<string>;
  requiredScopes: ReadonlyArray<AgentScope>;
  extensionPoints?: ReadonlyArray<CapabilityCategory>;
  ui: AgentProviderUi;
  security: AgentProviderSecurity;
  notes?: string;
}

export interface AgentBridgeSessionGrant {
  providerId: string;
  ownerUserId: string;
  subUserId?: string | null;
  tokenId?: string | null;
  locale: string;
  capabilityKeys: ReadonlyArray<string>;
  scopes: ReadonlyArray<AgentScope>;
  expiresAt: string;
  callbackUrl?: string;
  nonce: string;
}

export interface AgentBridgeAuditEvent {
  providerId: string;
  event: AgentAuditEventName;
  ownerUserId: string;
  subUserId?: string | null;
  tokenId?: string | null;
  timestamp: string;
  metadata?: Record<string, unknown>;
}


path: src/index.ts
content: export * from "./contracts";
export * from "./catalog";
export * from "./manifests";


path: src/manifests.ts
content: import { defineCapabilityCatalog, defineProviderManifest } from "./catalog";
import type { AgentCapabilityDescriptor } from "./contracts";

export const VIBEVIDEO_AGENT_CAPABILITY_CATALOG = defineCapabilityCatalog([
  {
    key: "auth.google_oauth.start",
    title: "Start Google OAuth login",
    category: "auth",
    status: "implemented",
    scopes: ["anonymous"],
    route: {
      method: "GET",
      path: "/api/auth/login",
      responseContract: "legacy",
      localeHeader: true,
      notes: "Returns Google OAuth URL and derives locale-aware redirect URI.",
    },
    tags: ["auth", "google", "oauth"],
  },
  {
    key: "auth.google_oauth.callback",
    title: "Complete Google OAuth login",
    category: "auth",
    status: "implemented",
    scopes: ["anonymous"],
    route: {
      method: "POST",
      path: "/api/auth/callback",
      responseContract: "legacy",
      localeHeader: true,
      notes: "Returns a local VibeVideo access token.",
    },
    tags: ["auth", "google", "oauth"],
  },
  {
    key: "auth.wechat.login_config",
    title: "Get WeChat login config",
    category: "auth",
    status: "implemented",
    scopes: ["anonymous"],
    route: {
      method: "GET",
      path: "/api/auth/wechat/login-config",
      responseContract: "standard",
      localeHeader: true,
    },
    tags: ["auth", "wechat"],
  },
  {
    key: "auth.wechat.callback",
    title: "Complete WeChat login",
    category: "auth",
    status: "implemented",
    scopes: ["anonymous"],
    route: {
      method: "POST",
      path: "/api/auth/wechat/callback",
      responseContract: "standard",
      localeHeader: true,
    },
    tags: ["auth", "wechat"],
  },
  {
    key: "auth.captcha.generate",
    title: "Generate CAPTCHA",
    category: "auth",
    status: "implemented",
    scopes: ["anonymous"],
    route: {
      method: "GET",
      path: "/api/auth/captcha/generate",
      responseContract: "typed",
      localeHeader: true,
    },
    tags: ["auth", "captcha"],
  },
  {
    key: "auth.captcha.verify",
    title: "Verify CAPTCHA",
    category: "auth",
    status: "implemented",
    scopes: ["anonymous"],
    route: {
      method: "POST",
      path: "/api/auth/captcha/verify",
      responseContract: "typed",
      localeHeader: true,
      notes: "Also doubles as email-link login sender in the current backend.",
    },
    tags: ["auth", "captcha", "email"],
  },
  {
    key: "auth.email_code.send",
    title: "Send email verification code",
    category: "auth",
    status: "implemented",
    scopes: ["anonymous", "owner"],
    route: {
      method: "POST",
      path: "/api/auth/email-code/send",
      responseContract: "standard",
      localeHeader: true,
      notes: "Supports owner login and sub-user bootstrap.",
    },
    tags: ["auth", "email", "verification-code"],
    dependsOn: ["auth.captcha.generate", "auth.captcha.verify"],
  },
  {
    key: "auth.email_code.verify",
    title: "Verify email code and issue session",
    category: "auth",
    status: "implemented",
    scopes: ["anonymous", "owner"],
    route: {
      method: "POST",
      path: "/api/auth/email-code/verify",
      responseContract: "standard",
      localeHeader: true,
    },
    tags: ["auth", "email", "verification-code"],
  },
  {
    key: "auth.email_link.verify",
    title: "Verify email login link",
    category: "auth",
    status: "implemented",
    scopes: ["anonymous"],
    route: {
      method: "GET",
      path: "/api/auth/email-verify",
      responseContract: "redirect",
      localeHeader: true,
    },
    tags: ["auth", "email", "magic-link"],
  },
  {
    key: "auth.cookie_setup",
    title: "Set cookies from issued local token",
    category: "auth",
    status: "implemented",
    scopes: ["anonymous", "authenticated"],
    route: {
      method: "POST",
      path: "/api/auth/email-token-setup",
      responseContract: "legacy",
      localeHeader: false,
      notes: "Useful when a provider or email flow returns a local token to the browser.",
    },
    tags: ["auth", "cookie", "session"],
  },
  {
    key: "identity.current_user",
    title: "Read current user profile",
    category: "identity",
    status: "implemented",
    scopes: ["authenticated", "owner", "sub_user"],
    route: {
      method: "GET",
      path: "/api/users/me",
      responseContract: "typed",
      localeHeader: true,
    },
    tags: ["identity", "profile"],
  },
  {
    key: "access.overview.read",
    title: "Read access-control overview",
    category: "access",
    status: "implemented",
    scopes: ["owner", "sub_user"],
    route: {
      method: "GET",
      path: "/api/access/overview",
      responseContract: "standard",
      localeHeader: true,
    },
    tags: ["access", "permissions"],
  },
  {
    key: "access.sub_users.read",
    title: "List sub users",
    category: "access",
    status: "implemented",
    scopes: ["owner"],
    route: {
      method: "GET",
      path: "/api/access/sub-users",
      responseContract: "standard",
      localeHeader: true,
    },
    tags: ["access", "sub-user"],
  },
  {
    key: "access.sub_users.create",
    title: "Create sub user",
    category: "access",
    status: "implemented",
    scopes: ["owner"],
    route: {
      method: "POST",
      path: "/api/access/sub-users",
      responseContract: "standard",
      localeHeader: true,
    },
    tags: ["access", "sub-user", "write"],
  },
  {
    key: "access.sub_users.update",
    title: "Update sub user",
    category: "access",
    status: "implemented",
    scopes: ["owner"],
    route: {
      method: "PUT",
      path: "/api/access/sub-users/{sub_user_id}",
      responseContract: "standard",
      localeHeader: true,
    },
    tags: ["access", "sub-user", "write"],
  },
  {
    key: "access.sub_users.delete",
    title: "Delete sub user",
    category: "access",
    status: "implemented",
    scopes: ["owner"],
    route: {
      method: "DELETE",
      path: "/api/access/sub-users/{sub_user_id}",
      responseContract: "standard",
      localeHeader: true,
    },
    tags: ["access", "sub-user", "write"],
  },
  {
    key: "access.tokens.read",
    title: "List active session tokens",
    category: "access",
    status: "implemented",
    scopes: ["owner", "sub_user"],
    route: {
      method: "GET",
      path: "/api/access/tokens",
      responseContract: "standard",
      localeHeader: true,
    },
    tags: ["access", "tokens"],
  },
  {
    key: "access.tokens.update",
    title: "Update session token policy",
    category: "access",
    status: "implemented",
    scopes: ["owner", "sub_user"],
    route: {
      method: "PUT",
      path: "/api/access/tokens/{token_id}",
      responseContract: "standard",
      localeHeader: true,
    },
    tags: ["access", "tokens", "write"],
  },
  {
    key: "scripts.create_async",
    title: "Create a Studio episode draft from script text",
    category: "scripts",
    status: "implemented",
    scopes: ["owner", "sub_user"],
    route: {
      method: "POST",
      path: "/api/scripts/create_async",
      responseContract: "standard",
      localeHeader: true,
      notes: "Creates a single Studio episode draft and returns video_id/query_id.",
    },
    tags: ["scripts", "studio", "episode", "write"],
  },
  {
    key: "scripts.task_status.read",
    title: "Read Studio script task status",
    category: "scripts",
    status: "implemented",
    scopes: ["owner", "sub_user"],
    route: {
      method: "GET",
      path: "/api/scripts/task_status",
      responseContract: "standard",
      localeHeader: true,
      notes: "Poll with step=create_script until the Studio draft is ready.",
    },
    tags: ["scripts", "studio", "polling"],
  },
  {
    key: "tools.generate_async",
    title: "Create async tool generation task",
    category: "tools",
    status: "implemented",
    scopes: ["owner", "sub_user"],
    route: {
      method: "POST",
      path: "/api/tools/generate_async",
      responseContract: "standard",
      localeHeader: true,
      notes: "Current entry point for image and video generation tasks.",
    },
    tags: ["tools", "image", "video"],
  },
  {
    key: "tools.task_status.read",
    title: "Read tool task status",
    category: "tools",
    status: "implemented",
    scopes: ["owner", "sub_user"],
    route: {
      method: "GET",
      path: "/api/tools/task_status",
      responseContract: "standard",
      localeHeader: true,
    },
    tags: ["tools", "polling"],
  },
  {
    key: "tools.result.read",
    title: "Fetch tool task result",
    category: "tools",
    status: "implemented",
    scopes: ["owner", "sub_user"],
    route: {
      method: "GET",
      path: "/api/tools/fetch_result",
      responseContract: "standard",
      localeHeader: true,
    },
    tags: ["tools", "result"],
  },
  {
    key: "service.flags.sora2_not_stable.read",
    title: "Read service flag",
    category: "service",
    status: "implemented",
    scopes: ["anonymous", "authenticated"],
    route: {
      method: "GET",
      path: "/api/service/get-sora2-not-stable",
      responseContract: "standard",
      localeHeader: false,
    },
    tags: ["service", "flags"],
  },
  {
    key: "service.flags.sora2_not_stable.write",
    title: "Write service flag",
    category: "service",
    status: "implemented",
    scopes: ["owner", "sub_user"],
    route: {
      method: "POST",
      path: "/api/service/set-sora2-not-stable",
      responseContract: "standard",
      localeHeader: false,
    },
    tags: ["service", "flags", "write"],
  },
  {
    key: "agent_bridge.session.issue",
    title: "Issue provider bridge session",
    category: "agent_bridge",
    status: "planned",
    scopes: ["owner", "sub_user"],
    route: {
      method: "POST",
      path: "/api/agent-bridge/providers/{provider_id}/session",
      responseContract: "planned",
      localeHeader: true,
      notes: "Recommended future endpoint for short-lived provider handoff tokens.",
    },
    tags: ["agent-bridge", "session"],
  },
  {
    key: "projects.read",
    title: "Read projects",
    category: "projects",
    status: "implemented",
    scopes: ["owner", "sub_user"],
    route: {
      method: "GET",
      path: "/api/projects",
      responseContract: "standard",
      localeHeader: true,
      notes: "Returns Studio projects for the current account.",
    },
    tags: ["projects"],
    notes: "Used by OpenClaw to list Studio projects and build direct project-page links.",
  },
  {
    key: "projects.write",
    title: "Mutate projects",
    category: "projects",
    status: "planned",
    scopes: ["owner", "sub_user"],
    tags: ["projects", "write"],
    notes: "Keep disabled for initial OpenClaw binding.",
  },
  {
    key: "assets.upload.presign",
    title: "Presign asset uploads",
    category: "assets",
    status: "planned",
    scopes: ["owner", "sub_user"],
    tags: ["assets", "upload"],
    notes: "Future capability for provider-mediated uploads.",
  },
  {
    key: "notifications.email.send",
    title: "Trigger system email",
    category: "notifications",
    status: "planned",
    scopes: ["internal"],
    tags: ["notifications", "email"],
    notes: "Keep internal-only unless a provider has a clear business need.",
  },
] as const satisfies ReadonlyArray<AgentCapabilityDescriptor>);

export const OPENCLAW_PROVIDER_MANIFEST = defineProviderManifest(
  {
    providerId: "openclaw",
    displayName: "OpenClaw",
    providerUrl: "https://openclaw.example.com",
    transport: "redirect",
    auth: {
      primaryStrategy: "session_handoff",
      fallbackStrategies: ["email_code"],
      login: {
        method: "POST",
        path: "/api/agent-bridge/providers/openclaw/session",
        responseContract: "planned",
        localeHeader: true,
        notes: "Recommended bridge-session entry for OpenClaw.",
      },
      verify: {
        method: "POST",
        path: "/api/auth/email-code/verify",
        responseContract: "standard",
        localeHeader: true,
      },
      callback: {
        method: "GET",
        path: "/api/agent-bridge/providers/openclaw/callback",
        responseContract: "planned",
        localeHeader: true,
      },
      currentUser: {
        method: "GET",
        path: "/api/users/me",
        responseContract: "typed",
        localeHeader: true,
      },
      cookieSetup: {
        method: "POST",
        path: "/api/auth/email-token-setup",
        responseContract: "legacy",
        localeHeader: false,
      },
      sessionBrokerPath: "/api/agent-bridge/providers/openclaw/session",
      bridgeTokenTtlSeconds: 300,
      neverSharePrimaryCookie: true,
    },
    capabilities: [
      "auth.captcha.generate",
      "identity.current_user",
      "auth.email_code.send",
      "auth.email_code.verify",
      "access.tokens.update",
      "projects.read",
      "scripts.create_async",
      "scripts.task_status.read",
    ],
    requiredScopes: ["authenticated", "owner"],
    extensionPoints: ["auth", "identity", "access", "scripts", "projects", "agent_bridge"],
    ui: {
      launcher: "redirect",
      entryPath: "/agents/openclaw",
      callbackPath: "/api/agent-bridge/providers/openclaw/callback",
    },
    security: {
      bridgeMode: "backend_only",
      auditEventNamespace: "agent.openclaw",
      requiresAuditTrail: true,
      allowedOrigins: ["https://openclaw.example.com"],
    },
    notes:
      "Implemented OpenClaw scope: email-code login with CAPTCHA, Studio project listing, single-episode Studio draft creation from script text, and local logout with best-effort remote token expiry.",
  },
  VIBEVIDEO_AGENT_CAPABILITY_CATALOG,
);


path: src/catalog.ts
content: import type { AgentCapabilityDescriptor, AgentProviderManifest } from "./contracts";

const buildCapabilityMap = (
  catalog: ReadonlyArray<AgentCapabilityDescriptor>,
): Map<string, AgentCapabilityDescriptor> => {
  const capabilityMap = new Map<string, AgentCapabilityDescriptor>();

  for (const capability of catalog) {
    if (capabilityMap.has(capability.key)) {
      throw new Error(`Duplicate capability key: ${capability.key}`);
    }
    capabilityMap.set(capability.key, capability);
  }

  return capabilityMap;
};

export const defineCapabilityCatalog = <T extends ReadonlyArray<AgentCapabilityDescriptor>>(catalog: T): T => {
  buildCapabilityMap(catalog);
  return catalog;
};

export const defineProviderManifest = <T extends AgentProviderManifest>(
  manifest: T,
  catalog: ReadonlyArray<AgentCapabilityDescriptor>,
): T => {
  const capabilityMap = buildCapabilityMap(catalog);

  for (const capabilityKey of manifest.capabilities) {
    if (!capabilityMap.has(capabilityKey)) {
      throw new Error(
        `Provider "${manifest.providerId}" references unknown capability "${capabilityKey}"`,
      );
    }
  }

  if (manifest.auth.primaryStrategy === "session_handoff") {
    if (!manifest.auth.sessionBrokerPath) {
      throw new Error(
        `Provider "${manifest.providerId}" must define auth.sessionBrokerPath for session handoff`,
      );
    }
    if (!manifest.auth.bridgeTokenTtlSeconds || manifest.auth.bridgeTokenTtlSeconds <= 0) {
      throw new Error(
        `Provider "${manifest.providerId}" must define a positive auth.bridgeTokenTtlSeconds for session handoff`,
      );
    }
  }

  if (manifest.transport === "redirect" && !manifest.ui.callbackPath && !manifest.auth.callback?.path) {
    throw new Error(
      `Provider "${manifest.providerId}" must define a callback path for redirect transport`,
    );
  }

  return manifest;
};

export const pickCapabilities = (
  catalog: ReadonlyArray<AgentCapabilityDescriptor>,
  capabilityKeys: ReadonlyArray<string>,
): AgentCapabilityDescriptor[] => {
  const capabilityMap = buildCapabilityMap(catalog);

  return capabilityKeys.map((key) => {
    const capability = capabilityMap.get(key);
    if (!capability) {
      throw new Error(`Unknown capability requested: ${key}`);
    }
    return capability;
  });
};

export const summarizeCapabilities = (
  catalog: ReadonlyArray<AgentCapabilityDescriptor>,
): Record<string, number> => {
  return catalog.reduce<Record<string, number>>((summary, capability) => {
    summary[capability.status] = (summary[capability.status] ?? 0) + 1;
    return summary;
  }, {});
};


path: skills/vibevideoio-ai-script-to-video/scripts/vibevideo-studio-local.mjs
content: import fs from "node:fs/promises";
import fsSync from "node:fs";
import os from "node:os";
import path from "node:path";
import process from "node:process";

function normalizeStringId(value) {
  const normalized = String(value ?? "").trim();
  return normalized || "";
}

export function resolveOpenClawHome() {
  const explicit = String(process.env.OPENCLAW_HOME || "").trim();
  if (explicit) {
    return path.resolve(explicit);
  }
  return path.join(os.homedir(), ".openclaw");
}

export const DEFAULT_SESSION_FILE = path.join(resolveOpenClawHome(), "secrets", "vibevideo-studio-session.json");
export const DEFAULT_CAPTCHA_DIR = path.join(resolveOpenClawHome(), "media", "vibevideo-studio-captcha");

export function hasOpenClawSessionContext() {
  return Boolean(normalizeStringId(process.env.OPENCLAW_MCP_SESSION_KEY));
}

export function shouldEmitOpenClawMediaDirective(filePath) {
  return Boolean(normalizeStringId(filePath) && hasOpenClawSessionContext());
}

export async function readJsonFile(filename) {
  try {
    const content = await fs.readFile(filename, "utf8");
    const parsed = JSON.parse(content);
    return parsed && typeof parsed === "object" ? parsed : null;
  } catch {
    return null;
  }
}

export async function ensureDirectory(dirname) {
  await fs.mkdir(dirname, { recursive: true });
}

export async function writeSecureFile(filename, content) {
  await ensureDirectory(path.dirname(filename));
  await fs.writeFile(filename, content);
  await fs.chmod(filename, 0o600).catch(() => {});
}

export async function deleteFileIfExists(filename) {
  try {
    await fs.unlink(filename);
  } catch {}
}

export async function fileExists(filename) {
  try {
    await fs.access(filename, fsSync.constants.F_OK);
    return true;
  } catch {
    return false;
  }
}

export function getPendingLoginFile(sessionFile) {
  const resolvedSessionFile = String(sessionFile || DEFAULT_SESSION_FILE).trim() || DEFAULT_SESSION_FILE;
  return path.join(path.dirname(resolvedSessionFile), "vibevideo-studio-login-pending.json");
}

export async function readTextFile(filename) {
  return fs.readFile(filename, "utf8");
}


path: scripts/register-openclaw-skill.mjs
content: #!/usr/bin/env node

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import process from "node:process";

const SKILL_ID = "vibevideoio-ai-script-to-video";
const SKIP_ENV = "OPENCLAW_VIBEVIDEO_STUDIO_SKIP_POSTINSTALL";
const DISABLE_ENV = "OPENCLAW_VIBEVIDEO_STUDIO_AUTO_SETUP";
const MANAGED_MARKER = ".openclaw-managed.json";

function log(message) {
  process.stdout.write(`[${SKILL_ID}] ${message}\n`);
}

function warn(message) {
  process.stderr.write(`[${SKILL_ID}] ${message}\n`);
}

function resolveOpenClawHome() {
  const explicit = String(process.env.OPENCLAW_HOME || "").trim();
  if (explicit) {
    return path.resolve(explicit);
  }
  return path.join(os.homedir(), ".openclaw");
}

function resolveSkillSourceRoot() {
  return path.resolve(process.cwd());
}

function resolveTargetPath() {
  return path.join(resolveOpenClawHome(), "skills", SKILL_ID);
}

async function pathExists(targetPath) {
  try {
    await fs.lstat(targetPath);
    return true;
  } catch {
    return false;
  }
}

async function safeRealpath(targetPath) {
  try {
    return await fs.realpath(targetPath);
  } catch {
    return "";
  }
}

function shouldSkip(sourceRoot, targetPath) {
  if (process.env[SKIP_ENV] === "1") {
    return "skip flag detected";
  }

  if (process.env[DISABLE_ENV] === "0") {
    return `${DISABLE_ENV}=0`;
  }

  const normalizedSource = sourceRoot.replace(/\\/g, "/");
  const normalizedTarget = targetPath.replace(/\\/g, "/");
  if (normalizedSource === normalizedTarget || normalizedSource.startsWith(`${normalizedTarget}/`)) {
    return "running inside local OpenClaw skills path";
  }

  return "";
}

async function ensureSkillShape(sourceRoot) {
  const requiredFiles = [
    path.join(sourceRoot, "SKILL.md"),
    path.join(sourceRoot, "agents", "openai.yaml"),
    path.join(sourceRoot, "package.json"),
  ];

  for (const filename of requiredFiles) {
    if (!(await pathExists(filename))) {
      throw new Error(`Missing required skill file: ${filename}`);
    }
  }
}

async function ensureParentDir(targetPath) {
  await fs.mkdir(path.dirname(targetPath), { recursive: true });
}

async function removePath(targetPath) {
  try {
    const stat = await fs.lstat(targetPath);
    if (stat.isSymbolicLink()) {
      await fs.unlink(targetPath);
      return;
    }
  } catch {}
  await fs.rm(targetPath, { recursive: true, force: true });
}

async function readJsonIfExists(targetPath) {
  try {
    const content = await fs.readFile(targetPath, "utf8");
    return JSON.parse(content);
  } catch {
    return null;
  }
}

function shouldCopyEntry(entryPath) {
  const relative = entryPath.replace(/\\/g, "/");
  const blockedSegments = [
    "/.git",
    "/node_modules",
    "/.turbo",
    "/dist",
    "/coverage",
    "/.DS_Store",
  ];
  return !blockedSegments.some((segment) => relative.includes(segment));
}

async function copySkillDirectory(sourceRoot, targetPath) {
  await fs.cp(sourceRoot, targetPath, {
    recursive: true,
    force: true,
    errorOnExist: false,
    filter: (entryPath) => shouldCopyEntry(entryPath),
  });
}

async function writeManagedMarker(sourceRoot, targetPath) {
  const markerPath = path.join(targetPath, MANAGED_MARKER);
  const payload = {
    skillId: SKILL_ID,
    sourceRoot,
    updatedAt: new Date().toISOString(),
  };
  await fs.writeFile(markerPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
}

async function main() {
  const sourceRoot = resolveSkillSourceRoot();
  const targetPath = resolveTargetPath();

  const skipReason = shouldSkip(sourceRoot, targetPath);
  if (skipReason) {
    log(`Skip local OpenClaw registration: ${skipReason}.`);
    return;
  }

  await ensureSkillShape(sourceRoot);
  await ensureParentDir(targetPath);

  if (await pathExists(targetPath)) {
    const targetStat = await fs.lstat(targetPath);
    const sourceRealpath = await safeRealpath(sourceRoot);
    const targetRealpath = await safeRealpath(targetPath);
    const marker = await readJsonIfExists(path.join(targetPath, MANAGED_MARKER));

    if (!targetStat.isSymbolicLink() && sourceRealpath && targetRealpath && sourceRealpath === targetRealpath) {
      log(`Already registered at ${targetPath}.`);
      return;
    }

    if (targetStat.isSymbolicLink() || marker?.skillId === SKILL_ID) {
      await removePath(targetPath);
      await copySkillDirectory(sourceRoot, targetPath);
      await writeManagedMarker(sourceRoot, targetPath);
      log(`Updated local OpenClaw skill copy at ${targetPath}`);
      return;
    }

    warn(`Target already exists and is not managed by this package: ${targetPath}`);
    warn("Skip automatic registration to avoid overwriting local skill files.");
    warn(`If you want to replace it manually, remove the target and run: npm --prefix "${sourceRoot}" run openclaw:register`);
    return;
  }

  await copySkillDirectory(sourceRoot, targetPath);
  await writeManagedMarker(sourceRoot, targetPath);
  log(`Registered local OpenClaw skill at ${targetPath}`);
  log("You can verify it with: openclaw skills info vibevideoio-ai-script-to-video");
}

main().catch((error) => {
  warn(`Local OpenClaw registration skipped: ${error instanceof Error ? error.message : String(error)}`);
  process.exitCode = 0;
});


path: agents/openai.yaml
content: interface:
  display_name: "VibeVideoIO AI Script to Video"
  short_description: "Create AI videos and handle Studio login/project management."
  default_prompt: >-
    Use $vibevideoio-ai-script-to-video when the user wants to create or generate an
    AI video, turn a prompt/script/story into a video, paste a screenplay and ask
    to make it into a video, log into Studio, or manage Studio projects. Common
    triggers include requests like "帮我创作一个视频", "帮我做个视频", "生成视频",
    "把这个剧本做成 AI 视频", "我现在有个剧本，想把它做成 AI 视频", "create a
    video", "make me a video", "generate a video from this script", "turn this
    script into a video", "log into bollo.video", and "log into vibevideo.io".
    Unless the user explicitly specifies a site, use bollo.video for Chinese
    requests and vibevideo.io for English requests.

policy:
  allow_implicit_invocation: true


path: references/architecture.md
content: # Third-Party Agent Bridge Architecture

## Why This Package Exists
The current repo already has several strong building blocks:
- multi-path authentication in `backend/auth/auth_router.py`
- local session issuance in `backend/auth/google_auth.py`
- owner / sub-user permission modeling in `backend/routers/access_router.py`
- async tool execution in `backend/routers/tool_router.py`

What is missing is a **provider-agnostic bridge layer** that lets OpenClaw or another external agent attach to those features without coupling the whole app to one vendor.

## Recommended Design

### 1. Capability-First Instead of Provider-First
Do not start by building `openclaw_login`, `openclaw_tools`, `openclaw_projects`.

Start with:
- a shared capability catalog
- a provider manifest that selects from that catalog
- a bridge service that issues short-lived grants for selected capabilities

This keeps OpenClaw, future custom agents, and internal automation on the same contract surface.

### 2. Session Handoff, Not Cookie Sharing
The existing VibeVideo session cookie should stay inside VibeVideo.

Recommended flow:
1. user logs into VibeVideo normally
2. VibeVideo backend issues a short-lived bridge session for `provider_id`
3. provider exchanges that bridge session for its own local state
4. provider calls back into VibeVideo only through scoped bridge endpoints or existing APIs

Why:
- avoids leaking primary auth cookies to third parties
- makes revocation and audit easier
- lets you map owner/sub-user permissions cleanly

### 3. Auth Strategy Stack
Use auth in layers:

- **Primary**: `session_handoff`
  - best default for OpenClaw and future agent UIs
- **Fallback**: `email_code`
  - useful if the provider needs a user bootstrap path without full OAuth
- **Legacy-compatible helper**: `email-token-setup`
  - good browser-side adapter if the provider receives a VibeVideo-issued local token

### 4. Scope Model
Keep scopes coarse at first:
- `authenticated`
- `owner`
- `sub_user`
- `internal`

Then gate each capability separately.

Example initial OpenClaw rollout:
- allow `identity.current_user`
- allow `tools.generate_async`
- allow `tools.task_status.read`
- allow `tools.result.read`
- optionally allow `auth.email_code.*`
- do **not** allow `projects.write` initially

### 5. Bridge Components To Add Later
This package intentionally stops at the contract layer. The next implementation layer should likely add:

- `backend/routers/agent_bridge_router.py`
  - issue session grants
  - verify provider callback
  - log audit events
- `frontend/.../agents/[provider]`
  - launcher UI
  - callback handling
  - locale-aware redirects
- provider-specific config
  - allowed origins
  - callback URLs
  - enabled capabilities

## Suggested OpenClaw Rollout

### Phase 1
- redirect launcher
- bridge session issuance
- current-user read
- async tools read/write
- audit logs

### Phase 2
- email-code fallback
- access token overview
- provider-side task polling UI

### Phase 3
- project read APIs
- asset upload presign
- limited project writes after audit confidence

## Important Constraints From This Repo
- locale should still come from `x-request-locale`
- backend APIs should prefer `{error_code, data?, error_message?}`
- auth already mixes direct and standard response shapes, so the bridge should normalize them before exposing them externally
- sub-user permissions already exist, so the bridge should preserve owner/sub-user identity instead of flattening everything into a single user role


path: references/provider-integration-checklist.md
content: # Provider Integration Checklist

Use this checklist before binding OpenClaw or another third-party agent.

## 1. Pick The Smallest Useful Capability Set
Start with:
- `identity.current_user`
- `tools.generate_async`
- `tools.task_status.read`
- `tools.result.read`

Only add more after the first provider flow is stable.

## 2. Choose Transport
- `redirect`: best default for OpenClaw-style external agent UI
- `embedded`: only if CSP, cookies, and iframe restrictions are understood
- `popup`: okay for admin or operator tools
- `server_proxy`: use when the external provider should never touch browser auth state

For the current Studio bridge runtime:
- default site selection to `bollo.video`
- let users switch explicitly to `vibevideo.io`
- store the issued access token in a local OpenClaw session file instead of relying on browser cookies

## 3. Choose Auth Strategy
- prefer `session_handoff`
- use `email_code` as fallback
- keep `internal_service` for backend-only automation

## 4. Preserve Existing Repo Contracts
- send locale via `x-request-locale`
- preserve `{error_code, data?, error_message?}` when exposing normalized APIs
- preserve owner / sub-user context

## 5. Security Review
- do not expose the primary auth cookie to the provider
- set a short bridge token TTL
- whitelist provider origins
- log session issuance and exchange events
- keep provider scopes explicit and revocable

## 6. OpenClaw-Specific Recommendation
For the first OpenClaw version:
- use `redirect`
- use backend-issued short-lived bridge sessions
- enable auth fallback through `email_code`
- expose tool generation only after successful identity lookup
- postpone project mutation and upload-write scopes

## 7. Rollout Gate
Do not widen capability scope until:
- audit events are visible
- token revocation is tested
- sub-user behavior is verified
- callback replay protection exists


path: references/current-system-map.md
content: # Current System Map

This file lists the current repo surfaces that are already useful for a third-party agent bridge.

## Auth
- `GET /api/auth/captcha/generate`
  - returns CAPTCHA image data for email-code login
- `GET /api/auth/login`
  - starts Google OAuth
- `POST /api/auth/callback`
  - exchanges Google code for local VibeVideo token
- `GET /api/auth/wechat/login-config`
  - gets WeChat QR login configuration
- `POST /api/auth/wechat/callback`
  - completes WeChat login
- `GET /api/auth/captcha/generate`
  - returns CAPTCHA challenge
- `POST /api/auth/captcha/verify`
  - verifies CAPTCHA and can send email-link login
- `POST /api/auth/email-code/send`
  - sends one-time email verification code
- `POST /api/auth/email-code/verify`
  - verifies code and issues local session
- `GET /api/auth/email-verify`
  - magic-link verification redirect
- `POST /api/auth/email-token-setup`
  - writes cookies from a local access token
- `PUT /api/access/tokens/{token_id}`
  - can expire the current saved session token during logout

## Identity
- `GET /api/users/me`
  - returns current user profile, permissions, credits, and plan state

## Access Control
- `GET /api/access/overview`
- `GET /api/access/sub-users`
- `POST /api/access/sub-users`
- `PUT /api/access/sub-users/{sub_user_id}`
- `DELETE /api/access/sub-users/{sub_user_id}`
- `GET /api/access/tokens`
- `PUT /api/access/tokens/{token_id}`

These routes are important because a provider bridge should inherit owner / sub-user semantics rather than building a second permission system.

## Studio Projects
- `GET /api/projects`
  - lists Studio projects for the current user

The OpenClaw runtime uses this endpoint to let the user pick a target project and to build direct project-page links.

## Studio Script Drafts
- `POST /api/scripts/create_async`
  - creates a single Studio episode draft from script text
- `GET /api/scripts/task_status?video_id=...&step=create_script`
  - polls until the Studio draft is ready to open

## Tools
- `POST /api/tools/generate_async`
  - image / video generation task creation
- `GET /api/tools/task_status`
  - polling entry
- `GET /api/tools/fetch_result`
  - fetches generated result payloads

## Service Flags
- `GET /api/service/get-sora2-not-stable`
- `POST /api/service/set-sora2-not-stable`

Useful as an example of small scoped capabilities behind auth.

## Recommended Missing Layer
Planned but not yet present:
- `POST /api/agent-bridge/providers/{provider_id}/session`
- `GET|POST /api/agent-bridge/providers/{provider_id}/callback`

That bridge layer should:
- mint short-lived handoff grants
- constrain provider capability set
- log audit trail
- normalize mixed auth response shapes for external agents


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