"""CLI command to install the Gradio skill for AI coding assistants. Usage: gradio skills add gradio skills add --global gradio skills add --claude gradio skills add --dest=~/my-skills gradio skills add abidlabs/english-translator """ from __future__ import annotations import os import shutil from pathlib import Path from typing import Annotated, cast import typer from gradio_client import Client from gradio_client.snippet import generate_code_snippets from huggingface_hub import HfApi SKILL_ID = "gradio" HF_SKILL_ID = "hf-gradio" _GITHUB_RAW = "https://raw.githubusercontent.com/gradio-app/gradio/main" _SKILL_PREFIX = ".agents/skills/gradio" _HF_SKILL_PREFIX = ".agents/skills/hf-gradio" _SKILL_FILES = ["SKILL.md"] _REFERENCE_FILES = ["examples.md", "api-signatures.md", "event-listeners.md"] _HF_SKILL_FILES = ["SKILL.md"] skills_app = typer.Typer(help="Manage Gradio skills for AI assistants.") def _get_skill_targets(): central_local = Path(".agents/skills") central_global = Path("~/.agents/skills") claude_local = Path(".claude/skills") claude_global = Path("~/.claude/skills") global_targets = { "claude": claude_global, "codex": Path("~/.codex/skills"), "opencode": Path("~/.opencode/skills"), } local_targets = { "claude": claude_local, "codex": Path(".codex/skills"), "opencode": Path(".opencode/skills"), } return central_global, central_local, global_targets, local_targets def _download(url: str) -> str: from huggingface_hub.utils import get_session try: response = get_session().get(url) response.raise_for_status() except Exception as e: raise SystemExit( f"Failed to download {url}\n{e}\n\n" "Make sure you have internet access. The skill files are fetched from " "the Gradio GitHub repository." ) from e return response.text def _remove_existing(path: Path, force: bool) -> None: if not (path.exists() or path.is_symlink()): return if not force: raise SystemExit( f"Skill already exists at {path}.\nRe-run with --force to overwrite." ) if path.is_dir() and not path.is_symlink(): shutil.rmtree(path) else: path.unlink() def _create_symlink( agent_skills_dir: Path, central_skill_path: Path, force: bool, skill_id: str = SKILL_ID, ) -> Path | None: agent_skills_dir = agent_skills_dir.expanduser().resolve() agent_skills_dir.mkdir(parents=True, exist_ok=True) link_path = agent_skills_dir / skill_id if link_path == central_skill_path: # The agent's skills dir is itself a symlink to the central location # (e.g. .claude/skills -> ../.agents/skills), so the skill is already # visible there and linking would clobber it with a link to itself. return None _remove_existing(link_path, force) try: link_path.symlink_to(os.path.relpath(central_skill_path, agent_skills_dir)) except OSError as error: if getattr(error, "winerror", None) != 1314: raise # Windows requires Developer Mode or elevated privileges for symlinks. shutil.copytree(central_skill_path, link_path) return link_path def _print_agent_install(path: Path) -> None: action = "Created symlink" if path.is_symlink() else "Copied skill" print(f"{action}: {path}") def _install_to(skills_dir: Path, force: bool) -> Path: """Install the gradio skill (for building apps).""" skills_dir = skills_dir.expanduser().resolve() skills_dir.mkdir(parents=True, exist_ok=True) dest = skills_dir / SKILL_ID _remove_existing(dest, force) dest.mkdir() # Download main skill files for fname in _SKILL_FILES: content = _download(f"{_GITHUB_RAW}/{_SKILL_PREFIX}/{fname}") (dest / fname).write_text(content, encoding="utf-8") # Download reference files to references/ subdirectory references_dir = dest / "references" references_dir.mkdir(exist_ok=True) for fname in _REFERENCE_FILES: content = _download(f"{_GITHUB_RAW}/{_SKILL_PREFIX}/references/{fname}") (references_dir / fname).write_text(content, encoding="utf-8") return dest def _install_hf_gradio_to(skills_dir: Path, force: bool) -> Path: """Install the hf-gradio skill (for using Spaces via API).""" skills_dir = skills_dir.expanduser().resolve() skills_dir.mkdir(parents=True, exist_ok=True) dest = skills_dir / HF_SKILL_ID _remove_existing(dest, force) dest.mkdir() # Download main skill files for fname in _HF_SKILL_FILES: content = _download(f"{_GITHUB_RAW}/{_HF_SKILL_PREFIX}/{fname}") (dest / fname).write_text(content, encoding="utf-8") return dest def _space_id_to_skill_id(space_id: str) -> str: return space_id.replace("/", "-") def _render_endpoint_section( api_name: str, endpoint_info: dict, space_id: str, src_url: str ) -> str: params = endpoint_info.get("parameters", []) returns = endpoint_info.get("returns", []) lines: list[str] = [] lines.append(f"### `{api_name}`\n") if params: lines.append("**Parameters:**\n") for p in params: ptype = p.get("python_type", {}) type_str = ( ptype.get("type", "Any") if isinstance(ptype, dict) else str(ptype) ) name = p.get("parameter_name") or p.get("label", "input") component = p.get("component", "") default_info = "" if p.get("parameter_has_default"): default_info = f", default: `{p.get('parameter_default')}`" required = ( " (required)" if not p.get("parameter_has_default", False) else "" ) lines.append( f"- `{name}` [{component}]: `{type_str}`{required}{default_info}" ) lines.append("") if returns: lines.append("**Returns:**\n") for r in returns: rtype = r.get("python_type", {}) type_str = ( rtype.get("type", "Any") if isinstance(rtype, dict) else str(rtype) ) label = r.get("label", "output") component = r.get("component", "") lines.append(f"- `{label}` [{component}]: `{type_str}`") lines.append("") snippets = generate_code_snippets( api_name, endpoint_info, src_url, space_id=space_id ) lines.append("**Python:**\n") lines.append("```python") lines.append(snippets["python"]) lines.append("```\n") lines.append("**JavaScript:**\n") lines.append("```javascript") lines.append(snippets["javascript"]) lines.append("```\n") lines.append("**cURL:**\n") lines.append("```bash") lines.append(snippets["bash"]) lines.append("```\n") return "\n".join(lines) def _get_space_description(space_id: str) -> str | None: try: info = HfApi().space_info(space_id) return getattr(info, "short_description", None) or None except Exception: return None def _generate_space_skill(space_id: str) -> tuple[str, str]: try: client = Client(space_id, download_files=False) except Exception as e: raise SystemExit( f"Failed to connect to Space '{space_id}'.\n{e}\n\n" "Make sure the Space exists, is public (or provide HF_TOKEN), and is running." ) from e api_info = cast(dict, client.view_api(print_info=False, return_format="dict")) src_url = client.src skill_id = _space_id_to_skill_id(space_id) space_description = _get_space_description(space_id) lines: list[str] = [] lines.append("---") lines.append(f"name: {skill_id}") desc = ( f"description: Use the {space_id} Gradio Space via API. " f"Provides Python, JavaScript, and cURL usage examples." ) if space_description: desc += f" Space description: {space_description}" lines.append(desc) lines.append("---\n") lines.append(f"# {space_id}\n") lines.append( f"This skill describes how to use the {space_id} " f"Gradio Space programmatically.\n" ) named = api_info.get("named_endpoints", {}) unnamed = api_info.get("unnamed_endpoints", {}) if named: lines.append("## API Endpoints\n") for api_name, endpoint_info in named.items(): lines.append( _render_endpoint_section(api_name, endpoint_info, space_id, src_url) ) if not named and unnamed: lines.append("## API Endpoints\n") for fn_index, endpoint_info in unnamed.items(): lines.append( _render_endpoint_section( f"fn_index={fn_index}", endpoint_info, space_id, src_url ) ) return skill_id, "\n".join(lines) + "\n" def _install_space_skill( skill_id: str, content: str, skills_dir: Path, force: bool ) -> Path: skills_dir = skills_dir.expanduser().resolve() skills_dir.mkdir(parents=True, exist_ok=True) dest = skills_dir / skill_id _remove_existing(dest, force) dest.mkdir() (dest / "SKILL.md").write_text(content, encoding="utf-8") return dest @skills_app.command( "add", ) def skills_add( space_id: Annotated[ str | None, typer.Argument( help="HF Space ID (e.g. 'user/my-space'). If omitted, installs the general Gradio skill." ), ] = None, cursor: Annotated[ bool, typer.Option("--cursor", help="Install for Cursor.") ] = False, claude: Annotated[ bool, typer.Option("--claude", help="Install for Claude.") ] = False, codex: Annotated[bool, typer.Option("--codex", help="Install for Codex.")] = False, opencode: Annotated[ bool, typer.Option("--opencode", help="Install for OpenCode.") ] = False, global_: Annotated[ bool, typer.Option( "--global", "-g", help="Install globally (user-level) instead of in the current project directory.", ), ] = False, dest: Annotated[ Path | None, typer.Option( help="Install into a custom destination (path to skills directory)." ), ] = None, force: Annotated[ bool, typer.Option("--force", help="Overwrite existing skills in the destination."), ] = False, ) -> None: """Download and install Gradio skills for an AI assistant. When called without a space_id, installs both the gradio (building apps) and hf-gradio (using Spaces via API) skills. When called with a space_id, generates and installs a skill for that specific Gradio Space with Python, JS, and cURL usage examples. """ central_global, central_local, hf_global_targets, hf_local_targets = ( _get_skill_targets() ) global_targets = {**hf_global_targets, "cursor": Path("~/.cursor/skills")} local_targets = {**hf_local_targets, "cursor": Path(".cursor/skills")} targets_dict = global_targets if global_ else local_targets if space_id is not None: skill_id, content = _generate_space_skill(space_id) print(f"Generated skill for Space '{space_id}'") if dest: if cursor or claude or codex or opencode or global_: print("--dest cannot be combined with agent flags or --global.") raise typer.Exit(code=1) skill_dest = _install_space_skill(skill_id, content, dest, force) print(f"Installed '{skill_id}' to {skill_dest}") return agent_targets: list[Path] = [] if cursor: agent_targets.append(targets_dict["cursor"]) if claude: agent_targets.append(targets_dict["claude"]) if codex: agent_targets.append(targets_dict["codex"]) if opencode: agent_targets.append(targets_dict["opencode"]) central_path = central_global if global_ else central_local central_skill_path = _install_space_skill( skill_id, content, central_path, force ) print(f"Installed '{skill_id}' to central location: {central_skill_path}") for agent_target in agent_targets: link_path = _create_symlink( agent_target, central_skill_path, force, skill_id=skill_id ) if link_path is not None: _print_agent_install(link_path) return if dest: if cursor or claude or codex or opencode or global_: print("--dest cannot be combined with agent flags or --global.") raise typer.Exit(code=1) skill_dest = _install_to(dest, force) hf_skill_dest = _install_hf_gradio_to(dest, force) print(f"Installed '{SKILL_ID}' to {skill_dest}") print(f"Installed '{HF_SKILL_ID}' to {hf_skill_dest}") return agent_targets = [] if cursor: agent_targets.append(targets_dict["cursor"]) if claude: agent_targets.append(targets_dict["claude"]) if codex: agent_targets.append(targets_dict["codex"]) if opencode: agent_targets.append(targets_dict["opencode"]) central_path = central_global if global_ else central_local # Install gradio skill central_skill_path = _install_to(central_path, force) print(f"Installed '{SKILL_ID}' to central location: {central_skill_path}") # Install hf-gradio skill central_hf_skill_path = _install_hf_gradio_to(central_path, force) print(f"Installed '{HF_SKILL_ID}' to central location: {central_hf_skill_path}") for agent_target in agent_targets: # Create symlinks for both skills link_path = _create_symlink(agent_target, central_skill_path, force) if link_path is not None: _print_agent_install(link_path) hf_link_path = _create_symlink( agent_target, central_hf_skill_path, force, skill_id=HF_SKILL_ID ) if hf_link_path is not None: _print_agent_install(hf_link_path)