diff --git a/.github/workflows/sync-huggingface-skills.yml b/.github/workflows/sync-huggingface-skills.yml index cce1b9b0196..3937fb0b930 100644 --- a/.github/workflows/sync-huggingface-skills.yml +++ b/.github/workflows/sync-huggingface-skills.yml @@ -50,14 +50,16 @@ jobs: uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 - name: Copy generated files - run: cp trl/skills/trl-training/SKILL.md skills-repo/skills/trl-training/ + run: | + mkdir -p skills-repo/skills/trl-training + cp skills/trl-training/SKILL.md skills-repo/skills/trl-training/ - name: Check for TRL skill changes id: check_changes working-directory: skills-repo - # git diff returns zero if there is no diff + # git status --porcelain prints nothing if there is no change run: | - if git diff --quiet -- skills/trl-training/SKILL.md; then + if [ -z "$(git status --porcelain -- skills/trl-training/)" ]; then echo "changed=false" >> "$GITHUB_OUTPUT" echo "No trl skill changes; skipping PR" else @@ -82,7 +84,7 @@ jobs: body: | Auto-generated from [trl@${{ github.sha }}](https://github.com/huggingface/trl/commit/${{ github.sha }}) - Triggered by changes to `trl/skills/trl-training` + Triggered by changes to `skills/trl-training` --- This PR was created automatically by the [sync-huggingface-skills](https://github.com/huggingface/trl/blob/main/.github/workflows/sync-huggingface-skills.yml) workflow. diff --git a/MANIFEST.in b/MANIFEST.in index e843c590270..5ec218a49f0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,6 +5,5 @@ include trl/accelerate_configs/*.yaml include trl/templates/*.md include trl/chat_templates/*.jinja include trl/chat_templates/*.md -include trl/skills/**/*.md recursive-exclude * __pycache__ prune tests diff --git a/pyproject.toml b/pyproject.toml index 53ffb54f9b8..75db0e9f0d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -169,7 +169,6 @@ extend-select = ["E", "F", "I", "W", "UP", "B", "T", "C"] "examples/**.py" = ["T201"] "scripts/**.py" = ["T201"] "trl/cli/**.py" = ["T201"] -"trl/skills/cli.py" = ["T201"] # Ignore import violations in all `__init__.py` files. "__init__.py" = ["F401"] diff --git a/trl/skills/trl-training/SKILL.md b/skills/trl-training/SKILL.md similarity index 100% rename from trl/skills/trl-training/SKILL.md rename to skills/trl-training/SKILL.md diff --git a/tests/test_skills.py b/tests/test_skills.py deleted file mode 100644 index 8a9362069db..00000000000 --- a/tests/test_skills.py +++ /dev/null @@ -1,544 +0,0 @@ -# Copyright 2020-2026 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. - -from pathlib import Path - -import pytest - -from trl.skills import install_skill, list_agent_names, list_skills, resolve_target_path, uninstall_skill -from trl.skills.skills import _get_trl_skills_dir - - -class TestGetTrlSkillsDir: - """Tests for _get_trl_skills_dir function.""" - - def test_returns_path_object(self): - """Test that returns a Path object.""" - skills_dir = _get_trl_skills_dir() - assert isinstance(skills_dir, Path) - - def test_directory_exists(self): - """Test that the returned directory exists.""" - skills_dir = _get_trl_skills_dir() - assert skills_dir.exists(), f"Skills directory does not exist: {skills_dir}" - - def test_is_directory(self): - """Test that the returned path is a directory.""" - skills_dir = _get_trl_skills_dir() - assert skills_dir.is_dir(), f"Skills path is not a directory: {skills_dir}" - - def test_contains_skills_module(self): - """Test that the path ends with 'skills' (the module name).""" - skills_dir = _get_trl_skills_dir() - assert skills_dir.name == "skills" - - -class TestListSkills: - """Tests for list_skills function.""" - - def test_returns_list(self): - """Test that list_skills returns a list.""" - skills = list_skills() - assert isinstance(skills, list) - - def test_contains_trl_training(self): - """Test that list_skills includes the trl-training skill.""" - skills = list_skills() - assert "trl-training" in skills - - def test_skills_are_sorted(self): - """Test that skills are returned in sorted order.""" - skills = list_skills() - assert skills == sorted(skills) - - def test_with_custom_directory(self, tmp_path): - """Test list_skills with a custom directory.""" - # Create fake skills - (tmp_path / "skill1").mkdir() - (tmp_path / "skill1" / "SKILL.md").write_text("# Skill 1") - (tmp_path / "skill2").mkdir() - (tmp_path / "skill2" / "SKILL.md").write_text("# Skill 2") - (tmp_path / "not-a-skill").mkdir() # No SKILL.md - - skills = list_skills(tmp_path) - assert skills == ["skill1", "skill2"] - - def test_empty_directory(self, tmp_path): - """Test list_skills with an empty directory.""" - skills = list_skills(tmp_path) - assert skills == [] - - def test_nonexistent_directory(self, tmp_path): - """Test list_skills with a non-existent directory.""" - nonexistent = tmp_path / "nonexistent" - skills = list_skills(nonexistent) - assert skills == [] - - def test_ignores_files(self, tmp_path): - """Test that list_skills ignores files, only returns directories.""" - (tmp_path / "skill1").mkdir() - (tmp_path / "skill1" / "SKILL.md").write_text("# Skill 1") - (tmp_path / "not-a-skill.txt").write_text("Not a skill") - - skills = list_skills(tmp_path) - assert skills == ["skill1"] - - def test_requires_skill_md(self, tmp_path): - """Test that directories without SKILL.md are ignored.""" - (tmp_path / "has-skill-md").mkdir() - (tmp_path / "has-skill-md" / "SKILL.md").write_text("# Valid") - (tmp_path / "no-skill-md").mkdir() - (tmp_path / "no-skill-md" / "readme.md").write_text("# Invalid") - - skills = list_skills(tmp_path) - assert skills == ["has-skill-md"] - - -class TestInstallSkill: - """Tests for install_skill function.""" - - def test_basic_installation(self, tmp_path): - """Test basic skill installation.""" - target_dir = tmp_path / "target" - - result = install_skill("trl-training", target_dir) - - assert result is True - assert (target_dir / "trl-training").exists() - assert (target_dir / "trl-training" / "SKILL.md").exists() - - def test_creates_target_directory(self, tmp_path): - """Test that install_skill creates the target directory if it doesn't exist.""" - target_dir = tmp_path / "nested" / "target" - - install_skill("trl-training", target_dir) - - assert target_dir.exists() - assert (target_dir / "trl-training").exists() - - def test_skill_not_found(self, tmp_path): - """Test that install_skill raises FileNotFoundError for non-existent skill.""" - target_dir = tmp_path / "target" - - with pytest.raises(FileNotFoundError, match="Skill 'nonexistent' not found"): - install_skill("nonexistent", target_dir) - - def test_skill_already_exists_without_force(self, tmp_path): - """Test that install_skill raises FileExistsError if skill exists and force=False.""" - target_dir = tmp_path / "target" - - # Install once - install_skill("trl-training", target_dir) - - # Try to install again without force - with pytest.raises(FileExistsError, match="already installed"): - install_skill("trl-training", target_dir, force=False) - - def test_force_overwrites_existing(self, tmp_path): - """Test that install_skill with force=True overwrites existing skill.""" - target_dir = tmp_path / "target" - - # Install once - install_skill("trl-training", target_dir) - - # Modify the installed skill - marker_file = target_dir / "trl-training" / "marker.txt" - marker_file.write_text("This should be removed") - - # Install again with force - result = install_skill("trl-training", target_dir, force=True) - - assert result is True - assert (target_dir / "trl-training").exists() - assert not marker_file.exists() # Marker should be gone - - def test_skill_not_directory(self, tmp_path): - """Test that install_skill raises ValueError if skill is not a directory.""" - source_dir = tmp_path / "source" - source_dir.mkdir() - target_dir = tmp_path / "target" - - # Create a file instead of directory - (source_dir / "fake-skill").write_text("not a directory") - - with pytest.raises(ValueError, match="is not a directory"): - install_skill("fake-skill", target_dir, source=source_dir) - - def test_preserves_directory_structure(self, tmp_path): - """Test that install_skill preserves the skill's directory structure.""" - source_dir = tmp_path / "source" - target_dir = tmp_path / "target" - - # Create a skill with subdirectories - skill_dir = source_dir / "test-skill" - skill_dir.mkdir(parents=True) - (skill_dir / "SKILL.md").write_text("# Test") - (skill_dir / "subdir").mkdir() - (skill_dir / "subdir" / "file.txt").write_text("content") - - install_skill("test-skill", target_dir, source=source_dir) - - assert (target_dir / "test-skill" / "SKILL.md").exists() - assert (target_dir / "test-skill" / "subdir" / "file.txt").exists() - assert (target_dir / "test-skill" / "subdir" / "file.txt").read_text() == "content" - - def test_install_to_same_directory_fails(self, tmp_path): - """Test that installing to the same directory as source is handled correctly.""" - source_dir = tmp_path / "skills" - source_dir.mkdir() - - # Create a skill - skill_dir = source_dir / "test-skill" - skill_dir.mkdir() - (skill_dir / "SKILL.md").write_text("# Test") - - # Try to install to same directory (should fail with exists error) - with pytest.raises(FileExistsError): - install_skill("test-skill", source_dir, source=source_dir, force=False) - - -class TestUninstallSkill: - """Tests for uninstall_skill function.""" - - def test_basic_uninstallation(self, tmp_path): - """Test basic skill uninstallation.""" - target_dir = tmp_path / "target" - - # Install first - install_skill("trl-training", target_dir) - assert (target_dir / "trl-training").exists() - - # Uninstall - result = uninstall_skill("trl-training", target_dir) - - assert result is True - assert not (target_dir / "trl-training").exists() - - def test_skill_not_installed(self, tmp_path): - """Test that uninstall_skill raises FileNotFoundError for non-existent skill.""" - target_dir = tmp_path / "target" - target_dir.mkdir() - - with pytest.raises(FileNotFoundError, match="not installed"): - uninstall_skill("nonexistent", target_dir) - - def test_uninstall_from_nonexistent_directory(self, tmp_path): - """Test uninstall_skill when target directory doesn't exist.""" - target_dir = tmp_path / "nonexistent" - - with pytest.raises(FileNotFoundError, match="not installed"): - uninstall_skill("trl-training", target_dir) - - def test_uninstall_removes_all_contents(self, tmp_path): - """Test that uninstall removes the entire skill directory.""" - source_dir = tmp_path / "source" - target_dir = tmp_path / "target" - - # Create a skill with multiple files - skill_dir = source_dir / "test-skill" - skill_dir.mkdir(parents=True) - (skill_dir / "SKILL.md").write_text("# Test") - (skill_dir / "file1.txt").write_text("content1") - (skill_dir / "subdir").mkdir() - (skill_dir / "subdir" / "file2.txt").write_text("content2") - - # Install and uninstall - install_skill("test-skill", target_dir, source=source_dir) - uninstall_skill("test-skill", target_dir) - - assert not (target_dir / "test-skill").exists() - # Target directory itself should still exist - assert target_dir.exists() - - def test_uninstall_doesnt_affect_other_skills(self, tmp_path): - """Test that uninstalling one skill doesn't affect others.""" - source_dir = tmp_path / "source" - target_dir = tmp_path / "target" - - # Create two skills - for skill_name in ["skill1", "skill2"]: - skill_dir = source_dir / skill_name - skill_dir.mkdir(parents=True) - (skill_dir / "SKILL.md").write_text(f"# {skill_name}") - - # Install both - install_skill("skill1", target_dir, source=source_dir) - install_skill("skill2", target_dir, source=source_dir) - - # Uninstall one - uninstall_skill("skill1", target_dir) - - # Check that only skill1 is removed - assert not (target_dir / "skill1").exists() - assert (target_dir / "skill2").exists() - - -class TestIntegration: - """Integration tests for skills functions.""" - - def test_full_workflow(self, tmp_path): - """Test complete install -> list -> uninstall workflow.""" - source_dir = tmp_path / "source" - target_dir = tmp_path / "target" - - # Create skills - for i in range(3): - skill_dir = source_dir / f"skill{i}" - skill_dir.mkdir(parents=True) - (skill_dir / "SKILL.md").write_text(f"# Skill {i}") - - # List available skills - available = list_skills(target=source_dir) - assert available == ["skill0", "skill1", "skill2"] - - # Install skills - for skill in available: - install_skill(skill, target_dir, source=source_dir) - - # List installed skills - installed_dirs = [d.name for d in target_dir.iterdir() if d.is_dir()] - assert sorted(installed_dirs) == ["skill0", "skill1", "skill2"] - - # Uninstall one skill - uninstall_skill("skill1", target_dir) - - # Verify - installed_dirs = [d.name for d in target_dir.iterdir() if d.is_dir()] - assert sorted(installed_dirs) == ["skill0", "skill2"] - - def test_install_uninstall_cycle(self, tmp_path): - """Test that we can install and uninstall the same skill multiple times.""" - source_dir = tmp_path / "source" - target_dir = tmp_path / "target" - - # Create skill - skill_dir = source_dir / "test-skill" - skill_dir.mkdir(parents=True) - (skill_dir / "SKILL.md").write_text("# Test") - - # Install -> Uninstall -> Install -> Uninstall - for _ in range(2): - install_skill("test-skill", target_dir, source=source_dir) - assert (target_dir / "test-skill").exists() - - uninstall_skill("test-skill", target_dir) - assert not (target_dir / "test-skill").exists() - - def test_force_reinstall_workflow(self, tmp_path): - """Test the workflow of using force to update an installed skill.""" - source_dir = tmp_path / "source" - target_dir = tmp_path / "target" - - # Create initial skill version - skill_dir = source_dir / "test-skill" - skill_dir.mkdir(parents=True) - (skill_dir / "SKILL.md").write_text("# Version 1") - - # Install - install_skill("test-skill", target_dir, source=source_dir) - assert (target_dir / "test-skill" / "SKILL.md").read_text() == "# Version 1" - - # Update source skill - (skill_dir / "SKILL.md").write_text("# Version 2") - - # Force reinstall - install_skill("test-skill", target_dir, source=source_dir, force=True) - assert (target_dir / "test-skill" / "SKILL.md").read_text() == "# Version 2" - - -class TestEdgeCases: - """Tests for edge cases and special scenarios.""" - - def test_skill_with_special_characters_in_name(self, tmp_path): - """Test handling skills with special characters in names.""" - source_dir = tmp_path / "source" - target_dir = tmp_path / "target" - - # Create skill with hyphens and underscores (common in skill names) - skill_name = "test-skill_v2" - skill_dir = source_dir / skill_name - skill_dir.mkdir(parents=True) - (skill_dir / "SKILL.md").write_text("# Test") - - # Should work fine - install_skill(skill_name, target_dir, source=source_dir) - assert (target_dir / skill_name).exists() - - uninstall_skill(skill_name, target_dir) - assert not (target_dir / skill_name).exists() - - def test_empty_skill_directory(self, tmp_path): - """Test installing a skill with only SKILL.md (no other files).""" - source_dir = tmp_path / "source" - target_dir = tmp_path / "target" - - skill_dir = source_dir / "minimal-skill" - skill_dir.mkdir(parents=True) - (skill_dir / "SKILL.md").write_text("# Minimal") - - install_skill("minimal-skill", target_dir, source=source_dir) - - assert (target_dir / "minimal-skill" / "SKILL.md").exists() - # Should only contain SKILL.md - files = list((target_dir / "minimal-skill").iterdir()) - assert len(files) == 1 - assert files[0].name == "SKILL.md" - - def test_skill_with_hidden_files(self, tmp_path): - """Test that hidden files are preserved during installation.""" - source_dir = tmp_path / "source" - target_dir = tmp_path / "target" - - skill_dir = source_dir / "test-skill" - skill_dir.mkdir(parents=True) - (skill_dir / "SKILL.md").write_text("# Test") - (skill_dir / ".hidden").write_text("hidden content") - - install_skill("test-skill", target_dir, source=source_dir) - - assert (target_dir / "test-skill" / ".hidden").exists() - assert (target_dir / "test-skill" / ".hidden").read_text() == "hidden content" - - -class TestListAgentNames: - """Tests for list_agent_names function.""" - - def test_returns_list(self): - """Test that list_agent_names returns a list.""" - agents = list_agent_names() - assert isinstance(agents, list) - - def test_contains_expected_agents(self): - """Test that list includes expected agent names.""" - agents = list_agent_names() - assert "agents" in agents - assert "claude" in agents - - -class TestResolveTargetPath: - """Tests for resolve_target_path function.""" - - def test_resolve_agent_name_project_scope(self): - """Test resolving agent name with project scope.""" - path = resolve_target_path("claude", "project") - assert path == Path("./.claude/skills").expanduser().resolve() - - def test_resolve_agent_name_global_scope(self): - """Test resolving agent name with global scope.""" - path = resolve_target_path("claude", "global") - assert path == Path("~/.claude/skills").expanduser().resolve() - - def test_resolve_custom_path_string(self): - """Test resolving custom path as string.""" - path = resolve_target_path("/custom/path", "project") - assert path == Path("/custom/path").resolve() - - def test_resolve_custom_path_object(self): - """Test resolving Path object.""" - custom = Path("/custom/path") - path = resolve_target_path(custom, "project") - assert path == Path("/custom/path").resolve() - - def test_resolve_path_with_tilde(self): - """Test that tilde expansion works.""" - path = resolve_target_path("~/my/skills", "project") - assert path == Path("~/my/skills").expanduser().resolve() - assert "~" not in str(path) - - def test_all_predefined_agents(self): - """Test that all predefined agents can be resolved.""" - for agent in list_agent_names(): - for scope in ["project", "global"]: - path = resolve_target_path(agent, scope) - assert isinstance(path, Path) - assert path.is_absolute() - - def test_invalid_scope_for_predefined_agent(self): - """Test invalid scope raises ValueError for predefined agents.""" - with pytest.raises(ValueError, match="Invalid scope"): - resolve_target_path("claude", "invalid") - - -class TestHighLevelAPI: - """Tests for the new high-level API (target/scope instead of Path).""" - - def test_list_skills_with_target_string(self, tmp_path): - """Test list_skills with target as string (custom path).""" - # Create skills in target - (tmp_path / "skill1").mkdir() - (tmp_path / "skill1" / "SKILL.md").write_text("# Skill 1") - - skills = list_skills(target=str(tmp_path), scope="project") - assert skills == ["skill1"] - - def test_list_skills_with_target_path(self, tmp_path): - """Test list_skills with target as Path object.""" - (tmp_path / "skill1").mkdir() - (tmp_path / "skill1" / "SKILL.md").write_text("# Skill 1") - - skills = list_skills(target=tmp_path, scope="project") - assert skills == ["skill1"] - - def test_list_skills_without_target(self): - """Test list_skills without target lists TRL's built-in skills.""" - skills = list_skills() - assert isinstance(skills, list) - assert "trl-training" in skills - - def test_install_skill_with_target_string(self, tmp_path): - """Test install_skill with target as string.""" - result = install_skill("trl-training", target=str(tmp_path), scope="project") - assert result is True - assert (tmp_path / "trl-training").exists() - - def test_install_skill_with_target_path(self, tmp_path): - """Test install_skill with target as Path object.""" - result = install_skill("trl-training", target=tmp_path, scope="project") - assert result is True - assert (tmp_path / "trl-training").exists() - - def test_install_skill_with_force(self, tmp_path): - """Test install_skill with force parameter.""" - install_skill("trl-training", target=tmp_path) - # Install again with force - result = install_skill("trl-training", target=tmp_path, force=True) - assert result is True - - def test_uninstall_skill_with_target_string(self, tmp_path): - """Test uninstall_skill with target as string.""" - install_skill("trl-training", target=tmp_path) - result = uninstall_skill("trl-training", target=str(tmp_path), scope="project") - assert result is True - assert not (tmp_path / "trl-training").exists() - - def test_uninstall_skill_with_target_path(self, tmp_path): - """Test uninstall_skill with target as Path object.""" - install_skill("trl-training", target=tmp_path) - result = uninstall_skill("trl-training", target=tmp_path, scope="project") - assert result is True - assert not (tmp_path / "trl-training").exists() - - def test_install_with_custom_source(self, tmp_path): - """Test install_skill with custom source parameter.""" - source_dir = tmp_path / "source" - target_dir = tmp_path / "target" - - # Create custom skill - skill_dir = source_dir / "custom-skill" - skill_dir.mkdir(parents=True) - (skill_dir / "SKILL.md").write_text("# Custom") - - result = install_skill("custom-skill", target=target_dir, source=source_dir) - assert result is True - assert (target_dir / "custom-skill").exists() diff --git a/tests/test_skills_cli.py b/tests/test_skills_cli.py deleted file mode 100644 index e2afff3fe90..00000000000 --- a/tests/test_skills_cli.py +++ /dev/null @@ -1,288 +0,0 @@ -# Copyright 2020-2026 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. - -import argparse - -import pytest - -from trl.skills import install_skill -from trl.skills.cli import add_skills_subcommands, cmd_install, cmd_list, cmd_uninstall - - -class TestCLICommands: - """Tests for CLI command handlers.""" - - def test_cmd_list_without_target(self, capsys): - """Test cmd_list without target (lists TRL skills).""" - args = argparse.Namespace(target=None, scope="project") - - result = cmd_list(args) - - captured = capsys.readouterr() - assert result == 0 - assert "TRL (available for installation)" in captured.out - assert "trl-training" in captured.out - assert "Use 'trl skills install" in captured.out - - def test_cmd_list_with_target(self, tmp_path, capsys): - """Test cmd_list with target (lists installed skills).""" - # Install a skill - install_skill("trl-training", target=tmp_path) - - args = argparse.Namespace(target=str(tmp_path), scope="project") - result = cmd_list(args) - - captured = capsys.readouterr() - assert result == 0 - assert "trl-training" in captured.out - assert str(tmp_path) in captured.out - - def test_cmd_list_empty_target(self, tmp_path, capsys): - """Test cmd_list with empty target directory.""" - args = argparse.Namespace(target=str(tmp_path), scope="project") - - result = cmd_list(args) - - captured = capsys.readouterr() - assert result == 0 - assert "No skills installed" in captured.out - - def test_cmd_install_single_skill(self, tmp_path, capsys): - """Test cmd_install with single skill.""" - args = argparse.Namespace(skill="trl-training", all=False, target=str(tmp_path), scope="project", force=False) - - result = cmd_install(args) - - captured = capsys.readouterr() - assert result == 0 - assert "✓" in captured.out - assert "1/1 skills installed" in captured.out - assert (tmp_path / "trl-training").exists() - - def test_cmd_install_all_skills(self, tmp_path, capsys): - """Test cmd_install with --all flag.""" - args = argparse.Namespace(skill=None, all=True, target=str(tmp_path), scope="project", force=False) - - result = cmd_install(args) - - captured = capsys.readouterr() - assert result == 0 - assert "✓" in captured.out - assert "installed successfully" in captured.out - assert (tmp_path / "trl-training").exists() - - def test_cmd_install_no_skill_or_all(self, capsys): - """Test cmd_install without skill name or --all flag.""" - args = argparse.Namespace(skill=None, all=False, target="/tmp/test", scope="project", force=False) - - result = cmd_install(args) - - captured = capsys.readouterr() - assert result == 1 - assert "Error: Either provide a skill name or use --all" in captured.out - - def test_cmd_install_both_skill_and_all(self, capsys): - """Test cmd_install with both skill name and --all (error).""" - args = argparse.Namespace(skill="trl-training", all=True, target="/tmp/test", scope="project", force=False) - - result = cmd_install(args) - - captured = capsys.readouterr() - assert result == 1 - assert "Cannot specify both" in captured.out - - def test_cmd_install_nonexistent_skill(self, tmp_path, capsys): - """Test cmd_install with non-existent skill.""" - args = argparse.Namespace(skill="nonexistent", all=False, target=str(tmp_path), scope="project", force=False) - - result = cmd_install(args) - - captured = capsys.readouterr() - assert result == 1 - assert "✗" in captured.out - assert "0/1 skills installed" in captured.out - - def test_cmd_install_already_exists(self, tmp_path, capsys): - """Test cmd_install when skill already exists without force.""" - # Install once - install_skill("trl-training", target=tmp_path) - - args = argparse.Namespace(skill="trl-training", all=False, target=str(tmp_path), scope="project", force=False) - - result = cmd_install(args) - - captured = capsys.readouterr() - assert result == 1 - assert "✗" in captured.out - assert "Use --force to overwrite" in captured.out - - def test_cmd_install_with_force(self, tmp_path, capsys): - """Test cmd_install with --force to overwrite.""" - # Install once - install_skill("trl-training", target=tmp_path) - - args = argparse.Namespace(skill="trl-training", all=False, target=str(tmp_path), scope="project", force=True) - - result = cmd_install(args) - - captured = capsys.readouterr() - assert result == 0 - assert "✓" in captured.out - assert "1/1 skills installed" in captured.out - - def test_cmd_uninstall_success(self, tmp_path, capsys): - """Test cmd_uninstall with installed skill.""" - # Install first - install_skill("trl-training", target=tmp_path) - - args = argparse.Namespace(skill="trl-training", target=str(tmp_path), scope="project") - - result = cmd_uninstall(args) - - captured = capsys.readouterr() - assert result == 0 - assert "✓" in captured.out - assert "has been removed" in captured.out - assert not (tmp_path / "trl-training").exists() - - def test_cmd_uninstall_not_installed(self, tmp_path, capsys): - """Test cmd_uninstall when skill is not installed.""" - args = argparse.Namespace(skill="nonexistent", target=str(tmp_path), scope="project") - - result = cmd_uninstall(args) - - captured = capsys.readouterr() - assert result == 1 - assert "✗" in captured.out - assert "Error:" in captured.out - - def test_cmd_install_creates_target_directory(self, tmp_path, capsys): - """Test cmd_install creates target directory if it doesn't exist.""" - # Custom path that doesn't exist yet - target_path = tmp_path / "new_directory" - assert not target_path.exists() - - args = argparse.Namespace( - skill="trl-training", all=False, target=str(target_path), scope="project", force=False - ) - - result = cmd_install(args) - - captured = capsys.readouterr() - assert result == 0 - assert "✓" in captured.out - assert target_path.exists() - - def test_cmd_uninstall_invalid_target(self, capsys): - """Test cmd_uninstall with non-existent path.""" - args = argparse.Namespace(skill="trl-training", target="/nonexistent/invalid/path", scope="project") - - result = cmd_uninstall(args) - - captured = capsys.readouterr() - assert result == 1 - assert "✗" in captured.out - - -class TestCLIArgumentParsing: - """Tests for CLI argument parsing setup.""" - - def test_add_skills_subcommands_creates_parsers(self): - """Test that add_skills_subcommands creates the expected subparsers.""" - parser = argparse.ArgumentParser() - subparsers = parser.add_subparsers(dest="command") - - add_skills_subcommands(subparsers) - - # Test that we can parse expected commands - args = parser.parse_args(["list"]) - assert args.command == "list" - assert hasattr(args, "func") - - args = parser.parse_args(["install", "trl-training", "--target", "claude"]) - assert args.command == "install" - assert args.skill == "trl-training" - assert args.target == "claude" - - args = parser.parse_args(["uninstall", "trl-training", "--target", "claude"]) - assert args.command == "uninstall" - assert args.skill == "trl-training" - - def test_list_command_optional_target(self): - """Test that list command has optional target.""" - parser = argparse.ArgumentParser() - subparsers = parser.add_subparsers(dest="command") - add_skills_subcommands(subparsers) - - # Should work without target - args = parser.parse_args(["list"]) - assert args.target is None - - # Should work with target - args = parser.parse_args(["list", "--target", "claude"]) - assert args.target == "claude" - - def test_default_target_is_agents(self): - """Test that default target is 'agents'.""" - parser = argparse.ArgumentParser() - subparsers = parser.add_subparsers(dest="command") - add_skills_subcommands(subparsers) - - args = parser.parse_args(["install", "trl-training"]) - assert args.target == "agents" - - def test_scope_choices(self): - """Test that scope parameter accepts valid choices.""" - parser = argparse.ArgumentParser() - subparsers = parser.add_subparsers(dest="command") - add_skills_subcommands(subparsers) - - # Valid scopes - args = parser.parse_args(["install", "trl-training", "--target", "claude", "--scope", "project"]) - assert args.scope == "project" - - args = parser.parse_args(["install", "trl-training", "--target", "claude", "--scope", "global"]) - assert args.scope == "global" - - # Invalid scope should fail - with pytest.raises(SystemExit): - parser.parse_args(["install", "trl-training", "--target", "claude", "--scope", "invalid"]) - - def test_install_all_flag(self): - """Test install --all flag.""" - parser = argparse.ArgumentParser() - subparsers = parser.add_subparsers(dest="command") - add_skills_subcommands(subparsers) - - args = parser.parse_args(["install", "--all", "--target", "claude"]) - assert args.all is True - assert args.skill is None - - def test_install_force_flag(self): - """Test install --force flag.""" - parser = argparse.ArgumentParser() - subparsers = parser.add_subparsers(dest="command") - add_skills_subcommands(subparsers) - - args = parser.parse_args(["install", "trl-training", "--target", "claude", "--force"]) - assert args.force is True - - def test_default_scope_is_project(self): - """Test that default scope is 'project'.""" - parser = argparse.ArgumentParser() - subparsers = parser.add_subparsers(dest="command") - add_skills_subcommands(subparsers) - - args = parser.parse_args(["install", "trl-training", "--target", "claude"]) - assert args.scope == "project" diff --git a/trl/cli/commands/__init__.py b/trl/cli/commands/__init__.py index 4d71188aa77..8cfaa42eec6 100644 --- a/trl/cli/commands/__init__.py +++ b/trl/cli/commands/__init__.py @@ -14,7 +14,6 @@ from .base import Command from .env import EnvCommand -from .skills import SkillsCommand from .training import TrainingCommand from .vllm_serve import VllmServeCommand @@ -30,7 +29,6 @@ def get_commands() -> list[Command]: TrainingCommand("reward"), TrainingCommand("rloo"), TrainingCommand("sft"), - SkillsCommand(), VllmServeCommand(), ] diff --git a/trl/cli/commands/skills.py b/trl/cli/commands/skills.py deleted file mode 100644 index 737bf474727..00000000000 --- a/trl/cli/commands/skills.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright 2020-2026 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. - -from argparse import Namespace - -from ...skills.cli import add_skills_subcommands -from .base import Command, CommandContext - - -class SkillsCommand(Command): - """CLI command that manages TRL agent skills.""" - - def __init__(self): - super().__init__(name="skills", help_text="Manage TRL agent skills") - self._skills_parser = None - - def register(self, subparsers) -> None: - self._skills_parser = subparsers.add_parser(self.name, help=self.help_text) - skills_subparsers = self._skills_parser.add_subparsers(dest="skills_command", help="Skills commands") - add_skills_subcommands(skills_subparsers) - - def run(self, args: Namespace, context: CommandContext) -> int: - if getattr(args, "skills_command", None): - if hasattr(args, "func"): - return args.func(args) - print("Error: Unknown skills command") - return 1 - - if self._skills_parser is not None: - self._skills_parser.print_help() - return 0 diff --git a/trl/skills/__init__.py b/trl/skills/__init__.py deleted file mode 100644 index 29603ebfd06..00000000000 --- a/trl/skills/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright 2020-2026 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. - -from .skills import ( - install_skill, - list_agent_names, - list_skills, - resolve_target_path, - uninstall_skill, -) diff --git a/trl/skills/cli.py b/trl/skills/cli.py deleted file mode 100644 index 712f50d1b0b..00000000000 --- a/trl/skills/cli.py +++ /dev/null @@ -1,199 +0,0 @@ -# Copyright 2020-2026 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. - -""" -CLI commands for TRL skills installation and management. - -This module provides command-line interface for installing TRL skills to various AI agent directories. -""" - -import argparse - -from .skills import install_skill, list_agent_names, list_skills, resolve_target_path, uninstall_skill - - -def add_skills_subcommands(subparsers: argparse._SubParsersAction) -> None: - """ - Add skills subcommands to the parser. - - This creates nested subcommands under 'trl skills' for managing skill installations. - - Args: - subparsers: Subparsers from 'trl skills' command - """ - # Parent parser for common target options - target_parser = argparse.ArgumentParser(add_help=False) - target_parser.add_argument( - "--target", - default="agents", - help=f"Installation target: agent name ({', '.join(list_agent_names())}) or directory path", - ) - target_parser.add_argument( - "--scope", - choices=["project", "global"], - default="project", - help="Scope when using --target with agent name: project (./agents/skills/) or global (user-level like ~/.agents/skills/)", - ) - - # trl skills list (no target required - lists TRL's built-in skills by default) - list_parser = subparsers.add_parser( - "list", - help="List available TRL skills or installed skills in a target", - description="Show TRL skills available for installation, or if --target is specified, show installed skills", - ) - list_parser.add_argument( - "--target", - help="Optional: show installed skills in target (agent name or directory path)", - ) - list_parser.add_argument( - "--scope", - choices=["project", "global"], - default="project", - help="Scope when using --target with agent name: project (./agents/skills/) or global (user-level like ~/.agents/skills/)", - ) - list_parser.set_defaults(func=cmd_list) - - # trl skills install - install_parser = subparsers.add_parser( - "install", - parents=[target_parser], - help="Install skill", - description="Install TRL skill to target", - ) - install_parser.add_argument("skill", nargs="?", help="Skill name to install (omit to use --all)") - install_parser.add_argument("--all", action="store_true", help="Install all available TRL skills") - install_parser.add_argument("--force", action="store_true", help="Overwrite if skill already exists") - install_parser.set_defaults(func=cmd_install) - - # trl skills uninstall - uninstall_parser = subparsers.add_parser( - "uninstall", - parents=[target_parser], - help="Uninstall skill from target", - description="Remove a TRL skill from an AI agent's skills directory", - ) - uninstall_parser.add_argument("skill", help="Skill name to uninstall") - uninstall_parser.set_defaults(func=cmd_uninstall) - - -def cmd_install(args): - """Handle 'trl skills install' command.""" - # Validate arguments - if not args.skill and not args.all: - print("Error: Either provide a skill name or use --all to install all skills") - print("Usage: trl skills install --target ") - print(" or: trl skills install --all --target ") - return 1 - - if args.skill and args.all: - print("Error: Cannot specify both a skill name and --all") - return 1 - - # Determine skills to install - if args.all: - skills_to_install = list_skills() - if not skills_to_install: - print("No skills available to install") - return 1 - print(f"Installing {len(skills_to_install)} skills to {args.target}") - else: - skills_to_install = [args.skill] - - # Install each skill - success_count = 0 - for skill_name in skills_to_install: - try: - print(f"Installing '{skill_name}'...", end=" ") - install_skill( - skill_name=skill_name, - target=args.target, - scope=args.scope, - force=args.force, - ) - print("✓") - success_count += 1 - - except FileExistsError as e: - print("✗") - print(f" Error: {e}") - if not args.force: - print(" Use --force to overwrite") - except (FileNotFoundError, ValueError) as e: - print("✗") - print(f" Error: {e}") - - # Summary - print(f"\n{success_count}/{len(skills_to_install)} skills installed successfully") - - if success_count > 0: - target_path = resolve_target_path(args.target, args.scope) - print(f"\nSkills are now available at: {target_path}") - print("You may need to restart your AI agent to use the new skills.") - - return 0 if success_count == len(skills_to_install) else 1 - - -def cmd_uninstall(args): - """Handle 'trl skills uninstall' command.""" - try: - print(f"Uninstalling '{args.skill}' from {args.target}...", end=" ") - uninstall_skill(args.skill, target=args.target, scope=args.scope) - print("✓") - print(f"\nSkill '{args.skill}' has been removed") - return 0 - - except (FileNotFoundError, PermissionError, ValueError) as e: - print("✗") - print(f"Error: {e}") - return 1 - - -def cmd_list(args): - """Handle 'trl skills list' command.""" - try: - # List skills - if no target specified, list TRL's built-in skills - if args.target: - skills = list_skills(target=args.target, scope=args.scope) - location = args.target - else: - skills = list_skills() - location = "TRL (available for installation)" - - if not skills: - if args.target: - print(f"No skills installed in {args.target}") - else: - print("No TRL skills available") - return 0 - - print(f"\nSkills in {location}:\n") - - for skill in skills: - print(f" {skill}") - - print(f"\nTotal: {len(skills)} skill(s)") - - if not args.target: - print("\nUse 'trl skills install --target ' to install a skill") - - return 0 - - except ValueError as e: - print(f"Error: {e}") - return 1 - - -__all__ = [ - "add_skills_subcommands", -] diff --git a/trl/skills/skills.py b/trl/skills/skills.py deleted file mode 100644 index dc59bfc115c..00000000000 --- a/trl/skills/skills.py +++ /dev/null @@ -1,350 +0,0 @@ -# Copyright 2020-2026 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. - -""" -Agent Skills. - -This module: -- provides utilities for discovering and accessing TRL skills that can be used by AI agents to learn how to use the TRL - CLI -- handles installation, uninstallation, and management of TRL skills -- defines where different AI agents and coding tools look for skills, enabling easy installation of TRL skills to the - appropriate directories - -Agent Skills are folders of instructions, scripts, and resources that agents can discover and use to perform tasks more -accurately and efficiently. Learn more at https://agentskills.io -""" - -import importlib.resources as resources -import shutil -from pathlib import Path - - -AGENT_PATHS = { - "agents": { - "global": Path("~/.agents/skills"), - "project": Path("./.agents/skills"), - }, - "claude": { - "global": Path("~/.claude/skills"), - "project": Path("./.claude/skills"), - }, -} - - -def list_agent_names() -> list[str]: - """ - List available predefined agent names. - - Returns: - `list[str]`: Sorted list of agent names (e.g., ['agents', 'claude']). - """ - return sorted(AGENT_PATHS.keys()) - - -def _get_trl_skills_dir() -> Path: - """ - Get the path to the TRL skills directory. - - This is the directory inside the TRL package containing skills that can be installed to AI agent directories. - - Returns: - `Path`: TRL skills directory. - """ - return Path(str(resources.files("trl.skills"))) - - -def resolve_target_path(target: str | Path, scope: str = "project") -> Path: - """ - Resolve target to a concrete directory path. - - Converts semantic agent names (e.g., 'claude') with scope to actual filesystem paths, or normalizes provided paths. - - Args: - target (`str | Path`): Agent name (e.g., 'agents', 'claude') or directory path. - scope (`str`, defaults to `"project"`): - Scope for agent names: 'global' (user-level like ~/.agents/skills/) or 'project' (./agents/skills/). - - Returns: - `Path`: Resolved absolute path. - - Raises: - `ValueError`: If `scope` is invalid for a predefined agent target. - - Example: - ```python - >>> from trl.skills import resolve_target_path - - >>> # Resolve agent name with scope - >>> resolve_target_path("claude", "global") - /home/user/.claude/skills - - >>> # Resolve custom path - >>> resolve_target_path("/custom/skills") - /custom/skills - ``` - """ - if isinstance(target, Path): - return target.expanduser().resolve() - - # Check if it's a predefined agent - if target in AGENT_PATHS: - if scope not in AGENT_PATHS[target]: - valid_scopes = ", ".join(sorted(AGENT_PATHS[target])) - raise ValueError(f"Invalid scope '{scope}' for agent '{target}'. Expected one of: {valid_scopes}") - agent_path = AGENT_PATHS[target][scope] - return agent_path.expanduser().resolve() - - # Treat as custom path string - return Path(target).expanduser().resolve() - - -def _list_skills_in_dir(skills_dir: Path) -> list[str]: - """ - List skills in directory. - - A skill is a directory containing a SKILL.md file. - - Args: - skills_dir (`Path`): Skills directory to scan. - - Returns: - `list[str]`: Skill names (directory names containing SKILL.md). - """ - if not skills_dir.exists(): - return [] - skills = [] - for item in skills_dir.iterdir(): - if item.is_dir() and (item / "SKILL.md").exists(): - skills.append(item.name) - return sorted(skills) - - -def list_skills(target: str | Path | None = None, scope: str = "project") -> list[str]: - """ - List skills. - - A skill is a directory containing a SKILL.md file. - - Args: - target (`str | Path`, *optional*): - Agent name (e.g., 'claude'), directory path, or `None` for TRL's built-in skills. - scope (`str`, defaults to `"project"`): - For agent names: 'global' (user-level) or 'project' (current directory). - - Returns: - `list[str]`: Skill names (directory names containing SKILL.md). - - Example: - ```python - >>> from trl.skills import list_skills - - >>> # List TRL's built-in skills - >>> list_skills() - ['trl-training'] - - >>> # List skills installed for Claude globally - >>> list_skills(target="claude", scope="global") - ['trl-training', 'custom-skill'] - - >>> # List skills in custom directory - >>> list_skills(target="/path/to/skills") - [...] - ``` - """ - if target is None: - # List TRL's built-in skills - return _list_skills_in_dir(_get_trl_skills_dir()) - - target_dir = resolve_target_path(target, scope) - return _list_skills_in_dir(target_dir) - - -def _install_skill_to_dir( - skill_name: str, - target_dir: Path, - source_dir: Path, - force: bool = False, -) -> bool: - """ - Install a skill to target directory. - - Args: - skill_name (`str`): Name of skill to install. - target_dir (`Path`): Target installation directory. - source_dir (`Path`): Source directory containing skills. - force (`bool`, defaults to `False`): Whether to overwrite if exists. - - Returns: - `bool`: True if installed successfully. - - Raises: - - `FileNotFoundError`: If skill doesn't exist in source_dir. - - `FileExistsError`: If skill already installed and not force. - - `PermissionError`: If no permission to write to target_dir. - - `ValueError`: If source_dir entry exists but is not a directory. - - `OSError`: If copying the skill fails. - """ - source_skill = source_dir / skill_name - - # Check if source skill exists - if not source_skill.exists(): - available = ", ".join(list_skills(target=source_dir)) - source_msg = f"source directory {source_dir}" - if available: - raise FileNotFoundError(f"Skill '{skill_name}' not found in {source_msg}. Available skills: {available}") - raise FileNotFoundError(f"Skill '{skill_name}' not found in {source_msg}") - - if not source_skill.is_dir(): - raise ValueError(f"Skill '{skill_name}' is not a directory") - - target_skill = target_dir / skill_name - - # Check if already exists - if target_skill.exists() and not force: - raise FileExistsError(f"Skill '{skill_name}' already installed at {target_skill}. Use --force to overwrite.") - - # Create target directory - try: - target_dir.mkdir(parents=True, exist_ok=True) - except PermissionError as e: - raise PermissionError(f"Cannot create directory {target_dir}: {e}") from e - - # Remove existing if force - if target_skill.exists() and force: - shutil.rmtree(target_skill) - - # Install - try: - shutil.copytree(source_skill, target_skill) - except OSError as e: - raise OSError(f"Failed to install skill: {e}") from e - - return True - - -def install_skill( - skill_name: str, - target: str | Path, - scope: str = "project", - source: str | Path | None = None, - force: bool = False, -) -> bool: - """ - Install a skill. - - Args: - skill_name (`str`): Name of skill to install. - target (`str | Path`): Agent name (e.g., 'agents', 'claude') or directory path. - scope (`str`, defaults to `"project"`): - Scope for agent names: 'global' (user-level) or 'project' (current directory). - source (`str | Path`, *optional*): - Source directory containing skills. If `None`, defaults to TRL skills directory. - force (`bool`, defaults to `False`): Whether to overwrite if skill already exists. - - Returns: - `bool`: True if installed successfully. - - Raises: - - `FileNotFoundError`: If skill doesn't exist in source. - - `FileExistsError`: If skill already installed and not force. - - `PermissionError`: If no permission to write to target. - - `ValueError`: - - If `scope` is invalid for a predefined agent target. - - If `source` entry exists but is not a directory. - - `OSError`: If copying the skill fails. - - Example: - ```python - >>> from trl.skills import install_skill - - >>> # Install to Claude's global skills directory - >>> install_skill("trl-training", target="claude", scope="global") - - >>> # Install to custom directory - >>> install_skill("trl-training", target="/path/to/skills") - - >>> # Overwrite existing installation - >>> install_skill("trl-training", target="claude", force=True) - ``` - """ - target_dir = resolve_target_path(target, scope) - source_dir = Path(source).expanduser().resolve() if source else _get_trl_skills_dir() - return _install_skill_to_dir(skill_name, target_dir, source_dir, force) - - -def _uninstall_skill_from_dir(skill_name: str, target_dir: Path) -> bool: - """ - Uninstall a skill from target directory. - - Args: - skill_name (`str`): Name of skill to uninstall. - target_dir (`Path`): Directory skill is installed in. - - Returns: - `bool`: True if uninstalled successfully. - - Raises: - - `FileNotFoundError`: If skill not installed. - - `PermissionError`: If no permission to remove. - - `OSError`: If removing the skill fails for another filesystem reason. - """ - target_skill = target_dir / skill_name - - if not target_skill.exists(): - raise FileNotFoundError(f"Skill '{skill_name}' not installed at {target_dir}") - - # Remove directory - try: - shutil.rmtree(target_skill) - except PermissionError as e: - raise PermissionError(f"Cannot remove skill: {e}") from e - except OSError as e: - raise OSError(f"Failed to remove skill: {e}") from e - - return True - - -def uninstall_skill(skill_name: str, target: str | Path, scope: str = "project") -> bool: - """ - Uninstall a skill. - - Args: - skill_name (`str`): Name of skill to uninstall. - target (`str | Path`): Agent name (e.g., 'agents', 'claude') or directory path. - scope (`str`, defaults to `"project"`): - Scope for agent names: 'global' (user-level) or 'project' (current directory). - - Returns: - `bool`: True if uninstalled successfully. - - Raises: - - `FileNotFoundError`: If skill not installed. - - `PermissionError`: If no permission to remove. - - `OSError`: If removing the skill fails for another filesystem reason. - - `ValueError`: If `scope` is invalid for a predefined agent target. - - Example: - ```python - >>> from trl.skills import uninstall_skill - - >>> # Uninstall from Claude's global directory - >>> uninstall_skill("trl-training", target="claude", scope="global") - - >>> # Uninstall from custom directory - >>> uninstall_skill("trl-training", target="/path/to/skills") - ``` - """ - target_dir = resolve_target_path(target, scope) - return _uninstall_skill_from_dir(skill_name, target_dir)