Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 36 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,19 @@ parallel-cli
│ ├── extend # Request additional candidates for a run
│ ├── schema # Get the schema for a FindAll run
│ └── cancel # Cancel a running FindAll
└── monitor # Continuous web change tracking
├── create # Create a new web monitor (event_stream or snapshot)
├── list # List monitors (cursor paginated)
├── get # Get monitor details
├── update # Update frequency, webhook, metadata
├── cancel # Cancel a monitor (irreversible)
├── events # List events for a monitor
└── trigger # Trigger an immediate one-off run
├── monitor # Continuous web change tracking
│ ├── create # Create a new web monitor (event_stream or snapshot)
│ ├── list # List monitors (cursor paginated)
│ ├── get # Get monitor details
│ ├── update # Update frequency, webhook, metadata
│ ├── cancel # Cancel a monitor (irreversible)
│ ├── events # List events for a monitor
│ └── trigger # Trigger an immediate one-off run
└── skills # Install and manage Parallel agent skills
├── list # List available skills from skills.parallel.ai
├── install # Install skills for your coding agents
├── reinstall # Reinstall the managed skill set
└── uninstall # Remove skills installed by parallel-cli
```

## Quick Start
Expand Down Expand Up @@ -331,6 +336,29 @@ result = findall_table(
result.result.show()
```

## Agent Skills

Install Parallel's agent skills into your coding agent:

```bash
parallel-cli skills list # See what's available
parallel-cli skills install # Install all skills globally
parallel-cli skills install --skill parallel-web-search
parallel-cli skills install --project # Install into the current project instead
```

Skills install into `~/.agents/skills` (or `<project>/.agents/skills` with `--project`), the
cross-agent location read by Gemini CLI, Copilot, Codex and Amp. Claude Code only scans its own
tree, so the installer also links each skill into `~/.claude/skills` (or `<project>/.claude/skills`)
when a `.claude` directory is present. Links are symlinks to the single canonical copy, so there is
nothing to keep in sync; on systems where symlinks need privileges the skill folder is copied
instead. An existing skill of the same name is never overwritten — the CLI reports it and moves on.
Restart Claude Code after installing to pick up new skills.

The CLI installs loose skills, so each one is invoked by its folder name — `/parallel-web-search`,
not `/parallel:parallel-web-search`. The namespaced form belongs to the Claude Code plugin, which is
a separate install channel from this CLI.

## Programmatic Usage

