| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446 |
- # Copyright 2025 The HuggingFace Team. All rights reserved.
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- """Contains commands to manage skills for AI assistants.
- Usage:
- # install the hf-cli skill in common .agents/skills directory (either in current directory or user-level)
- hf skills add
- hf skills add --global
- # install the hf-cli skill for Claude (project-level, in current directory)
- hf skills add --claude
- # install globally (user-level)
- hf skills add --claude --global
- # install to a custom directory
- hf skills add --dest=~/my-skills
- # overwrite an existing skill
- hf skills add --claude --force
- """
- import os
- import shutil
- from pathlib import Path
- from typing import Annotated
- import typer
- from click import Command, Context, Group
- from typer.main import get_command
- from huggingface_hub.errors import CLIError
- from . import _skills
- from ._cli_utils import typer_factory
- DEFAULT_SKILL_ID = "hf-cli"
- _SKILL_DESCRIPTION = (
- "Hugging Face Hub CLI (`hf`) for downloading, uploading, and managing"
- " models, datasets, spaces, buckets, repos, papers, jobs, and more on the Hugging Face Hub."
- " Use when: handling authentication;"
- " managing local cache;"
- " managing Hugging Face Buckets;"
- " running or scheduling jobs on Hugging Face infrastructure;"
- " managing Hugging Face repos;"
- " discussions and pull requests;"
- " browsing models, datasets and spaces;"
- " reading, searching, or browsing academic papers;"
- " managing collections;"
- " querying datasets;"
- " configuring spaces;"
- " setting up webhooks;"
- " or deploying and managing HF Inference Endpoints."
- " Make sure to use this skill whenever the user mentions"
- " 'hf', 'huggingface', 'Hugging Face', 'huggingface-cli', or 'hugging face cli',"
- " or wants to do anything related to the Hugging Face ecosystem and to AI and ML in general."
- " Also use for cloud storage needs like training checkpoints, data pipelines, or agent traces."
- " Use even if the user doesn't explicitly ask for a CLI command."
- " Replaces the deprecated `huggingface-cli`."
- )
- _SKILL_YAML_PREFIX = f"""\
- ---
- name: hf-cli
- description: "{_SKILL_DESCRIPTION}"
- ---
- Install: `curl -LsSf https://hf.co/cli/install.sh | bash -s`.
- The Hugging Face Hub CLI tool `hf` is available. IMPORTANT: The `hf` command replaces the deprecated `huggingface-cli` command.
- Use `hf --help` to view available functions. Note that auth commands are now all under `hf auth` e.g. `hf auth whoami`.
- """
- _SKILL_TIPS = """
- ## Mounting repos as local filesystems
- To mount Hub repositories or buckets as local filesystems — no download, no copy, no waiting — use `hf-mount`. Files are fetched on demand. GitHub: https://github.com/huggingface/hf-mount
- Install: `curl -fsSL https://raw.githubusercontent.com/huggingface/hf-mount/main/install.sh | sh`
- Some command examples:
- - `hf-mount start repo openai-community/gpt2 /tmp/gpt2` — mount a repo (read-only)
- - `hf-mount start --hf-token $HF_TOKEN bucket myuser/my-bucket /tmp/data` — mount a bucket (read-write)
- - `hf-mount status` / `hf-mount stop /tmp/data` — list or unmount
- ## Tips
- - Use `hf <command> --help` for full options, descriptions, usage, and real-world examples
- - Authenticate with `HF_TOKEN` env var (recommended) or with `--token`
- """
- CENTRAL_LOCAL = Path(".agents/skills")
- CENTRAL_GLOBAL = Path("~/.agents/skills")
- CLAUDE_LOCAL = Path(".claude/skills")
- CLAUDE_GLOBAL = Path("~/.claude/skills")
- # Flags worth explaining in the common-options glossary. Self-explanatory flags
- # (--namespace, --yes, --private, …) are omitted even if they appear frequently.
- _COMMON_FLAG_ALLOWLIST = {"--token", "--quiet", "--type", "--format", "--revision"}
- # Keep token out of inline command signatures to encourage env based auth.
- _INLINE_FLAG_EXCLUDE = {"--token"}
- _COMMON_FLAG_HELP_OVERRIDES: dict[str, str] = {
- "--format": "Output format: `--format json` (or `--json`) or `--format table` (default).",
- "--token": "Use a User Access Token. Prefer setting `HF_TOKEN` env var instead of passing `--token`.",
- }
- skills_cli = typer_factory(help="Manage skills for AI assistants.")
- def _format_params(cmd: Command) -> str:
- """Format required params: positional as UPPER_CASE, options as ``--name TYPE``."""
- parts = []
- for p in cmd.params:
- if not p.required or p.human_readable_name == "--help":
- continue
- if p.name and p.name.startswith("_"):
- continue
- long_name = next((o for o in getattr(p, "opts", []) if o.startswith("--")), None)
- if long_name is not None:
- type_name = getattr(p.type, "name", "").upper() or "VALUE"
- parts.append(f"{long_name} {type_name}")
- elif p.name:
- parts.append(p.human_readable_name)
- return " ".join(parts)
- def _collect_leaf_commands(group: Group, ctx: Context, path_parts: list[str]) -> list[tuple[list[str], Command]]:
- """Recursively walk a Click Group, returning (full_path_parts, cmd) for every leaf command."""
- leaves: list[tuple[list[str], Command]] = []
- sub_ctx = Context(group, parent=ctx, info_name=path_parts[-1])
- for name in group.list_commands(sub_ctx):
- cmd = group.get_command(sub_ctx, name)
- if cmd is None or cmd.hidden:
- continue
- child_path = [*path_parts, name]
- if isinstance(cmd, Group):
- leaves.extend(_collect_leaf_commands(cmd, sub_ctx, child_path))
- else:
- leaves.append((child_path, cmd))
- return leaves
- def _iter_optional_params(cmd: Command):
- """Yield (param, long_name, short_name) for each optional, non-internal param."""
- for p in cmd.params:
- if p.required or p.human_readable_name == "--help":
- continue
- if p.name and p.name.startswith("_"):
- continue
- long_name = None
- short_name = None
- for opt in getattr(p, "opts", []):
- if opt.startswith("--"):
- long_name = long_name or opt
- elif opt.startswith("-"):
- short_name = opt
- if long_name:
- yield p, long_name, short_name
- def _get_flag_names(cmd: Command, *, exclude: set[str] | None = None) -> list[str]:
- """Return long-form flag names (--foo) for optional, non-internal params.
- Boolean flags are bare (``--dry-run``). Value-taking options include a
- type hint (``--include TEXT``, ``--max-workers INTEGER``).
- """
- flags: list[str] = []
- for p, long_name, _short in _iter_optional_params(cmd):
- if exclude and long_name in exclude:
- continue
- if getattr(p, "is_flag", False):
- flags.append(long_name)
- else:
- type_name = getattr(p.type, "name", "").upper() or "VALUE"
- flags.append(f"{long_name} {type_name}")
- return flags
- def _compute_common_flags(
- leaf_commands: list[tuple[list[str], Command]],
- ) -> dict[str, tuple[str, str]]:
- """Collect display info for flags in the allowlist."""
- flag_info: dict[str, tuple[str, str]] = {}
- for _path, cmd in leaf_commands:
- for p, long_name, short_name in _iter_optional_params(cmd):
- if long_name not in _COMMON_FLAG_ALLOWLIST:
- continue
- # Prefer the version with a short form (e.g. "-q / --quiet" over just "--quiet")
- if long_name not in flag_info or (short_name and " / " not in flag_info[long_name][0]):
- display = f"{short_name} / {long_name}" if short_name else long_name
- help_text = (getattr(p, "help", None) or "").split("\n")[0].strip()
- flag_info[long_name] = (display, help_text)
- return flag_info
- def _render_leaf(path_parts: list[str], cmd: Command) -> str:
- """Render a single leaf command as a markdown list entry."""
- help_text = (cmd.help or "").split("\n")[0].strip()
- params = _format_params(cmd)
- parts = ["hf", *path_parts] + ([params] if params else [])
- entry = f"- `{' '.join(parts)}` — {help_text}"
- flags = _get_flag_names(cmd, exclude=_INLINE_FLAG_EXCLUDE)
- if flags:
- entry += f" `[{' '.join(flags)}]`"
- return entry
- def build_skill_md() -> str:
- # Lazy import to avoid circular dependency (hf.py imports skills_cli from this module)
- from huggingface_hub import __version__
- from huggingface_hub.cli.hf import app
- click_app = get_command(app)
- ctx = Context(click_app, info_name="hf")
- top_level: list[tuple[list[str], Command]] = []
- groups: list[tuple[str, Group]] = []
- for name in sorted(click_app.list_commands(ctx)): # type: ignore[attr-defined]
- cmd = click_app.get_command(ctx, name) # type: ignore[attr-defined]
- if cmd is None or cmd.hidden:
- continue
- if isinstance(cmd, Group):
- groups.append((name, cmd))
- else:
- top_level.append(([name], cmd))
- group_leaves: list[tuple[str, list[tuple[list[str], Command]]]] = []
- all_leaf_commands: list[tuple[list[str], Command]] = list(top_level)
- for name, group in groups:
- leaves = _collect_leaf_commands(group, ctx, [name])
- group_leaves.append((name, leaves))
- all_leaf_commands.extend(leaves)
- common_flags = _compute_common_flags(all_leaf_commands)
- # wrap in list to widen list[LiteralString] -> list[str] for `ty`
- lines: list[str] = list(_SKILL_YAML_PREFIX.splitlines())
- lines.append("")
- lines.append(f"Generated with `huggingface_hub v{__version__}`. Run `hf skills add --force` to regenerate.")
- lines.append("")
- lines.append("## Commands")
- lines.append("")
- for path_parts, cmd in top_level:
- lines.append(_render_leaf(path_parts, cmd))
- groups_dict = dict(groups)
- for name, leaves in group_leaves:
- group_cmd = groups_dict[name]
- help_text = (group_cmd.help or "").split("\n")[0].strip()
- lines.append("")
- lines.append(f"### `hf {name}` — {help_text}")
- lines.append("")
- for path_parts, cmd in leaves:
- lines.append(_render_leaf(path_parts, cmd))
- if common_flags:
- lines.append("")
- lines.append("## Common options")
- lines.append("")
- for long_name, (display, help_text) in sorted(common_flags.items()):
- help_text = _COMMON_FLAG_HELP_OVERRIDES.get(long_name, help_text)
- if help_text:
- lines.append(f"- `{display}` — {help_text}")
- else:
- lines.append(f"- `{display}`")
- lines.extend(_SKILL_TIPS.splitlines())
- return "\n".join(lines)
- def _remove_existing(path: Path, force: bool) -> None:
- """Remove existing file/directory/symlink if force is True, otherwise raise an error."""
- if not (path.exists() or path.is_symlink()):
- return
- if not force:
- raise CLIError(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 _install_to(skills_dir: Path, skill_name: str, force: bool) -> Path:
- """Install a marketplace skill into a skills directory. Returns the installed path."""
- skill = _skills.get_marketplace_skill(skill_name)
- try:
- return _skills.install_marketplace_skill(skill, skills_dir, force=force)
- except FileExistsError as exc:
- raise CLIError(f"{exc}\nRe-run with --force to overwrite.") from exc
- def _create_symlink(agent_skills_dir: Path, skill_name: str, central_skill_path: Path, force: bool) -> Path:
- """Create a relative symlink from agent directory to the central skill location."""
- agent_skills_dir = agent_skills_dir.expanduser().resolve()
- agent_skills_dir.mkdir(parents=True, exist_ok=True)
- link_path = agent_skills_dir / skill_name
- _remove_existing(link_path, force)
- link_path.symlink_to(os.path.relpath(central_skill_path, agent_skills_dir))
- return link_path
- def _resolve_update_roots(
- *,
- claude: bool,
- global_: bool,
- dest: Path | None,
- ) -> list[Path]:
- if dest is not None:
- if claude or global_:
- raise CLIError("--dest cannot be combined with --claude or --global.")
- return [dest.expanduser().resolve()]
- roots: list[Path] = [CENTRAL_GLOBAL if global_ else CENTRAL_LOCAL]
- if claude:
- roots.append(CLAUDE_GLOBAL if global_ else CLAUDE_LOCAL)
- return [root.expanduser().resolve() for root in roots]
- @skills_cli.command("preview")
- def skills_preview() -> None:
- """Print the generated `hf-cli` SKILL.md to stdout."""
- print(build_skill_md())
- @skills_cli.command(
- "add",
- examples=[
- "hf skills add",
- "hf skills add huggingface-gradio --dest=~/my-skills",
- "hf skills add --global",
- "hf skills add --claude",
- "hf skills add huggingface-gradio --claude --global",
- ],
- )
- def skills_add(
- name: Annotated[
- str,
- typer.Argument(help="Marketplace skill name.", show_default=False),
- ] = DEFAULT_SKILL_ID,
- claude: Annotated[bool, typer.Option("--claude", help="Install for Claude.")] = 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 a Hugging Face skill and install it for an AI assistant.
- Default location is in the current directory (.agents/skills) or user-level (~/.agents/skills).
- If `--claude` is specified, the skill is also symlinked into Claude's legacy skills directory.
- """
- if dest is not None:
- if claude or global_:
- raise CLIError("--dest cannot be combined with --claude or --global.")
- skill_dest = _install_to(dest, name, force)
- print(f"Installed '{name}' to {skill_dest}")
- return
- # Install to central location
- central_path = CENTRAL_GLOBAL if global_ else CENTRAL_LOCAL
- central_skill_path = _install_to(central_path, name, force)
- print(f"Installed '{name}' to central location: {central_skill_path}")
- if claude:
- agent_target = CLAUDE_GLOBAL if global_ else CLAUDE_LOCAL
- link_path = _create_symlink(agent_target, name, central_skill_path, force)
- print(f"Created symlink: {link_path}")
- @skills_cli.command(
- "upgrade",
- examples=[
- "hf skills upgrade",
- "hf skills upgrade hf-cli",
- "hf skills upgrade huggingface-gradio --dest=~/my-skills",
- "hf skills upgrade --claude",
- ],
- )
- def skills_upgrade(
- name: Annotated[
- str | None,
- typer.Argument(help="Optional installed skill name to upgrade.", show_default=False),
- ] = None,
- claude: Annotated[bool, typer.Option("--claude", help="Upgrade skills installed for Claude.")] = False,
- global_: Annotated[
- bool,
- typer.Option(
- "--global",
- "-g",
- help="Use global skills directories instead of the current project.",
- ),
- ] = False,
- dest: Annotated[
- Path | None,
- typer.Option(
- help="Upgrade skills in a custom skills directory.",
- ),
- ] = None,
- ) -> None:
- """Upgrade installed Hugging Face marketplace skills."""
- roots = _resolve_update_roots(claude=claude, global_=global_, dest=dest)
- results = _skills.apply_updates(roots, selector=name)
- if not results:
- print("No installed skills found.")
- return
- for result in results:
- detail = f" ({result.detail})" if result.detail else ""
- print(f"{result.name}: {result.status}{detail}")
|