|
|
# 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 for Claude (project-level, in current directory)
|
|
|
hf skills add --claude
|
|
|
|
|
|
# install for multiple assistants (project-level)
|
|
|
hf skills add --claude --codex --opencode
|
|
|
|
|
|
# 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, Optional
|
|
|
|
|
|
import typer
|
|
|
|
|
|
from huggingface_hub.errors import CLIError
|
|
|
from huggingface_hub.utils import get_session
|
|
|
|
|
|
from ._cli_utils import typer_factory
|
|
|
|
|
|
|
|
|
DEFAULT_SKILL_ID = "hf-cli"
|
|
|
|
|
|
_GITHUB_RAW_BASE = "https://raw.githubusercontent.com/huggingface/huggingface_hub/main/docs/source/en"
|
|
|
_SKILL_MD_URL = f"{_GITHUB_RAW_BASE}/guides/cli.md"
|
|
|
_REFERENCE_URL = f"{_GITHUB_RAW_BASE}/package_reference/cli.md"
|
|
|
|
|
|
_SKILL_YAML_PREFIX = """\
|
|
|
---
|
|
|
name: hf-cli
|
|
|
description: >
|
|
|
Hugging Face Hub CLI (`hf`) for downloading, uploading, and managing
|
|
|
repositories, models, datasets, and Spaces on the Hugging Face Hub.
|
|
|
---
|
|
|
|
|
|
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`.
|
|
|
"""
|
|
|
|
|
|
CENTRAL_LOCAL = Path(".agents/skills")
|
|
|
CENTRAL_GLOBAL = Path("~/.agents/skills")
|
|
|
|
|
|
GLOBAL_TARGETS = {
|
|
|
"codex": Path("~/.codex/skills"),
|
|
|
"claude": Path("~/.claude/skills"),
|
|
|
"opencode": Path("~/.config/opencode/skills"),
|
|
|
}
|
|
|
|
|
|
LOCAL_TARGETS = {
|
|
|
"codex": Path(".codex/skills"),
|
|
|
"claude": Path(".claude/skills"),
|
|
|
"opencode": Path(".opencode/skills"),
|
|
|
}
|
|
|
|
|
|
skills_cli = typer_factory(help="Manage skills for AI assistants.")
|
|
|
|
|
|
|
|
|
def _download(url: str) -> str:
|
|
|
"""Download text content from a URL."""
|
|
|
response = get_session().get(url)
|
|
|
response.raise_for_status()
|
|
|
return response.text
|
|
|
|
|
|
|
|
|
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 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 _install_to(skills_dir: Path, force: bool) -> Path:
|
|
|
"""Download and install the skill files into a skills directory. Returns the installed path."""
|
|
|
skills_dir = skills_dir.expanduser().resolve()
|
|
|
skills_dir.mkdir(parents=True, exist_ok=True)
|
|
|
dest = skills_dir / DEFAULT_SKILL_ID
|
|
|
|
|
|
_remove_existing(dest, force)
|
|
|
dest.mkdir()
|
|
|
|
|
|
# SKILL.md – the main guide, prefixed with YAML metadata
|
|
|
skill_content = _download(_SKILL_MD_URL)
|
|
|
(dest / "SKILL.md").write_text(_SKILL_YAML_PREFIX + skill_content, encoding="utf-8")
|
|
|
|
|
|
# references/cli.md – the full CLI reference
|
|
|
ref_dir = dest / "references"
|
|
|
ref_dir.mkdir()
|
|
|
ref_content = _download(_REFERENCE_URL)
|
|
|
(ref_dir / "cli.md").write_text(ref_content, encoding="utf-8")
|
|
|
|
|
|
return dest
|
|
|
|
|
|
|
|
|
def _create_symlink(agent_skills_dir: Path, 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 / DEFAULT_SKILL_ID
|
|
|
|
|
|
_remove_existing(link_path, force)
|
|
|
link_path.symlink_to(os.path.relpath(central_skill_path, agent_skills_dir))
|
|
|
|
|
|
return link_path
|
|
|
|
|
|
|
|
|
@skills_cli.command(
|
|
|
"add",
|
|
|
examples=[
|
|
|
"hf skills add --claude",
|
|
|
"hf skills add --claude --global",
|
|
|
"hf skills add --codex --opencode",
|
|
|
],
|
|
|
)
|
|
|
def skills_add(
|
|
|
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[
|
|
|
Optional[Path],
|
|
|
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 skill and install it for an AI assistant."""
|
|
|
if not (claude or codex or opencode or dest):
|
|
|
raise CLIError("Pick a destination via --claude, --codex, --opencode, or --dest.")
|
|
|
|
|
|
if dest:
|
|
|
if claude or codex or opencode or global_:
|
|
|
print("--dest cannot be combined with --claude, --codex, --opencode, or --global.")
|
|
|
raise typer.Exit(code=1)
|
|
|
skill_dest = _install_to(dest, force)
|
|
|
print(f"Installed '{DEFAULT_SKILL_ID}' to {skill_dest}")
|
|
|
return
|
|
|
|
|
|
targets_dict = GLOBAL_TARGETS if global_ else LOCAL_TARGETS
|
|
|
agent_targets: list[Path] = []
|
|
|
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_to(central_path, force)
|
|
|
print(f"Installed '{DEFAULT_SKILL_ID}' to central location: {central_skill_path}")
|
|
|
|
|
|
for agent_target in agent_targets:
|
|
|
link_path = _create_symlink(agent_target, central_skill_path, force)
|
|
|
print(f"Created symlink: {link_path}")
|