```python
Expand Down
71 changes: 69 additions & 2 deletions parallel_web_tools/cli/skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from __future__ import annotations

import json
from typing import NoReturn, Protocol
from pathlib import Path
from typing import Any, NoReturn, Protocol

import click
from rich.console import Console
Expand All @@ -19,6 +20,39 @@ def __call__(
) -> NoReturn: ...


def _project_root_for(install_dir: str, project: bool) -> Path | None:
"""Return the project root a project-local install dir sits in.

Project installs land in ``<root>/.agents/skills``; global installs have no
project root, so Claude Code's global tree is used instead.
"""
if not project:
return None
return Path(install_dir).parent.parent


def _print_skill_commands(console: Console, skill_names: list[str]) -> None:
"""Print how to invoke the installed skills.

Skills installed by the CLI are loose (not bundled in a Claude Code plugin),
so their slash command is the un-namespaced folder name.
"""
if not skill_names:
return
commands = ", ".join(f"/{name}" for name in skill_names)
console.print(f"Skill commands: [cyan]{commands}[/cyan]")


def _print_claude_code_result(console: Console, claude_code: dict[str, Any]) -> None:
if claude_code["linked"] or claude_code["copied"]:
console.print(
f"Linked into Claude Code ([cyan]{claude_code['claude_skills_dir']}[/cyan]); "
"restart Claude Code to pick up the new skills."
)
for warning in claude_code["warnings"]:
console.print(f"[yellow]{warning}[/yellow]")


def create_skills_group(
console: Console,
handle_error: HandleError,
Expand Down Expand Up @@ -86,6 +120,7 @@ def skills_install(project: bool, skill_names: tuple[str, ...], output_json: boo
SkillsInputError,
SkillsInstallLocationError,
install_skills,
link_into_claude_code,
resolve_install_dir,
)

Expand All @@ -104,6 +139,13 @@ def skills_install(project: bool, skill_names: tuple[str, ...], output_json: boo
except Exception as e:
handle_error(e, output_json=output_json, exit_code=exit_api_error, prefix="Skills install failed")

# Make the freshly installed skills visible to Claude Code (best-effort).
result["claude_code"] = link_into_claude_code(
Path(str(result["install_dir"])),
list(result["installed_skills"]),
project_root=_project_root_for(str(result["install_dir"]), project),
)

if output_json:
print(json.dumps(result, indent=2))
return
Expand All @@ -112,6 +154,8 @@ def skills_install(project: bool, skill_names: tuple[str, ...], output_json: boo
console.print(f"Location: [cyan]{result['install_dir']}[/cyan]")
console.print(f"Ref: [cyan]{result['ref']}[/cyan]")
console.print(f"Installed ({result['count']}): [cyan]{', '.join(result['installed_skills'])}[/cyan]")
_print_skill_commands(console, list(result["installed_skills"]))
_print_claude_code_result(console, result["claude_code"])

@skills.command(name="uninstall")
@click.option(
Expand All @@ -122,7 +166,12 @@ def skills_install(project: bool, skill_names: tuple[str, ...], output_json: boo
@click.option("--json", "output_json", is_flag=True, help="Output as JSON")
def skills_uninstall(project: bool, output_json: bool) -> None:
"""Uninstall skills previously installed by parallel-cli."""
from parallel_web_tools.core.skills import SkillsInstallLocationError, resolve_install_dir, uninstall_skills
from parallel_web_tools.core.skills import (
SkillsInstallLocationError,
resolve_install_dir,
uninstall_skills,
unlink_from_claude_code,
)

try:
install_dir = resolve_install_dir(project=project)
Expand All @@ -132,6 +181,13 @@ def skills_uninstall(project: bool, output_json: bool) -> None:
except Exception as e:
handle_error(e, output_json=output_json, exit_code=exit_api_error, prefix="Skills uninstall failed")

# Drop the Claude Code links pointing at the skills we just removed.
result["claude_code"] = unlink_from_claude_code(
Path(str(result["install_dir"])),
list(result["removed_skills"]),
project_root=_project_root_for(str(result["install_dir"]), project),
)

if output_json:
print(json.dumps(result, indent=2))
return
Expand All @@ -144,6 +200,8 @@ def skills_uninstall(project: bool, output_json: bool) -> None:
console.print("[bold green]Skills uninstalled[/bold green]")
console.print(f"Location: [cyan]{result['install_dir']}[/cyan]")
console.print(f"Removed ({result['count']}): [cyan]{', '.join(result['removed_skills'])}[/cyan]")
for warning in result["claude_code"]["warnings"]:
console.print(f"[yellow]{warning}[/yellow]")

@skills.command(name="reinstall")
@click.option(
Expand All @@ -168,6 +226,7 @@ def skills_reinstall(project: bool, skill_names: tuple[str, ...], output_json: b
SkillsError,
SkillsInputError,
SkillsInstallLocationError,
link_into_claude_code,
reinstall_skills,
resolve_install_dir,
)
Expand All @@ -187,6 +246,12 @@ def skills_reinstall(project: bool, skill_names: tuple[str, ...], output_json: b
except Exception as e:
handle_error(e, output_json=output_json, exit_code=exit_api_error, prefix="Skills reinstall failed")

result["claude_code"] = link_into_claude_code(
Path(str(result["install_dir"])),
list(result["installed_skills"]),
project_root=_project_root_for(str(result["install_dir"]), project),
)

if output_json:
print(json.dumps(result, indent=2))
return
Expand All @@ -196,5 +261,7 @@ def skills_reinstall(project: bool, skill_names: tuple[str, ...], output_json: b
console.print(f"Ref: [cyan]{result['ref']}[/cyan]")
console.print(f"Removed ({result['removed_count']}): [cyan]{', '.join(result['removed_skills'])}[/cyan]")
console.print(f"Installed ({result['installed_count']}): [cyan]{', '.join(result['installed_skills'])}[/cyan]")
_print_skill_commands(console, list(result["installed_skills"]))
_print_claude_code_result(console, result["claude_code"])

return skills
Loading
Loading