{
  "schemaVersion": 1,
  "version": "0.9.44",
  "generatedAt": "2026-09-22T14:28:26.210Z",
  "skills": [
    {
      "id": "doc-parse",
      "toolName": "doc_parse",
      "tier": "tool",
      "category": "consuming",
      "title": "Document Extraction",
      "summary": "Extract a document (.pptx) into structured data: per-page title and body text, the speaker notes, and — when asked for the layout layer — the exact shapes, coordinates, colors and animation beats of every slide. Runs server-side (ab-render /extract); nothing is installed locally and no model is involved, so the result is deterministic. Two layers via `fields`: `content` is lightweight text (title / text / rawNotes) and is what you want for narration or for summarizing a deck; `layout` additionally returns the slide payload that the ppt-to-video template renders one-to-one from the original file — it embeds every icon as a data URI and is large, so it is written to a file rather than printed.",
      "triggers": [
        "Turn this PPT / slide deck into a video",
        "Extract text or speaker notes from a .pptx",
        "Use a PowerPoint file as source material",
        "Get customPayload.slide for ppt-to-video"
      ],
      "entry": {
        "type": "python",
        "scriptPath": "scripts/extract_via_render.py"
      },
      "runtime": "python",
      "envVars": [
        "RENDER_API_URL",
        "PRIV_TOKEN",
        "CONVERSATION_ID"
      ],
      "parameters": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "Direct document URL (http/https). A page link or a share link will not work — pass the file URL itself (e.g. what the asset library returns)."
          },
          "kind": {
            "type": "string",
            "enum": [
              "pptx"
            ],
            "description": "Document kind. Only pptx today; a binary .ppt must be re-saved as .pptx first."
          },
          "fields": {
            "type": "string",
            "description": "Comma-separated layers: `content` (title/text/rawNotes — small) and/or `layout` (slide shapes + beats — large, embeds icons). Default `content,layout`. Ask for content only when you just need the words."
          },
          "slide": {
            "type": "number",
            "description": "Only extract this 1-based page. Omit for the whole deck."
          },
          "out": {
            "type": "string",
            "description": "Where to write the full JSON result (default ./<name>.extract.json). stdout gets a per-page summary instead."
          },
          "json_output": {
            "type": "boolean",
            "description": "Pipeline mode — emit the full JSON on stdout. Avoid with the layout layer: it is hundreds of KB of base64 per page."
          }
        },
        "required": [
          "url"
        ]
      },
      "ui": {
        "primary": [
          "url",
          "fields"
        ],
        "advanced": [
          "slide",
          "kind"
        ],
        "hidden": [
          "out",
          "json_output"
        ]
      }
    },
    {
      "id": "export-jianying",
      "toolName": "export_jianying",
      "tier": "orchestration",
      "category": "consuming",
      "title": "Jianying (CapCut) Draft Export",
      "summary": "Jianying (CapCut) draft export: package asset URLs into a draft ZIP that Jianying can import. Supports automatic conversion from a RenderPlan.",
      "triggers": [
        "Export to Jianying, Jianying draft, package for Jianying, import into Jianying",
        "Export materials to the Jianying format, generate a Jianying project",
        "Bundle video / image / audio into a Jianying draft",
        "Export a Jianying draft from a RenderPlan"
      ],
      "entry": {
        "type": "python",
        "scriptPath": "scripts/gen_jianying_draft.py"
      },
      "runtime": "python",
      "envVars": [
        "PRIV_TOKEN",
        "MM_API_BASE_URL",
        "AGENT_NAME"
      ],
      "parameters": {
        "type": "object",
        "properties": {
          "from_render_plan": {
            "type": "string",
            "description": "Build the draft from a RenderPlan JSON (replaces --scenes)"
          },
          "scenes": {
            "type": "string",
            "description": "Scene array as JSON (inline string or file path)"
          },
          "title": {
            "type": "string",
            "description": "Draft title"
          },
          "width": {
            "type": "number",
            "description": "Canvas width in pixels (default 1080)"
          },
          "height": {
            "type": "number",
            "description": "Canvas height in pixels (default 1920)"
          },
          "system": {
            "type": "string",
            "enum": [
              "mac",
              "windows"
            ],
            "description": "Draft-root preset"
          },
          "draft_root_path": {
            "type": "string",
            "description": "Explicit Jianying draft root path (overrides --system)"
          },
          "no_download": {
            "type": "boolean",
            "description": "Do not download the ZIP; print the URL only"
          },
          "output": {
            "type": "string",
            "description": "Local download path"
          }
        },
        "required": []
      },
      "ui": {
        "primary": [
          "title"
        ],
        "advanced": [
          "system",
          "width",
          "height",
          "output"
        ],
        "hidden": [
          "from_render_plan",
          "scenes",
          "draft_root_path",
          "no_download"
        ]
      }
    },
    {
      "id": "gen-digital-human",
      "toolName": "gen_digital_human",
      "tier": "atomic",
      "category": "asset",
      "title": "Digital-Human Talking-Head",
      "summary": "Digital-human video: list available avatars; produce a talking-head video from text via TTS, or drive an avatar from an existing audio URL.",
      "triggers": [
        "Digital human, talking-head video, AI presenter, virtual host",
        "Make a talking-head video out of this script / this audio file",
        "List or browse digital-human avatars",
        "Use Jimeng or HiFly to drive an avatar"
      ],
      "entry": {
        "type": "http",
        "handler": "gen-digital-human"
      },
      "runtime": "node",
      "envVars": [
        "PRIV_TOKEN",
        "MM_API_BASE_URL",
        "AGENT_NAME"
      ],
      "parameters": {
        "type": "object",
        "properties": {
          "list_avatars": {
            "type": "boolean",
            "description": "List available digital-human avatars"
          },
          "mine": {
            "type": "boolean",
            "description": "With list_avatars=true: list the caller's own custom avatars instead of the public catalog. Custom avatars do not appear without this."
          },
          "name": {
            "type": "string",
            "description": "With list_avatars=true: fuzzy-filter avatars by name"
          },
          "source": {
            "type": "string",
            "enum": [
              "jimeng",
              "hifly"
            ],
            "description": "Provider: jimeng is image-driven, hifly is video-driven. Usually inferred from the avatar; pass it explicitly when the avatar declares no source."
          },
          "gender": {
            "type": "string",
            "enum": [
              "male",
              "female"
            ],
            "description": "Filter by gender"
          },
          "avatar_id": {
            "type": "number",
            "description": "Avatar id"
          },
          "text": {
            "type": "string",
            "description": "Narration text (TTS mode)"
          },
          "audio_url": {
            "type": "string",
            "description": "Audio URL (audio-driven mode)"
          },
          "voice_id": {
            "type": "string",
            "description": "Voice id (TTS mode). Shares the Minimax catalog with gen-voice — call gen_voice with list_voices=true to see available ids rather than inventing one."
          },
          "voice_name": {
            "type": "string",
            "description": "Voice display name, recorded alongside voice_id for bookkeeping. Does not affect synthesis."
          },
          "aspect_ratio": {
            "type": "string",
            "description": "Aspect ratio: 9:16 / 16:9 / 3:4 / 1:1. Defaults to the avatar's own ratio."
          },
          "prompt": {
            "type": "string",
            "description": "Action prompt describing how the avatar should perform, e.g. 'more hand gestures'"
          },
          "check_status": {
            "type": "boolean",
            "description": "Status-check mode: poll an earlier job instead of starting a new one. Requires generation_id. Use this when a generate call timed out."
          },
          "generation_id": {
            "type": "number",
            "description": "Job id to poll (required when check_status=true)"
          },
          "json_output": {
            "type": "boolean",
            "description": "Emit a JSON result (generate: { url, generationId }; list: { avatars }; check-status: { status, url }) instead of human-readable output"
          }
        },
        "required": []
      },
      "ui": {
        "primary": [
          "avatar_id",
          "text"
        ],
        "advanced": [
          "source",
          "voice_id",
          "aspect_ratio",
          "prompt",
          "audio_url"
        ],
        "hidden": [
          "json_output",
          "list_avatars",
          "mine",
          "name",
          "gender",
          "check_status",
          "generation_id",
          "voice_name"
        ]
      }
    },
    {
      "id": "gen-image",
      "toolName": "gen_image",
      "tier": "atomic",
      "category": "asset",
      "title": "AI Image Generation",
      "summary": "AI image generation: produce an image from a text prompt. Supports the Seedream 5.0 family (Lite and a high-fidelity 'pro' variant), plus image-to-image with reference images.",
      "triggers": [
        "AI image generation, text-to-image, \"draw me ...\", \"generate an image of ...\"",
        "Image-to-image, reference image, style transfer, image variation",
        "Generate an image with Doubao / Seedream",
        "Provide a prompt and ask for an image"
      ],
      "entry": {
        "type": "http",
        "handler": "gen-image"
      },
      "runtime": "node",
      "envVars": [
        "PRIV_TOKEN",
        "MM_API_BASE_URL",
        "AGENT_NAME",
        "MM_IMAGE_MODEL"
      ],
      "parameters": {
        "type": "object",
        "properties": {
          "prompt": {
            "type": "string",
            "description": "Image description (required)"
          },
          "model": {
            "type": "string",
            "description": "Model: 'seedream' (default, Seedream 5.0 Lite) or 'seedream-pro' (high fidelity, precise placement and on-image text; costs more per image)"
          },
          "size": {
            "type": "string",
            "description": "Aspect ratio or WxH, e.g. 1:1, 9:16"
          },
          "resolution": {
            "type": "string",
            "enum": [
              "1K",
              "2K",
              "4K"
            ],
            "description": "Output resolution tier. Currently inert: the only backend model that read it (Gemini) is not wired up, so this is ignored — use `size` for image dimensions."
          },
          "n": {
            "type": "number",
            "description": "Number of images, 1-4"
          },
          "reference": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Reference images for image-to-image: local file path, https URL, or data URI. Pass multiple to blend several references (seedream: max 14, seedream-pro: max 10 — over the limit fails before spending credits)."
          },
          "image_strength": {
            "type": "number",
            "description": "How strongly the reference images influence the result, 0-1 (Seedream family only). Omit to use the backend default."
          },
          "guidance_scale": {
            "type": "number",
            "description": "Prompt-adherence strength, where supported. Omit to use the backend default."
          },
          "negative_prompt": {
            "type": "string",
            "description": "Negative prompt — content to avoid"
          },
          "seed": {
            "type": "number",
            "description": "Random seed. Pass the same seed with the same prompt and model to make a run reproducible."
          },
          "watermark": {
            "type": "boolean",
            "description": "Add a watermark to the output. Only true has an effect; there is no opt-out override of the backend default."
          },
          "json_output": {
            "type": "boolean",
            "description": "Emit a JSON result ({ urls: [...] }) instead of human-readable output"
          }
        },
        "required": [
          "prompt"
        ]
      },
      "ui": {
        "primary": [
          "prompt",
          "reference",
          "model",
          "size",
          "n"
        ],
        "advanced": [
          "negative_prompt"
        ],
        "hidden": [
          "json_output",
          "watermark",
          "resolution",
          "seed",
          "guidance_scale",
          "image_strength"
        ]
      }
    },
    {
      "id": "gen-script",
      "toolName": "gen_script",
      "tier": "orchestration",
      "category": "authoring",
      "title": "Video Script Generation",
      "summary": "Video script generation: turn a topic into a structured Video DSL (JSON) that describes the full video — scene structure, asset requirements, and narrative flow.",
      "triggers": [
        "Write a video script, generate a video script, plan a video, write storyboards",
        "Create a short video, plan video content, generate a Video DSL",
        "Turn a topic into a video structure / video content plan"
      ],
      "entry": {
        "type": "python",
        "scriptPath": "scripts/gen_script.py"
      },
      "runtime": "python",
      "envVars": [
        "DEFAULT_IMAGE_MODEL",
        "DEFAULT_VIDEO_MODEL",
        "STUB_IMAGE_URL",
        "STUB_VIDEO_URL"
      ],
      "parameters": {
        "type": "object",
        "properties": {
          "topic": {
            "type": "string",
            "description": "Video topic (required)"
          },
          "duration": {
            "type": "number",
            "description": "Target duration in seconds"
          },
          "style": {
            "type": "string",
            "description": "Style tag"
          },
          "ratio": {
            "type": "string",
            "description": "Aspect ratio, e.g. 16:9 or 9:16"
          },
          "scenes": {
            "type": "number",
            "description": "Scene count"
          },
          "voice_id": {
            "type": "string",
            "description": "Narration voice id. Default depends on the bound template's outputLanguage; query gen_voice with list_voices=true to see available ids."
          },
          "speed": {
            "type": "number",
            "description": "Narration speech rate, 0.5-2.0 (default 1.0). Stored at global.narration.speed and applied when prepare_video_assets synthesizes the voice-over. **Must** pass when the user picked a speech rate — a template's 语速 / speed parameter arrives as a multiplier like 1.1, and without this flag it is silently dropped and the narration comes back at 1.0. Above ~1.3 subtitle alignment drifts and the delivery sounds mechanical; suggest shortening the script instead of raising it further."
          },
          "template_id": {
            "type": "string",
            "description": "Template id (e.g. html-slide). The template owns outputLanguage and may also declare a defaultVoiceId; both flow into the produced DSL."
          },
          "headline": {
            "type": "string",
            "description": "On-screen headline. Stored at meta.headline and pushed into every scene's textLayers[role=headline] so the template can render it as the top big-text. **Must** be set when the user explicitly provided a headline / main title; without it, headline falls back to the long-form topic and overflows the top text layer. This is on-screen copy, so write it for a viewer who has never heard of the subject — a bare identifier (repo name, file name, product code) is not a title. Length limits, line breaks (\\n) and emphasis syntax are per-template: follow the chosen template's llmHint, which gen_script prints in full on stderr."
          },
          "subheadline": {
            "type": "string",
            "description": "On-screen subheadline: the line under the headline, saying why the viewer should care. Multi-line via \\n. Stored at meta.subheadline and pushed into every scene's textLayers[role=subheadline] as the top small-text. **Must** be set when the user explicitly provided a subtitle / slogan. **Never a bare URL** — text layers are painted on screen, where a link is neither readable nor clickable; say what the thing is instead. How many lines the template keeps, and whether it parses **emphasis**, is declared in that template's llmHint (gen_script prints it in full on stderr). Note: this is the on-screen subheadline, not the CC subtitle (global.subtitle) — they are independent."
          },
          "carousel_items": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Media URLs for the template's image/video carousel. When provided together with a template_id whose capabilities.payloadStyle=carousel-caption, these URLs are placed into customPayload.sequence.items when payloadDefaults.sequence is declared, otherwise customPayload.carousel.items, as existing assets — NO AI image generation is triggered. **Must** pass when the user explicitly provides image/video URLs for such a template. Each element is a full URL string."
          },
          "scene_images": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Per-scene visuals for a visual-overlay template (image-slide etc.) — the counterpart of carousel_items for templates that have real scenes. Each element is EITHER a full URL (use that existing asset for the scene, no gen-image call) OR the literal string 'ai' / 'ai:<prompt>' (leave that scene to gen-image; the text after the colon, when present, is the user's own image prompt for it). Mapping is POSITIONAL: element i is scene i, so an AI scene can sit anywhere in the order — pass ['url1','ai','url2'] verbatim when the user arranged it that way. Scenes past the last element still get an AI-generated image, so 2 images still produce a full video. When `scenes` is not set the scene count is raised (never lowered) to fit the elements, so nothing the user arranged is dropped and handing over fewer images does not shrink the video. **Must** pass when the user supplies images for such a template — otherwise their images are ignored entirely and every scene is AI-generated. Do NOT pass for carousel-caption templates; those take carousel_items."
          },
          "caption_lines": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "On-screen caption lines for templates that declare a caption area (capabilities.payloadDefaults.caption). For image sequences (payloadDefaults.sequence), lines map in order to sequence.items[].caption, with at most one line per image. Otherwise each element is one line. Line-count limits and whether **emphasis** is parsed are declared by the template — read its llmHint, which gen_script prints in full on stderr. **Must** pass when the user explicitly provides bullet-point text / bottom copy. **Also must pass — written by you — when the user did NOT provide any copy but the template is typewriter-driven** (capabilities.durationStrategy=fit-caption): such templates have no narration, so these lines are both the video's content and the thing that decides its duration. Draft them from the material you researched (repo README, page screenshots, the topic). Leaving this empty for a fit-caption template is rejected: nothing auto-generates caption text, and an empty caption renders a titled carousel with no words at all."
          },
          "stub_image_url": {
            "type": "string",
            "description": "Test-mode image stub URL. Only pass when the user explicitly says things like 'just testing / don't actually generate / use a placeholder image / stub URL / save credits' AND provides a concrete URL. With this set, every image AssetRef in the produced DSL is written as source=existing, status=generated, url=<this URL> — no gen-image call. Do not pass otherwise; if the user expressed the intent without a URL, ask for one — do not invent one."
          },
          "stub_video_url": {
            "type": "string",
            "description": "Test-mode video stub URL. Only pass when the user explicitly says things like 'just testing / don't actually generate the video / placeholder clip / save credits' AND provides a concrete URL. With this set, every video AssetRef in the produced DSL is written as source=existing, status=generated, url=<this URL> — no gen-video call. Do not pass otherwise; if the user expressed the intent without a URL, ask for one — do not invent one."
          },
          "skip_asset_generation": {
            "type": "boolean",
            "description": "All-in-one switch for downstream agents (e.g. template-creator) that only want the DSL shape: every produced AssetRef is marked as already generated with placeholder URLs (image: https://placeholder.local/stub.png, video: stub.mp4, audio: stub.mp3). Implies the equivalent of --stub-image-url + --stub-video-url with sentinel defaults plus the same rewrite for gen-voice / gen-digital-human assets. Useful when the agent only needs to inspect DSL structure or feed it into try_render_local with all assets pre-stubbed."
          },
          "item_count": {
            "type": "integer",
            "minimum": 1,
            "description": "Number of content beats, for templates declaring capabilities.sceneStrategy=countdown — the '一个开场 + N 个同类节拍，倒数编号' shape (Top-N lists, '5 个技巧', '7 个常见错误'). Produces one opening plus N scenes counting down from #N to #1. The accepted range and the default are declared by the chosen template (template_registry prints capabilities.countdown and the count field's bounds); a value outside it fails with the template's own numbers in the message. **Pass the count the user asked for, not the total scene count** — `scenes` is the total and the two are cross-checked, so passing the user's 5 here yields 5 beats while passing it as `scenes` would yield 4. The skeleton only carries each beat's index: fill in the real per-scene content before preparing assets. Keep any count stated in headline/subheadline consistent with this value — gen_script warns on a mismatch but never rewrites your copy."
          }
        },
        "required": [
          "topic"
        ]
      },
      "ui": {
        "primary": [
          "topic"
        ],
        "advanced": [
          "item_count"
        ],
        "hidden": [
          "duration",
          "style",
          "ratio",
          "scenes",
          "voice_id",
          "speed",
          "template_id",
          "headline",
          "subheadline",
          "carousel_items",
          "scene_images",
          "caption_lines",
          "stub_image_url",
          "stub_video_url",
          "skip_asset_generation"
        ]
      }
    },
    {
      "id": "gen-video",
      "toolName": "gen_video",
      "tier": "atomic",
      "category": "asset",
      "title": "AI Video Generation",
      "summary": "AI video generation: produce a short video clip from a text prompt. Three Seedance 2.0 tiers (mini / fast / standard), plus first/last frame and reference images.",
      "triggers": [
        "Text-to-video, AI-generated clip, \"make a short video of ...\"",
        "Generate video with Doubao / Seedance",
        "Image-to-video, first-frame / last-frame, reference-image-to-video",
        "Reference video / reference audio, \"same camera move as this clip\", \"use this track\""
      ],
      "entry": {
        "type": "http",
        "handler": "gen-video"
      },
      "runtime": "node",
      "envVars": [
        "PRIV_TOKEN",
        "MM_API_BASE_URL",
        "AGENT_NAME",
        "MM_VIDEO_MODEL"
      ],
      "parameters": {
        "type": "object",
        "properties": {
          "prompt": {
            "type": "string",
            "description": "Video description"
          },
          "model": {
            "type": "string",
            "description": "Model: 'seedance-mini' (default; cheapest, 480p/720p), 'seedance-fast' (480p/720p), or 'seedance' (Seedance 2.0, adds 1080p). All are 4-15s and share the same features"
          },
          "duration": {
            "type": "number",
            "description": "Duration in seconds"
          },
          "ratio": {
            "type": "string",
            "description": "Aspect ratio"
          },
          "resolution": {
            "type": "string",
            "description": "Resolution"
          },
          "first_frame": {
            "type": "string",
            "description": "First-frame image path or URL"
          },
          "last_frame": {
            "type": "string",
            "description": "Last-frame image path or URL"
          },
          "reference": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Reference images: local file path, https URL, or data URI. Up to 9; combinable with first_frame / last_frame."
          },
          "reference_video": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Reference videos (https URL only, must be publicly reachable). Up to 3. The model borrows their subject, camera work and style. Cannot be combined with first_frame / last_frame."
          },
          "reference_audio": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Reference audio (https URL only, must be publicly reachable). Up to 3. Borrows timbre, melody or dialogue. Needs at least one reference image or video alongside it; cannot be combined with first_frame / last_frame."
          },
          "web_search": {
            "type": "boolean",
            "description": "Let the model search the web for up-to-date subjects before generating. Text-only input — ignored once any image / video / audio is attached."
          },
          "return_last_frame": {
            "type": "boolean",
            "description": "Also return the generated clip's last frame, to chain it into the next shot"
          },
          "generate_audio": {
            "type": "boolean",
            "description": "Generate native audio"
          },
          "camera_fixed": {
            "type": "boolean",
            "description": "Lock the camera in place (Seedance)"
          },
          "negative_prompt": {
            "type": "string",
            "description": "Content to steer away from. Veo only — Seedance ignores it (kept for when a Veo channel is available again)"
          },
          "seed": {
            "type": "number",
            "description": "Random seed, -1 for random. Pass the same seed with the same prompt and model to make a run reproducible."
          },
          "person_generation": {
            "type": "string",
            "enum": [
              "allow_all",
              "dont_allow"
            ],
            "description": "Whether the model may render people (Veo). Omit to use the backend default."
          },
          "json_output": {
            "type": "boolean",
            "description": "Emit a JSON result ({ url }) instead of human-readable output"
          }
        },
        "required": [
          "prompt"
        ]
      },
      "ui": {
        "primary": [
          "prompt",
          "model",
          "duration",
          "ratio",
          "resolution"
        ],
        "advanced": [
          "first_frame",
          "last_frame",
          "reference",
          "reference_video",
          "reference_audio",
          "generate_audio",
          "camera_fixed",
          "web_search",
          "return_last_frame",
          "seed"
        ],
        "hidden": [
          "json_output",
          "negative_prompt",
          "person_generation"
        ]
      }
    },
    {
      "id": "gen-voice",
      "toolName": "gen_voice",
      "tier": "atomic",
      "category": "asset",
      "title": "Text-to-Speech (Minimax)",
      "summary": "Text-to-speech (TTS): synthesize narration audio from text via the Minimax TTS model. Returns the persisted audio URL — no download needed.",
      "triggers": [
        "AI voice-over, TTS, text-to-speech, generate narration audio",
        "Use Minimax for speech synthesis",
        "Read a piece of text aloud / produce an audio file from text"
      ],
      "entry": {
        "type": "http",
        "handler": "gen-voice"
      },
      "runtime": "node",
      "envVars": [
        "PRIV_TOKEN",
        "MM_API_BASE_URL",
        "AGENT_NAME"
      ],
      "parameters": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string",
            "description": "Text to synthesize (required)"
          },
          "voice_id": {
            "type": "string",
            "description": "Voice id. Default 'Chinese (Mandarin)_Male_Announcer'. When unsure, call with list_voices=true first to see what's available — do not invent ids, and do not declare a voice unavailable unless it is missing from that list."
          },
          "speed": {
            "type": "number",
            "description": "Speech rate, 0.5-2.0"
          },
          "list_voices": {
            "type": "boolean",
            "description": "List the available voices and exit. This queries the live voice service and is the authoritative catalog — one call is enough, and its absence from this list is the only evidence that a voice id is invalid."
          },
          "json_output": {
            "type": "boolean",
            "description": "Emit a JSON result (url, audio_length_ms, subtitles)"
          }
        },
        "required": []
      },
      "ui": {
        "primary": [
          "text",
          "voice_id"
        ],
        "advanced": [
          "speed"
        ],
        "hidden": [
          "json_output",
          "list_voices"
        ]
      }
    },
    {
      "id": "prepare-video-assets",
      "toolName": "prepare_video_assets",
      "tier": "orchestration",
      "category": "authoring",
      "title": "Video Asset Preparation",
      "summary": "Resolves and generates every asset (image / audio / video) referenced by a Video DSL, persists a RenderPlan to the database, and returns a job_id for the subsequent render_video call. This is Phase 1 of the two-phase video pipeline; Phase 3 (Remotion render) lives in render_video.",
      "triggers": [
        "Generate / prepare video assets",
        "Resolve missing assets in a DSL",
        "User confirmed the script and the agent needs to prepare assets for review",
        "User asks to regenerate a specific scene's image or narration"
      ],
      "entry": {
        "type": "python",
        "scriptPath": "scripts/prepare_video_assets.py"
      },
      "runtime": "python",
      "envVars": [
        "PRIV_TOKEN",
        "MM_API_BASE_URL",
        "MM_BACKEND_API_URL",
        "AGENT_NAME",
        "REMOTION_RENDER_API_URL",
        "REMOTION_RENDER_MODE",
        "REMOTION_OUTPUT_DIR",
        "ASSET_CACHE_DIR"
      ],
      "parameters": {
        "type": "object",
        "properties": {
          "dsl_json": {
            "type": "string",
            "description": "DSL JSON as an inline string (preferred — no disk write needed; required for multi-user concurrent flows). Pass the full DSL JSON, or — when a gen_script skeleton is cached in the session — a minimal JSON with only scenes[].audio.narration.text overrides (the agent layer merges narration onto the cached skeleton)."
          },
          "dsl": {
            "type": "string",
            "description": "DSL file path (legacy fallback; prefer dsl_json inline)."
          },
          "template_id": {
            "type": "string",
            "description": "Template id. When provided, the script invokes template-registry internally; no separate binding step is needed."
          },
          "binding_json": {
            "type": "string",
            "description": "TemplateBinding JSON as an inline string (alternative to template_id; no file needed)."
          },
          "binding": {
            "type": "string",
            "description": "TemplateBinding file path (optional; auto-generated when template_id is provided)."
          },
          "save_job": {
            "type": "boolean",
            "description": "Persist the RenderPlan to the database (default true). On success the stdout contains '📦 render job jobId: N'. Auto-degrades to false when PRIV_TOKEN is missing, falling back to file mode. Pass false to opt out explicitly."
          },
          "stub_image_url": {
            "type": "string",
            "description": "Test-mode image stub URL (renderer-side short-circuit). Only pass when the user explicitly says things like 'just testing / don't actually generate / use a placeholder image / save credits' AND provides a concrete URL. With this set, every image+source=gen-image AssetRef is short-circuited to that URL with no gen-image call. Do not pass otherwise; if the user expressed the intent without a URL, ask for one — do not invent one."
          },
          "stub_video_url": {
            "type": "string",
            "description": "Test-mode video stub URL (renderer-side short-circuit). Only pass when the user explicitly says things like 'just testing / don't actually generate the video / use a placeholder clip / save credits' AND provides a concrete URL. With this set, every video+source=gen-video AssetRef is short-circuited to that URL with no gen-video call. Do not pass otherwise; if the user expressed the intent without a URL, ask for one — do not invent one."
          }
        },
        "required": []
      },
      "ui": {
        "primary": [],
        "advanced": [
          "dsl_json",
          "dsl",
          "template_id",
          "binding_json",
          "binding",
          "save_job",
          "stub_image_url",
          "stub_video_url"
        ],
        "hidden": []
      }
    },
    {
      "id": "render-video",
      "toolName": "render_video",
      "tier": "orchestration",
      "category": "authoring",
      "title": "Remotion Video Renderer",
      "summary": "Loads a persisted RenderPlan by job_id and drives the Remotion engine to produce the final video. Assets must already be generated via prepare_video_assets — this skill never resolves or regenerates assets.",
      "triggers": [
        "Render the video, composite the video, export the video",
        "Turn the prepared assets into the final clip",
        "Render with Remotion",
        "Agent has a job_id from prepare_video_assets and needs the final video"
      ],
      "entry": {
        "type": "python",
        "scriptPath": "scripts/render_video.py"
      },
      "runtime": "python",
      "envVars": [
        "PRIV_TOKEN",
        "MM_API_BASE_URL",
        "MM_BACKEND_API_URL",
        "AGENT_NAME",
        "REMOTION_RENDER_API_URL",
        "REMOTION_RENDER_MODE",
        "REMOTION_OUTPUT_DIR",
        "ASSET_CACHE_DIR"
      ],
      "parameters": {
        "type": "object",
        "properties": {
          "job_id": {
            "type": "number",
            "minimum": 1,
            "description": "Render job id (positive integer, e.g. 8). This is the integer N parsed from prepare_video_assets's stdout line '📦 render job jobId: N' (also tolerated: legacy zh '📦 渲染任务 jobId: N'). Never pass 0, a placeholder string, or descriptive text."
          },
          "save_job": {
            "type": "boolean",
            "description": "Persist the render Manifest back to the database under the same jobId (default true). Auto-degrades to false when PRIV_TOKEN is missing. Pass false to opt out explicitly."
          },
          "upload_title": {
            "type": "string",
            "description": "Upload title for the resulting video file. Defaults to the local output filename."
          },
          "no_upload": {
            "type": "boolean",
            "description": "Skip the upload step. Only effective for local rendering; remote rendering uploads server-side."
          },
          "renderer": {
            "type": "string",
            "enum": [
              "local",
              "remote"
            ],
            "description": "Render mode: local runs the Remotion CLI on this host; remote calls the standalone remotion-renderer service. Defaults to REMOTION_RENDER_MODE."
          },
          "render_plan": {
            "type": "string",
            "description": "Existing RenderPlan file path. Local / single-user fallback used only when job_id is unavailable; prefer job_id."
          }
        },
        "required": [
          "job_id"
        ]
      },
      "ui": {
        "primary": [
          "job_id"
        ],
        "advanced": [
          "save_job",
          "upload_title",
          "no_upload",
          "renderer",
          "render_plan"
        ],
        "hidden": []
      }
    },
    {
      "id": "template-registry",
      "toolName": "template_registry",
      "tier": "orchestration",
      "category": "authoring",
      "title": "Video Template Registry",
      "summary": "List all available video templates (templateId / name / aspect ratio / style tags), or read one template's full definition with template_id. Listing returns summaries; pass template_id to get that template's authoring contract (llmHint / customPayloadSchema / slotMapping / variants). Template-to-DSL binding is no longer exposed as a separate step — once prepare_video_assets receives a template_id it runs the full DSL→RenderPlan pipeline internally.",
      "triggers": [
        "View available templates / list every template"
      ],
      "entry": {
        "type": "python",
        "scriptPath": "scripts/list_templates.py"
      },
      "runtime": "python",
      "envVars": [
        "PRIV_TOKEN",
        "VIDEO_TEMPLATE_REGISTRY_URL",
        "MM_API_BASE_URL",
        "VIDEO_TEMPLATE_REGISTRY_HTTP_METHOD"
      ],
      "parameters": {
        "type": "object",
        "properties": {
          "list_templates": {
            "type": "boolean",
            "description": "List available templates as summaries — templateId / name / description / aspect ratios / language / status / styleTags / variantIds / key capabilities / a truncated llmHint (default behavior; also implied when other flags are passed)."
          },
          "template_id": {
            "type": "string",
            "description": "Return this template's FULL definition instead of the summary list: llmHint in full, customPayloadSchema (every template-specific field incl. the legal slideId values), slotMapping, compositions, variants. This is the per-template authoring contract — read it before writing DSL for that template."
          },
          "full": {
            "type": "boolean",
            "description": "List mode only: emit full definitions for every listed template instead of summaries. Rejected when the result would be too large for one tool result — prefer template_id, or narrow with the filter_* parameters."
          },
          "list_examples": {
            "type": "boolean",
            "description": "List the *.dsl.json / *.binding.json reference examples under template-registry/video_dsl/schema/examples/, grouped by templateId. That directory is optional and is not part of the published package, so this is normally empty — to read a template's reference shape, use template_id instead (the registry's own contract)."
          },
          "filter_tag": {
            "type": "string",
            "description": "Show only templates whose styleTags contain (or are contained in) this string. Case-insensitive. E.g. 'tech' / '科普' / 'walkthrough'."
          },
          "filter_aspect": {
            "type": "string",
            "description": "Show only templates that declare this aspect ratio. E.g. '9:16' / '16:9' / '1:1'."
          },
          "filter_language": {
            "type": "string",
            "description": "Show only templates whose contentLanguage includes this code ('zh' or 'en'). Templates with no declared language are always shown (treated as language-agnostic)."
          },
          "include_beta": {
            "type": "boolean",
            "description": "Also list templates with status='beta'. Default off (only 'stable' shows). The ENABLE_BETA_TEMPLATES env var has the same effect process-wide."
          },
          "json_output": {
            "type": "boolean",
            "description": "Emit a JSON result instead of the human-readable table: { templates: [...] } — summaries, or full definitions when template_id / full is set — or { examples: [...] }."
          }
        },
        "required": []
      },
      "ui": {
        "primary": [],
        "advanced": [
          "list_templates",
          "template_id",
          "full",
          "list_examples",
          "filter_tag",
          "filter_aspect",
          "filter_language",
          "include_beta",
          "json_output"
        ],
        "hidden": []
      }
    },
    {
      "id": "video-parser",
      "toolName": "video_parser",
      "tier": "tool",
      "category": "consuming",
      "title": "Video Deconstruction",
      "summary": "Video deconstruction: split a video into reusable assets (audio extraction, ASR transcript, keyframe extraction, scene segmentation).",
      "triggers": [
        "Deconstruct / split / analyze / parse a video",
        "Extract keyframes, extract the script from a video, transcribe a video",
        "Pull content assets or raw material out of a video"
      ],
      "entry": {
        "type": "python",
        "scriptPath": "scripts/parse_via_render.py"
      },
      "runtime": "python",
      "envVars": [
        "RENDER_API_URL",
        "PRIV_TOKEN",
        "CONVERSATION_ID"
      ],
      "parameters": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "Direct remote video URL (required). Share/page links are not supported."
          },
          "scene_threshold": {
            "type": "number",
            "description": "Scene-cut detection threshold 0.0-1.0 (default 0.3)"
          },
          "skip_asr": {
            "type": "boolean",
            "description": "Skip the ASR step"
          },
          "skip_keyframes": {
            "type": "boolean",
            "description": "Skip the keyframe-extraction step"
          },
          "json_output": {
            "type": "boolean",
            "description": "Pipeline mode — emit JSON only on stdout"
          }
        },
        "required": [
          "url"
        ]
      },
      "ui": {
        "primary": [
          "url"
        ],
        "advanced": [
          "scene_threshold",
          "skip_asr",
          "skip_keyframes"
        ],
        "hidden": [
          "json_output"
        ]
      }
    },
    {
      "id": "web-read",
      "toolName": "web_read",
      "tier": "tool",
      "category": "consuming",
      "title": "Web Page Reader",
      "summary": "Open any URL in a headless browser (Playwright Python) and return the page's MAIN TEXT — title, headings, paragraphs, lists, code blocks and tables — as Markdown, plain text, or structured JSON. Boilerplate (nav / sidebar / comments / ads / footer) is stripped by a Readability-style pass, and JS-rendered pages work because a real browser runs the page. This is the tool to use whenever you need to KNOW WHAT A PAGE SAYS: summarizing an article, pulling source material for a script, reading a README or docs page, checking what a link contains. It returns text, not pictures — for a screenshot (png/jpg) use web_screenshot, for a recording (mp4/webm) use web_record. GitHub repository pages also return observed Stars/Forks/Watchers before sidebar cleanup (missing counters stay unknown). Reuse successful reads across script and template fields; do not repeat the same URL just for counters already returned. Output is capped by max_chars (default 20000) and truncated on a block boundary; pass an `output` path to keep the full text on disk.",
      "triggers": [
        "Read this link / what does this page say / summarize this article",
        "Fetch page content, extract article text, get the text of a URL",
        "Use a web page as source material for a script or video",
        "Read a README / docs page / changelog / blog post",
        "Pull code samples or tables out of a page"
      ],
      "entry": {
        "type": "python",
        "scriptPath": "../web-screenshot/scripts/read_page.py"
      },
      "runtime": "python",
      "envVars": [
        "WEB_CAPTURE_BROWSER",
        "WEB_CAPTURE_ALLOW_PRIVATE_HOSTS",
        "PLAYWRIGHT_BROWSERS_PATH"
      ],
      "parameters": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "Target page URL (http/https). Private / loopback / link-local addresses are refused unless WEB_CAPTURE_ALLOW_PRIVATE_HOSTS=1."
          },
          "format": {
            "type": "string",
            "enum": [
              "markdown",
              "text",
              "json"
            ],
            "description": "markdown (default: keeps headings, lists, code fences, tables) | text (plain) | json (structured blocks + metadata, not truncated)"
          },
          "max_chars": {
            "type": "number",
            "description": "Cap on the printed text, cut at a block boundary with an explicit [truncated] notice (default 20000, 0 = unlimited). Raise it when you need the whole document; a very long page will otherwise fill your context."
          },
          "selector": {
            "type": "string",
            "description": "Read only inside this CSS selector, preserving the selected region's sidebar/button text instead of pruning boilerplate. Scripts/styles are still removed. Leave empty to auto-detect the article container; use a targeted selector only for a concrete missing required fact."
          },
          "include_links": {
            "type": "boolean",
            "description": "Keep hyperlinks as [text](url) instead of plain text. Useful when you need to follow links from the page."
          },
          "include_images": {
            "type": "boolean",
            "description": "Keep images as ![alt](src). Useful for harvesting illustration URLs out of an article."
          },
          "output": {
            "type": "string",
            "description": "Also write the FULL (untruncated) text to this local path. stdout still respects max_chars — use this when a long page must be kept for later steps."
          },
          "settle_ms": {
            "type": "number",
            "description": "Extra wait before extracting, in ms. Raise for pages that render content late."
          },
          "wait_for_selector": {
            "type": "string",
            "description": "Wait for this CSS selector before extracting (the reliable fix for JS-rendered content)"
          },
          "wait_for_timeout": {
            "type": "number",
            "description": "Fixed wait before extracting, in ms"
          },
          "device": {
            "type": "string",
            "description": "Device emulation name, e.g. 'iPhone 15 Pro' — some sites serve a leaner page to mobile"
          },
          "viewport": {
            "type": "string",
            "description": "Viewport as 'width,height', e.g. '1280,800'"
          },
          "color_scheme": {
            "type": "string",
            "enum": [
              "light",
              "dark",
              "no-preference"
            ],
            "description": "Emulate prefers-color-scheme"
          },
          "user_agent": {
            "type": "string",
            "description": "Override the User-Agent (try this when a site blocks headless browsers)"
          },
          "timeout": {
            "type": "number",
            "description": "Global Playwright action timeout in ms"
          },
          "ignore_https_errors": {
            "type": "boolean",
            "description": "Ignore HTTPS certificate errors"
          },
          "storage_state": {
            "type": "string",
            "description": "Path to a Playwright storageState JSON file (logged-in session)"
          },
          "cookies": {
            "type": "string",
            "description": "Playwright cookies as a JSON string or a path to a JSON file (top level is an array)"
          },
          "browser": {
            "type": "string",
            "enum": [
              "chromium",
              "firefox",
              "webkit"
            ],
            "description": "Browser engine (default chromium)"
          },
          "quiet": {
            "type": "boolean",
            "description": "Suppress the extraction diagnostics on stderr"
          }
        },
        "required": [
          "url"
        ]
      },
      "ui": {
        "primary": [
          "url",
          "format",
          "max_chars"
        ],
        "advanced": [
          "selector",
          "include_links",
          "include_images",
          "wait_for_selector",
          "settle_ms",
          "device",
          "viewport",
          "color_scheme",
          "timeout"
        ],
        "hidden": [
          "output",
          "quiet",
          "user_agent",
          "ignore_https_errors",
          "storage_state",
          "cookies",
          "browser",
          "wait_for_timeout"
        ]
      }
    },
    {
      "id": "web-record",
      "toolName": "web_record",
      "tier": "tool",
      "category": "consuming",
      "title": "Web Page Recording",
      "summary": "Drive a headless browser (Playwright Python) to RECORD any URL to a video, then (by default) transcode to mp4, grab a cover frame, upload to VOD and return a playable CDN URL. Modes: fixed-duration recording, condition-triggered stop (element appears / disappears), auto-scroll from top to bottom, custom storyboards, and parameterized templates. Storyboard scenes: highlight / focus / zoom / scroll / virtual-cursor click / type / hover / caption / title-card / arrow / numbered sequence / redact / code-line highlight. For still images (png/jpg) use the web_screenshot tool instead. Use this tool whenever the user wants a video / recording / screencast of a web page: record a page, scroll-through video, page-from-top-to-bottom clip, demo of clicks/typing/hover, storyboard / multi-scene intro video, or a template-based clip.",
      "triggers": [
        "Screen recording, record a web page, record a video, capture an interaction, produce a webm, screencast",
        "Scroll recording, a video that pans the page from top to bottom",
        "Multi-scene video, storyboard video, timeline-sequenced clips",
        "Animated demos of clicks / typing / hover (virtual cursor + ripple / typewriter / triggering tooltips)",
        "One-command template clips, numbering and highlighting several regions in sequence and recording it"
      ],
      "entry": {
        "type": "python",
        "scriptPath": "../web-screenshot/scripts/record.py"
      },
      "runtime": "python",
      "envVars": [
        "WEB_CAPTURE_BROWSER",
        "WEB_CAPTURE_OUTPUT_BASE",
        "WEB_CAPTURE_NO_SANDBOX",
        "WEB_CAPTURE_ALLOW_PRIVATE_HOSTS",
        "PLAYWRIGHT_BROWSERS_PATH",
        "REMOTION_RENDER_API_URL",
        "PRIV_TOKEN"
      ],
      "parameters": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "Target page URL (http/https). May be omitted only when a template or storyboard JSON supplies its own url."
          },
          "output": {
            "type": "string",
            "description": "Local output path, must end in .webm (default recording.webm)"
          },
          "browser": {
            "type": "string",
            "description": "chromium | firefox | webkit (default chromium; chromium recommended for recording)"
          },
          "device": {
            "type": "string",
            "description": "Device emulation name, e.g. 'iPhone 15 Pro'"
          },
          "viewport": {
            "type": "string",
            "description": "Viewport as 'width,height', e.g. '1280,720'"
          },
          "duration": {
            "type": "number",
            "description": "Fixed recording length in ms"
          },
          "stop_when_selector": {
            "type": "string",
            "description": "Stop recording once this CSS selector appears"
          },
          "stop_when_hidden": {
            "type": "string",
            "description": "Stop recording once this CSS selector disappears"
          },
          "max_duration": {
            "type": "number",
            "description": "Safety cap (ms) for condition-based stop modes (default 60000)"
          },
          "scroll_through": {
            "type": "boolean",
            "description": "Auto-scroll smoothly from top to bottom while recording"
          },
          "scroll_step": {
            "type": "number",
            "description": "Pixels per scroll step (default 60)"
          },
          "scroll_interval": {
            "type": "number",
            "description": "Interval between scroll steps in ms (default 50)"
          },
          "scroll_pause_top": {
            "type": "number",
            "description": "Pause at the top before scrolling, ms (default 800)"
          },
          "scroll_pause_bottom": {
            "type": "number",
            "description": "Pause at the bottom after scrolling, ms (default 1200)"
          },
          "storyboard": {
            "type": "string",
            "description": "Path to a storyboard JSON file describing scenes (mutually exclusive with template)"
          },
          "template": {
            "type": "string",
            "description": "Template name under templates/ (e.g. github-repo-intro); combine with param (mutually exclusive with storyboard)"
          },
          "param": {
            "type": "array",
            "description": "Template params as 'key=value' strings; repeatable"
          },
          "list_templates": {
            "type": "boolean",
            "description": "List available templates and exit"
          },
          "wait_for_selector": {
            "type": "string",
            "description": "Wait for this CSS selector before recording"
          },
          "wait_for_timeout": {
            "type": "number",
            "description": "Fixed wait (ms) before recording"
          },
          "color_scheme": {
            "type": "string",
            "description": "light | dark | no-preference (emulate prefers-color-scheme)"
          },
          "user_agent": {
            "type": "string",
            "description": "Override User-Agent"
          },
          "timeout": {
            "type": "number",
            "description": "Playwright global action timeout in ms"
          },
          "ignore_https_errors": {
            "type": "boolean",
            "description": "Ignore HTTPS certificate errors"
          },
          "storage_state": {
            "type": "string",
            "description": "storageState JSON file path (logged-in recording)"
          },
          "cookies": {
            "type": "string",
            "description": "Playwright cookies JSON string or file (top-level array)"
          },
          "no_upload": {
            "type": "boolean",
            "description": "Skip VOD upload; keep only the local webm. By default the recording is transcoded to mp4, a cover frame is grabbed, uploaded to VOD, and a playable CDN URL is returned."
          },
          "vod_title": {
            "type": "string",
            "description": "Title for the uploaded VOD asset (defaults to the output file name)"
          },
          "cover_at_sec": {
            "type": "number",
            "description": "Cover frame timestamp in seconds (default 0.5)"
          },
          "keep_webm": {
            "type": "boolean",
            "description": "Keep the local webm after a successful upload (default: delete it since VOD holds the copy)"
          }
        },
        "required": [
          "url"
        ]
      },
      "ui": {
        "primary": [
          "url",
          "duration",
          "scroll_through"
        ],
        "advanced": [
          "device",
          "viewport",
          "max_duration",
          "stop_when_selector",
          "stop_when_hidden",
          "scroll_step",
          "scroll_interval",
          "scroll_pause_top",
          "scroll_pause_bottom",
          "color_scheme",
          "wait_for_selector",
          "wait_for_timeout",
          "vod_title",
          "cover_at_sec",
          "browser"
        ],
        "hidden": [
          "output",
          "storyboard",
          "template",
          "param",
          "list_templates",
          "user_agent",
          "timeout",
          "ignore_https_errors",
          "storage_state",
          "cookies",
          "no_upload",
          "keep_webm"
        ]
      }
    },
    {
      "id": "web-screenshot",
      "toolName": "web_screenshot",
      "tier": "tool",
      "category": "consuming",
      "title": "Web Page Screenshot",
      "summary": "Drive a headless browser (Playwright Python) to capture any URL to a local STILL IMAGE (png/jpg): full-page / viewport / element / region screenshots, with device emulation, waiting, hide/mask/redact, and static annotations (highlight / arrow / caption / numbered sequence / redact). Images only — for video / recording / scroll-through / storyboard clips (webm) use the web_record tool instead.",
      "triggers": [
        "Web page screenshot, screen capture, full-page screenshot, long screenshot",
        "Capture a specific element / region, partial screenshot, focus on an area",
        "Hide or mask elements before capturing, annotate a still screenshot (highlight box / arrow / label)",
        "Highlight lines L5-L20 of a file and screenshot it",
        "Mobile / device-emulated screenshot, capture with cookies / a logged-in session"
      ],
      "entry": {
        "type": "python",
        "scriptPath": "scripts/screenshot.py"
      },
      "runtime": "python",
      "envVars": [
        "WEB_CAPTURE_BROWSER",
        "WEB_CAPTURE_OUTPUT_BASE",
        "WEB_CAPTURE_NO_SANDBOX",
        "WEB_CAPTURE_ALLOW_PRIVATE_HOSTS",
        "PLAYWRIGHT_BROWSERS_PATH"
      ],
      "parameters": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "Target page URL (http/https)"
          },
          "output": {
            "type": "string",
            "description": "Local output path, must be an image (*.png / *.jpg, default screenshot.png). For .webm video use the web_record tool."
          },
          "browser": {
            "type": "string",
            "description": "chromium | firefox | webkit (default chromium)"
          },
          "device": {
            "type": "string",
            "description": "Device emulation name, e.g. 'iPhone 15 Pro'"
          },
          "viewport": {
            "type": "string",
            "description": "Viewport as 'width,height', e.g. '1280,800'"
          },
          "full_page": {
            "type": "boolean",
            "description": "screenshot.py: capture the whole scrollable page"
          },
          "selector": {
            "type": "string",
            "description": "screenshot.py: capture only the element matching this CSS selector"
          },
          "clip": {
            "type": "string",
            "description": "screenshot.py: region clip 'x,y,w,h' (combined with selector = offset from element box)"
          },
          "wait_for_selector": {
            "type": "string",
            "description": "Wait for this CSS selector before acting"
          },
          "wait_for_timeout": {
            "type": "number",
            "description": "Fixed wait (ms) before acting"
          },
          "annotate": {
            "type": "string",
            "description": "screenshot.py: annotation JSON file or string (array or {annotations, settleMs})"
          },
          "storage_state": {
            "type": "string",
            "description": "storageState JSON file path (logged-in capture)"
          },
          "cookies": {
            "type": "string",
            "description": "Playwright cookies JSON string or file (top-level array)"
          }
        },
        "required": [
          "url"
        ]
      },
      "ui": {
        "primary": [
          "url",
          "full_page"
        ],
        "advanced": [
          "selector",
          "device",
          "viewport",
          "wait_for_selector",
          "wait_for_timeout",
          "browser",
          "clip",
          "annotate",
          "output"
        ],
        "hidden": [
          "storage_state",
          "cookies"
        ]
      }
    }
  ]
}
