""" SKILL.md Generator for CLI-Anything This module extracts metadata from CLI-Anything harnesses and generates SKILL.md files following the skill-creator methodology. The generated SKILL.md files contain: - YAML frontmatter with name and description (triggering metadata) - Markdown body with usage instructions - Command documentation - Examples for AI agents """ import re from pathlib import Path from typing import Optional from dataclasses import dataclass, field def _format_display_name(name: str) -> str: """Format software name for display (replace underscores/hyphens with spaces, then title).""" return name.replace("_", " ").replace("-", " ").title() @dataclass class CommandInfo: """Information about a CLI command.""" name: str description: str @dataclass class CommandGroup: """A group of related CLI commands.""" name: str description: str commands: list[CommandInfo] = field(default_factory=list) @dataclass class Example: """An example of CLI usage.""" title: str description: str code: str @dataclass class SkillMetadata: """Metadata extracted from a CLI-Anything harness.""" skill_name: str skill_description: str software_name: str skill_intro: str version: str system_package: Optional[str] = None command_groups: list[CommandGroup] = field(default_factory=list) examples: list[Example] = field(default_factory=list) def extract_cli_metadata(harness_path: str) -> SkillMetadata: """ Extract metadata from a CLI-Anything harness directory. Args: harness_path: Path to the agent-harness directory Returns: SkillMetadata containing extracted information """ harness_path = Path(harness_path) # Find the cli_anything/ directory cli_anything_dir = harness_path / "cli_anything" if not cli_anything_dir.exists(): raise ValueError( f"cli_anything directory not found in {harness_path}. " "Ensure the harness structure includes cli_anything//" ) software_dirs = [d for d in cli_anything_dir.iterdir() if d.is_dir() and (d / "__init__.py").exists()] if not software_dirs: raise ValueError(f"No CLI package found in {harness_path}") software_dir = software_dirs[0] software_name = software_dir.name # Extract metadata from README.md readme_path = software_dir / "README.md" skill_intro = "" system_package = None if readme_path.exists(): readme_content = readme_path.read_text(encoding="utf-8") skill_intro = extract_intro_from_readme(readme_content) system_package = extract_system_package(readme_content) # Extract version from setup.py setup_path = harness_path / "setup.py" version = "1.0.0" if setup_path.exists(): version = extract_version_from_setup(setup_path) # Extract commands from CLI file cli_file = software_dir / f"{software_name}_cli.py" command_groups = [] if cli_file.exists(): command_groups = extract_commands_from_cli(cli_file) # Generate examples based on software type examples = generate_examples(software_name, command_groups) # Build skill name and description skill_name = f"cli-anything-{software_name}" skill_description = f"Command-line interface for {_format_display_name(software_name)} - {skill_intro[:100]}..." return SkillMetadata( skill_name=skill_name, skill_description=skill_description, software_name=software_name, skill_intro=skill_intro, version=version, system_package=system_package, command_groups=command_groups, examples=examples ) def extract_intro_from_readme(content: str) -> str: """Extract introduction text from README content.""" # Find the first paragraph after the title lines = content.split("\n") intro_lines = [] in_intro = False for line in lines: line = line.strip() if not line: if in_intro and intro_lines: break continue if line.startswith("# "): in_intro = True continue if line.startswith("##"): break if in_intro: intro_lines.append(line) return " ".join(intro_lines) or f"CLI interface for the software." def extract_system_package(content: str) -> Optional[str]: """Extract system package installation command from README.""" # Look for apt/brew install patterns patterns = [ r"`apt install ([\w\-]+)`", r"`brew install ([\w\-]+)`", r"apt-get install ([\w\-]+)", ] for pattern in patterns: match = re.search(pattern, content) if match: package = match.group(1) if "apt" in pattern: return f"apt install {package}" elif "brew" in pattern: return f"brew install {package}" return None def extract_version_from_setup(setup_path: Path) -> str: """Extract version from setup.py.""" content = setup_path.read_text(encoding="utf-8") match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content) if match: return match.group(1) return "1.0.0" def extract_commands_from_cli(cli_path: Path) -> list[CommandGroup]: """Extract command groups and commands from CLI file.""" content = cli_path.read_text(encoding="utf-8") groups = [] # Find Click group decorators # Pattern handles: # - Multi-line decorators (decorators on separate lines) # - Docstrings on the same line or following line after function definition # - Various Click decorator patterns like @click.option(), @click.argument() # Uses re.DOTALL to match across newlines between decorator and def group_pattern = ( r'@(\w+)\.group\([^)]*\)' # @xxx.group(...) r'(?:\s*@[\w.]+\([^)]*\))*' # optional additional decorators r'\s*def\s+(\w+)\([^)]*\)' # def xxx(...): r':\s*' # colon with optional whitespace r'(?:"""([\s\S]*?)"""|\'\'\'([\s\S]*?)\'\'\')?' # optional docstring (""" or ''') ) for match in re.finditer(group_pattern, content): group_func = match.group(2) # Docstring can be in group 3 (triple-double) or group 4 (triple-single) group_doc = (match.group(3) or match.group(4) or "").strip() group_name = group_func.replace("_", " ").title() if not group_name: group_name = group_func.title() groups.append(CommandGroup( name=group_name, description=group_doc or f"Commands for {group_name.lower()} operations.", commands=[] )) # Find Click command decorators # Pattern handles: # - Multi-line decorators (decorators on separate lines) # - Docstrings on the same line or following line after function definition # - Various Click decorator patterns like @click.option(), @click.argument() command_pattern = ( r'@(\w+)\.command\([^)]*\)' # @xxx.command(...) r'(?:\s*@[\w.]+\([^)]*\))*' # optional additional decorators r'\s*def\s+(\w+)\([^)]*\)' # def xxx(...): r':\s*' # colon with optional whitespace r'(?:"""([\s\S]*?)"""|\'\'\'([\s\S]*?)\'\'\')?' # optional docstring (""" or ''') ) for match in re.finditer(command_pattern, content): group_name = match.group(1) cmd_name = match.group(2) # Docstring can be in group 3 (triple-double) or group 4 (triple-single) cmd_doc = (match.group(3) or match.group(4) or "").strip() # Find the matching group for group in groups: if group.name.lower().replace(" ", "_") == group_name.lower(): group.commands.append(CommandInfo( name=cmd_name.replace("_", "-"), description=cmd_doc or f"Execute {cmd_name} operation." )) # If no groups found, create a default one with all commands if not groups: default_group = CommandGroup( name="General", description="General commands for the CLI.", commands=[] ) for match in re.finditer(command_pattern, content): cmd_name = match.group(2) # Docstring can be in group 3 (triple-double) or group 4 (triple-single) cmd_doc = (match.group(3) or match.group(4) or "").strip() default_group.commands.append(CommandInfo( name=cmd_name.replace("_", "-"), description=cmd_doc or f"Execute {cmd_name} operation." )) if default_group.commands: groups.append(default_group) return groups def generate_examples(software_name: str, command_groups: list[CommandGroup]) -> list[Example]: """Generate usage examples based on software type and available commands.""" examples = [] # Basic project creation example examples.append(Example( title="Create a New Project", description=f"Create a new {software_name} project file.", code=f"""cli-anything-{software_name} project new -o myproject.json # Or with JSON output for programmatic use cli-anything-{software_name} --json project new -o myproject.json""" )) # REPL usage example examples.append(Example( title="Interactive REPL Session", description="Start an interactive session with undo/redo support.", code=f"""cli-anything-{software_name} # Enter commands interactively # Use 'help' to see available commands # Use 'undo' and 'redo' for history navigation""" )) # Export example if export commands exist for group in command_groups: if "export" in group.name.lower(): examples.append(Example( title="Export Project", description="Export the project to a final output format.", code=f"""cli-anything-{software_name} --project myproject.json export render output.pdf --overwrite""" )) break return examples def generate_skill_md(metadata: SkillMetadata, template_path: Optional[str] = None) -> str: """ Generate SKILL.md content from metadata using Jinja2 template. Args: metadata: SkillMetadata containing CLI information template_path: Optional path to custom template file Returns: Generated SKILL.md content as string """ try: from jinja2 import Environment, FileSystemLoader except ImportError: # Fallback to simple string formatting if Jinja2 not available return generate_skill_md_simple(metadata) # Load template if template_path is None: template_path = Path(__file__).parent / "templates" / "SKILL.md.template" else: template_path = Path(template_path) if not template_path.exists(): return generate_skill_md_simple(metadata) env = Environment(loader=FileSystemLoader(template_path.parent)) template = env.get_template(template_path.name) # Render template return template.render( skill_name=metadata.skill_name, skill_description=metadata.skill_description, software_name=metadata.software_name, skill_intro=metadata.skill_intro, version=metadata.version, system_package=metadata.system_package, command_groups=[{ "name": g.name, "description": g.description, "commands": [{"name": c.name, "description": c.description} for c in g.commands] } for g in metadata.command_groups], examples=[{ "title": e.title, "description": e.description, "code": e.code } for e in metadata.examples] ) def generate_skill_md_simple(metadata: SkillMetadata) -> str: """Generate SKILL.md without Jinja2 dependency.""" lines = [ "---", f'name: "{metadata.skill_name}"', f'description: "{metadata.skill_description}"', "---", "", f"# {metadata.skill_name}", "", metadata.skill_intro, "", "## Installation", "", f"This CLI is installed as part of the cli-anything-{metadata.software_name} package:", "", f"```bash", f"pip install cli-anything-{metadata.software_name}", f"```", "", "**Prerequisites:**", "- Python 3.10+", f"- {_format_display_name(metadata.software_name)} must be installed on your system", ] if metadata.system_package: lines.extend([ f"- Install {metadata.software_name}: `{metadata.system_package}`" ]) lines.extend([ "", "## Usage", "", "### Basic Commands", "", "```bash", "# Show help", f"cli-anything-{metadata.software_name} --help", "", "# Start interactive REPL mode", f"cli-anything-{metadata.software_name}", "", "# Create a new project", f"cli-anything-{metadata.software_name} project new -o project.json", "", "# Run with JSON output (for agent consumption)", f"cli-anything-{metadata.software_name} --json project info -p project.json", "```", "", ]) # Add command groups if metadata.command_groups: lines.append("## Command Groups") lines.append("") for group in metadata.command_groups: lines.append(f"### {group.name}") lines.append("") lines.append(group.description) lines.append("") if group.commands: lines.append("| Command | Description |") lines.append("|---------|-------------|") for cmd in group.commands: lines.append(f"| `{cmd.name}` | {cmd.description} |") lines.append("") # Add examples if metadata.examples: lines.append("## Examples") lines.append("") for example in metadata.examples: lines.append(f"### {example.title}") lines.append("") lines.append(example.description) lines.append("") lines.append("```bash") lines.append(example.code) lines.append("```") lines.append("") # Add AI agent guidance lines.extend([ "## For AI Agents", "", "When using this CLI programmatically:", "", "1. **Always use `--json` flag** for parseable output", "2. **Check return codes** - 0 for success, non-zero for errors", "3. **Parse stderr** for error messages on failure", "4. **Use absolute paths** for all file operations", "5. **Verify outputs exist** after export operations", "", "## Version", "", metadata.version, ]) return "\n".join(lines) def generate_skill_file(harness_path: str, output_path: Optional[str] = None, template_path: Optional[str] = None) -> str: """ Generate a SKILL.md file for a CLI-Anything harness. Args: harness_path: Path to the agent-harness directory output_path: Optional output path for SKILL.md (default: cli_anything//skills/SKILL.md) template_path: Optional path to custom Jinja2 template Returns: Path to the generated SKILL.md file """ # Extract metadata metadata = extract_cli_metadata(harness_path) # Generate content content = generate_skill_md(metadata, template_path) # Determine output path if output_path is None: # Default to skills/ directory under harness_path harness_path_obj = Path(harness_path) output_path = harness_path_obj / "cli_anything" / metadata.software_name / "skills" / "SKILL.md" else: output_path = Path(output_path) # Ensure output directory exists output_path.parent.mkdir(parents=True, exist_ok=True) # Write file output_path.write_text(content, encoding="utf-8") return str(output_path) # CLI interface for standalone usage if __name__ == "__main__": import argparse parser = argparse.ArgumentParser( description="Generate SKILL.md for CLI-Anything harnesses" ) parser.add_argument( "harness_path", help="Path to the agent-harness directory" ) parser.add_argument( "-o", "--output", help="Output path for SKILL.md (default: cli_anything//skills/SKILL.md)", default=None ) parser.add_argument( "-t", "--template", help="Path to custom Jinja2 template", default=None ) args = parser.parse_args() output_file = generate_skill_file( args.harness_path, args.output, args.template ) print(f"Generated: {output_file}")