diff --git a/README.md b/README.md index 8c016ee..811e384 100644 --- a/README.md +++ b/README.md @@ -106,12 +106,20 @@ parallel-cli │ ├── cancel # Cancel a monitor (irreversible) │ ├── events # List events for a monitor │ └── trigger # Trigger an immediate one-off run +├── skills # Install, update, and manage Parallel agent skills +│ ├── install # Install or update managed Parallel skills +│ ├── list # List available Parallel skills +│ ├── reinstall # Cleanly reinstall managed Parallel skills +│ └── uninstall # Remove only CLI-managed Parallel skills └── memory # Saved Task, Monitor, and FindAll entries ├── retrieve # Search Memory or list recent entries ├── evict # Remove one entry from Memory └── clear # Permanently clear selected Memory ``` +Run `parallel-cli skills install` to install or update agent skills. When using +`--skill`, other managed skills are removed; unmanaged skills are preserved. + ## Quick Start ### 1. Authenticate diff --git a/parallel_web_tools/cli/skills.py b/parallel_web_tools/cli/skills.py index 6b469f4..f0febee 100644 --- a/parallel_web_tools/cli/skills.py +++ b/parallel_web_tools/cli/skills.py @@ -33,7 +33,9 @@ def create_skills_group( @click.group(name="skills") def skills() -> None: - """Install and manage Parallel agent skills. + """Install, update, and manage Parallel agent skills. + + Run parallel-cli skills install to install or update managed skills. Downloads come from skills.parallel.ai. Set PARALLEL_SKILLS_INDEX_URL to use a custom index. @@ -93,14 +95,14 @@ def skills_list(output_json: bool) -> None: "--skill", "skill_names", multiple=True, - help="Skill name to install (repeatable). Defaults to all. Skills not listed will be removed.", + help="Skill name to install (repeatable). Defaults to all. Other managed skills will be removed.", ) @click.option("--json", "output_json", is_flag=True, help="Output as JSON") def skills_install(project: bool, skill_names: tuple[str, ...], output_json: bool) -> None: - """Install Parallel skills from skills.parallel.ai. + """Install or update Parallel skills from skills.parallel.ai. When --skill is provided, the managed install set is replaced with exactly - the listed skills. + the listed skills. Unmanaged skills are never overwritten or removed. """ from parallel_web_tools.core.skills import ( SkillsError, @@ -178,7 +180,7 @@ def skills_uninstall(project: bool, output_json: bool) -> None: "--skill", "skill_names", multiple=True, - help="Skill name to reinstall (repeatable). Defaults to all. Skills not listed will be removed.", + help="Skill name to reinstall (repeatable). Defaults to all. Other managed skills will be removed.", ) @click.option("--json", "output_json", is_flag=True, help="Output as JSON") def skills_reinstall(project: bool, skill_names: tuple[str, ...], output_json: bool) -> None: diff --git a/tests/test_cli.py b/tests/test_cli.py index f1b39ed..b4c37f2 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2432,18 +2432,27 @@ def test_enrich_run_wait_output_file(self, runner, tmp_path): class TestSkillsCommands: - def test_skills_help_mentions_cdn_and_replacement_behavior(self, runner): + def test_skills_help_mentions_updates_cdn_and_managed_replacement(self, runner): result = runner.invoke(main, ["skills", "--help"]) assert result.exit_code == 0 + help_text = " ".join(result.output.split()) + assert "parallel-cli skills install to install or update managed skills" in help_text assert "skills.parallel.ai" in result.output assert "PARALLEL_SKILLS_INDEX_URL" in result.output result = runner.invoke(main, ["skills", "install", "--help"]) assert result.exit_code == 0 - assert "Skills not" in result.output - assert "listed will be removed" in result.output + help_text = " ".join(result.output.split()) + assert "Install or update Parallel skills" in help_text + assert "Unmanaged skills are never overwritten or removed" in help_text + assert "Other managed skills will be removed" in help_text + + result = runner.invoke(main, ["skills", "reinstall", "--help"]) + + assert result.exit_code == 0 + assert "Other managed skills will be removed" in " ".join(result.output.split()) def test_skills_list_json(self, runner): with (