SKILL.md
"""CLI commands for skill management.
These commands are registered with the CLI via cli.py:
- deepagents skills list --agent <agent> [--project]
- deepagents skills create <name>
- deepagents skills info <name>
"""
import argparse import re from pathlib import Path from typing import Any
from deepagentscli.config import COLORS, Settings, console from deepagentscli.skills.load import list_skills
MAXSKILLNAME_LENGTH = 64
def validatename(name: str) -> tuple[bool, str]: """Validate name per Agent Skills spec.
Requirements (https://agentskills.io/specification): - Max 64 characters - Lowercase alphanumeric and hyphens only (a-z, 0-9, -) - Cannot start or end with hyphen - No consecutive hyphens - No path traversal sequences
Args: name: The name to validate
Returns: Tuple of (isvalid, errormessage). If valid, error_message is empty. """ # Check for empty or whitespace-only names if not name or not name.strip(): return False, "cannot be empty"
# Check length (spec: max 64 chars) if len(name) > MAXSKILLNAME_LENGTH: return False, "cannot exceed 64 characters"
# Check for path traversal sequences if ".." in name or "/" in name or "\\" in name: return False, "cannot contain path components"
# Spec: lowercase alphanumeric and hyphens only # Pattern ensures: no start/end hyphen, no consecutive hyphens if not re.match(r"^[a-z0-9]+(-[a-z0-9]+)*$", name): return ( False, "must be lowercase letters, numbers, and hyphens only " "(no uppercase, no underscores, cannot start/end with hyphen)", )
return True, ""
def validateskillpath(skilldir: Path, base_dir: Path) -> tuple[bool, str]: """Validate that the resolved skill directory is within the base directory.
Args: skilldir: The skill directory path to validate basedir: The base skills directory that should contain skill_dir
Returns: Tuple of (isvalid, errormessage). If valid, errormessage is empty. """ try: # Resolve both paths to their canonical form resolvedskill = skilldir.resolve() resolvedbase = base_dir.resolve()
# Check if skilldir is within basedir # Use isrelativeto if available (Python 3.9+), otherwise use string comparison if hasattr(resolvedskill, "isrelativeto"): if not resolvedskill.isrelativeto(resolvedbase): return False, f"Skill directory must be within {basedir}" else: # Fallback for older Python versions try: resolvedskill.relativeto(resolvedbase) except ValueError: return False, f"Skill directory must be within {basedir}"
return True, "" except (OSError, RuntimeError) as e: return False, f"Invalid path: {e}"
def _list(agent: str, *, project: bool = False) -> None: """List all available skills for the specified agent.
Args: agent: Agent identifier for skills (default: agent). project: If True, show only project skills. If False, show all skills (user + project). """ settings = Settings.fromenvironment() userskillsdir = settings.getuserskillsdir(agent) projectskillsdir = settings.getprojectskills_dir()
# If --project flag is used, only show project skills if project: if not projectskillsdir: console.print("[yellow]Not in a project directory.[/yellow]") console.print( "[dim]Project skills require a .git directory in the project root.[/dim]", style=COLORS["dim"], ) return
if not projectskillsdir.exists() or not any(projectskillsdir.iterdir()): console.print("[yellow]No project skills found.[/yellow]") console.print( f"[dim]Project skills will be created in {projectskillsdir}/ when you add them.[/dim]", style=COLORS["dim"], ) console.print( "\n[dim]Create a project skill:\n deepagents skills create my-skill --project[/dim]", style=COLORS["dim"], ) return
skills = listskills(userskillsdir=None, projectskillsdir=projectskillsdir) console.print("\n[bold]Project Skills:[/bold]\n", style=COLORS["primary"]) else: # Load both user and project skills skills = listskills(userskillsdir=userskillsdir, projectskillsdir=projectskillsdir)
if not skills: console.print("[yellow]No skills found.[/yellow]") console.print( "[dim]Skills will be created in ~/.deepagents/agent/skills/ when you add them.[/dim]", style=COLORS["dim"], ) console.print( "\n[dim]Create your first skill:\n deepagents skills create my-skill[/dim]", style=COLORS["dim"], ) return
console.print("\n[bold]Available Skills:[/bold]\n", style=COLORS["primary"])
# Group skills by source userskills = [s for s in skills if s["source"] == "user"] projectskills_list = [s for s in skills if s["source"] == "project"]
# Show user skills if userskills and not project: console.print("[bold cyan]User Skills:[/bold cyan]", style=COLORS["primary"]) for skill in userskills: skillpath = Path(skill["path"]) console.print(f" • [bold]{skill['name']}[/bold]", style=COLORS["primary"]) console.print(f" {skill['description']}", style=COLORS["dim"]) console.print(f" Location: {skillpath.parent}/", style=COLORS["dim"]) console.print()
# Show project skills if projectskillslist: if not project and userskills: console.print() console.print("[bold green]Project Skills:[/bold green]", style=COLORS["primary"]) for skill in projectskillslist: skillpath = Path(skill["path"]) console.print(f" • [bold]{skill['name']}[/bold]", style=COLORS["primary"]) console.print(f" {skill['description']}", style=COLORS["dim"]) console.print(f" Location: {skill_path.parent}/", style=COLORS["dim"]) console.print()
def create(skillname: str, agent: str, project: bool = False) -> None: """Create a new skill with a template SKILL.md file.
Args: skillname: Name of the skill to create. agent: Agent identifier for skills project: If True, create in project skills directory. If False, create in user skills directory. """ # Validate skill name first (per Agent Skills spec) isvalid, errormsg = validatename(skillname) if not isvalid: console.print(f"[bold red]Error:[/bold red] Invalid skill name: {errormsg}") console.print( "[dim]Per Agent Skills spec: names must be lowercase alphanumeric with hyphens only.\n" "Examples: web-research, code-review, data-analysis[/dim]", style=COLORS["dim"], ) return
# Determine target directory settings = Settings.fromenvironment() if project: if not settings.projectroot: console.print("[bold red]Error:[/bold red] Not in a project directory.") console.print( "[dim]Project skills require a .git directory in the project root.[/dim]", style=COLORS["dim"], ) return skillsdir = settings.ensureprojectskillsdir() else: skillsdir = settings.ensureuserskillsdir(agent)
skilldir = skillsdir / skill_name
# Validate the resolved path is within skillsdir isvalidpath, patherror = validateskillpath(skilldir, skillsdir) if not isvalidpath: console.print(f"[bold red]Error:[/bold red] {patherror}") return
if skilldir.exists(): console.print( f"[bold red]Error:[/bold red] Skill '{skillname}' already exists at {skill_dir}" ) return
# Create skill directory skilldir.mkdir(parents=True, existok=True)
# Create template SKILL.md (per Agent Skills spec: https://agentskills.io/specification) template = f"""--- name: {skill_name} description: Brief description of what this skill does and when to use it.
Optional fields per Agent Skills spec:
license: Apache-2.0
compatibility: Designed for deepagents CLI
metadata:
author: your-org
version: "1.0"
allowed-tools: Bash(git:*) Read
{skill_name.title().replace("-", " ")} Skill
Description
[Provide a detailed explanation of what this skill does and when it should be used]
When to Use
- [Scenario 1: When the user asks...]
- [Scenario 2: When you need to...]
- [Scenario 3: When the task involves...]
How to Use
Step 1: [First Action]
[Explain what to do first]
Step 2: [Second Action]
[Explain what to do next]
Step 3: [Final Action]
[Explain how to complete the task]
Best Practices
- [Best practice 1]
- [Best practice 2]
- [Best practice 3]
Supporting Files
This skill directory can include supporting files referenced in the instructions:
helper.py- Python scripts for automationconfig.json- Configuration filesreference.md- Additional reference documentation
Examples
Example 1: [Scenario Name]
User Request: "[Example user request]"
Approach:
- [Step-by-step breakdown]
- [Using tools and commands]
- [Expected outcome]
Example 2: [Another Scenario]
User Request: "[Another example]"
Approach:
- [Different approach]
- [Relevant commands]
- [Expected result]
Notes
- [Additional tips, warnings, or context]
- [Known limitations or edge cases]
- [Links to external resources if helpful]
"""
skillmd = skilldir / "SKILL.md" skillmd.writetext(template)
console.print(f"✓ Skill '{skillname}' created successfully!", style=COLORS["primary"]) console.print(f"Location: {skilldir}\n", style=COLORS["dim"]) console.print( "[dim]Edit the SKILL.md file to customize:\n" " 1. Update the description in YAML frontmatter\n" " 2. Fill in the instructions and examples\n" " 3. Add any supporting files (scripts, configs, etc.)\n" "\n" f" nano {skill_md}\n" "\n" "💡 See examples/skills/ in the deepagents repo for example skills:\n" " - web-research: Structured research workflow\n" " - langgraph-docs: LangGraph documentation lookup\n" "\n" " Copy an example: cp -r examples/skills/web-research ~/.deepagents/agent/skills/\n", style=COLORS["dim"], )
def info(skillname: str, *, agent: str = "agent", project: bool = False) -> None: """Show detailed information about a specific skill.
Args: skillname: Name of the skill to show info for. agent: Agent identifier for skills (default: agent). project: If True, only search in project skills. If False, search in both user and project skills. """ settings = Settings.fromenvironment() userskillsdir = settings.getuserskillsdir(agent) projectskillsdir = settings.getprojectskillsdir()
# Load skills based on --project flag if project: if not projectskillsdir: console.print("[bold red]Error:[/bold red] Not in a project directory.") return skills = listskills(userskillsdir=None, projectskillsdir=projectskillsdir) else: skills = listskills(userskillsdir=userskillsdir, projectskillsdir=projectskillsdir)
# Find the skill skill = next((s for s in skills if s["name"] == skill_name), None)
if not skill: console.print(f"[bold red]Error:[/bold red] Skill '{skill_name}' not found.") console.print("\n[dim]Available skills:[/dim]", style=COLORS["dim"]) for s in skills: console.print(f" - {s['name']}", style=COLORS["dim"]) return
# Read the full SKILL.md file skillpath = Path(skill["path"]) skillcontent = skillpath.readtext()
# Determine source label sourcelabel = "Project Skill" if skill["source"] == "project" else "User Skill" sourcecolor = "green" if skill["source"] == "project" else "cyan"
console.print( f"\n[bold]Skill: {skill['name']}[/bold] [bold {sourcecolor}]({sourcelabel})[/bold {sourcecolor}]\n", style=COLORS["primary"], ) console.print(f"[bold]Description:[/bold] {skill['description']}\n", style=COLORS["dim"]) console.print(f"[bold]Location:[/bold] {skillpath.parent}/\n", style=COLORS["dim"])
# List supporting files skilldir = skillpath.parent supportingfiles = [f for f in skilldir.iterdir() if f.name != "SKILL.md"]
if supportingfiles: console.print("[bold]Supporting Files:[/bold]", style=COLORS["dim"]) for file in supportingfiles: console.print(f" - {file.name}", style=COLORS["dim"]) console.print()
# Show the full SKILL.md content console.print("[bold]Full SKILL.md Content:[/bold]\n", style=COLORS["primary"]) console.print(skill_content, style=COLORS["dim"]) console.print()
def setupskillsparser( subparsers: Any, ) -> argparse.ArgumentParser: """Setup the skills subcommand parser with all its subcommands.""" skillsparser = subparsers.addparser( "skills", help="Manage agent skills", description="Manage agent skills - create, list, and view skill information", ) skillssubparsers = skillsparser.addsubparsers(dest="skillscommand", help="Skills command")
# Skills list listparser = skillssubparsers.addparser( "list", help="List all available skills", description="List all available skills" ) listparser.addargument( "--agent", default="agent", help="Agent identifier for skills (default: agent)", ) listparser.addargument( "--project", action="storetrue", help="Show only project-level skills", )
# Skills create createparser = skillssubparsers.addparser( "create", help="Create a new skill", description="Create a new skill with a template SKILL.md file", ) createparser.addargument("name", help="Name of the skill to create (e.g., web-research)") createparser.addargument( "--agent", default="agent", help="Agent identifier for skills (default: agent)", ) createparser.addargument( "--project", action="storetrue", help="Create skill in project directory instead of user directory", )
# Skills info infoparser = skillssubparsers.addparser( "info", help="Show detailed information about a skill", description="Show detailed information about a specific skill", ) infoparser.addargument("name", help="Name of the skill to show info for") infoparser.addargument( "--agent", default="agent", help="Agent identifier for skills (default: agent)", ) infoparser.addargument( "--project", action="storetrue", help="Search only in project skills", ) return skills_parser
def executeskillscommand(args: argparse.Namespace) -> None: """Execute skills subcommands based on parsed arguments.
Args: args: Parsed command line arguments with skillscommand attribute """ # validate agent argument if args.agent: isvalid, errormsg = validatename(args.agent) if not isvalid: console.print(f"[bold red]Error:[/bold red] Invalid agent name: {error_msg}") console.print( "[dim]Agent names must only contain letters, numbers, hyphens, and underscores.[/dim]", style=COLORS["dim"], ) return
if args.skillscommand == "list": list(agent=args.agent, project=args.project) elif args.skillscommand == "create": create(args.name, agent=args.agent, project=args.project) elif args.skillscommand == "info": info(args.name, agent=args.agent, project=args.project) else: # No subcommand provided, show help console.print("[yellow]Please specify a skills subcommand: list, create, or info[/yellow]") console.print("\n[bold]Usage:[/bold]", style=COLORS["primary"]) console.print(" deepagents skills <command> [options]\n") console.print("[bold]Available commands:[/bold]", style=COLORS["primary"]) console.print(" list List all available skills") console.print(" create <name> Create a new skill") console.print(" info <name> Show detailed information about a skill") console.print("\n[bold]Examples:[/bold]", style=COLORS["primary"]) console.print(" deepagents skills list") console.print(" deepagents skills create web-research") console.print(" deepagents skills info web-research") console.print("\n[dim]For more help on a specific command:[/dim]", style=COLORS["dim"]) console.print(" deepagents skills <command> --help", style=COLORS["dim"])
all = [ "executeskillscommand", "setupskillsparser", ]