diff --git a/nanobot/skills/skill-creator/SKILL.md b/nanobot/skills/skill-creator/SKILL.md index 9b5eb6f..f4d6e0b 100644 --- a/nanobot/skills/skill-creator/SKILL.md +++ b/nanobot/skills/skill-creator/SKILL.md @@ -349,7 +349,6 @@ scripts/package_skill.py ./dist The packaging script will: 1. **Validate** the skill automatically, checking: - - YAML frontmatter format and required fields - Skill naming conventions and directory structure - Description completeness and quality @@ -357,6 +356,8 @@ The packaging script will: 2. **Package** the skill if validation passes, creating a .skill file named after the skill (e.g., `my-skill.skill`) that includes all files and maintains the proper directory structure for distribution. The .skill file is a zip file with a .skill extension. + Security restriction: symlinks are rejected and packaging fails when any symlink is present. + If validation fails, the script will report the errors and exit without creating a package. Fix any validation errors and run the packaging command again. ### Step 6: Iterate diff --git a/nanobot/skills/skill-creator/scripts/init_skill.py b/nanobot/skills/skill-creator/scripts/init_skill.py new file mode 100755 index 0000000..8633fe9 --- /dev/null +++ b/nanobot/skills/skill-creator/scripts/init_skill.py @@ -0,0 +1,378 @@ +#!/usr/bin/env python3 +""" +Skill Initializer - Creates a new skill from template + +Usage: + init_skill.py --path [--resources scripts,references,assets] [--examples] + +Examples: + init_skill.py my-new-skill --path skills/public + init_skill.py my-new-skill --path skills/public --resources scripts,references + init_skill.py my-api-helper --path skills/private --resources scripts --examples + init_skill.py custom-skill --path /custom/location +""" + +import argparse +import re +import sys +from pathlib import Path + +MAX_SKILL_NAME_LENGTH = 64 +ALLOWED_RESOURCES = {"scripts", "references", "assets"} + +SKILL_TEMPLATE = """--- +name: {skill_name} +description: [TODO: Complete and informative explanation of what the skill does and when to use it. Include WHEN to use this skill - specific scenarios, file types, or tasks that trigger it.] +--- + +# {skill_title} + +## Overview + +[TODO: 1-2 sentences explaining what this skill enables] + +## Structuring This Skill + +[TODO: Choose the structure that best fits this skill's purpose. Common patterns: + +**1. Workflow-Based** (best for sequential processes) +- Works well when there are clear step-by-step procedures +- Example: DOCX skill with "Workflow Decision Tree" -> "Reading" -> "Creating" -> "Editing" +- Structure: ## Overview -> ## Workflow Decision Tree -> ## Step 1 -> ## Step 2... + +**2. Task-Based** (best for tool collections) +- Works well when the skill offers different operations/capabilities +- Example: PDF skill with "Quick Start" -> "Merge PDFs" -> "Split PDFs" -> "Extract Text" +- Structure: ## Overview -> ## Quick Start -> ## Task Category 1 -> ## Task Category 2... + +**3. Reference/Guidelines** (best for standards or specifications) +- Works well for brand guidelines, coding standards, or requirements +- Example: Brand styling with "Brand Guidelines" -> "Colors" -> "Typography" -> "Features" +- Structure: ## Overview -> ## Guidelines -> ## Specifications -> ## Usage... + +**4. Capabilities-Based** (best for integrated systems) +- Works well when the skill provides multiple interrelated features +- Example: Product Management with "Core Capabilities" -> numbered capability list +- Structure: ## Overview -> ## Core Capabilities -> ### 1. Feature -> ### 2. Feature... + +Patterns can be mixed and matched as needed. Most skills combine patterns (e.g., start with task-based, add workflow for complex operations). + +Delete this entire "Structuring This Skill" section when done - it's just guidance.] + +## [TODO: Replace with the first main section based on chosen structure] + +[TODO: Add content here. See examples in existing skills: +- Code samples for technical skills +- Decision trees for complex workflows +- Concrete examples with realistic user requests +- References to scripts/templates/references as needed] + +## Resources (optional) + +Create only the resource directories this skill actually needs. Delete this section if no resources are required. + +### scripts/ +Executable code (Python/Bash/etc.) that can be run directly to perform specific operations. + +**Examples from other skills:** +- PDF skill: `fill_fillable_fields.py`, `extract_form_field_info.py` - utilities for PDF manipulation +- DOCX skill: `document.py`, `utilities.py` - Python modules for document processing + +**Appropriate for:** Python scripts, shell scripts, or any executable code that performs automation, data processing, or specific operations. + +**Note:** Scripts may be executed without loading into context, but can still be read by Codex for patching or environment adjustments. + +### references/ +Documentation and reference material intended to be loaded into context to inform Codex's process and thinking. + +**Examples from other skills:** +- Product management: `communication.md`, `context_building.md` - detailed workflow guides +- BigQuery: API reference documentation and query examples +- Finance: Schema documentation, company policies + +**Appropriate for:** In-depth documentation, API references, database schemas, comprehensive guides, or any detailed information that Codex should reference while working. + +### assets/ +Files not intended to be loaded into context, but rather used within the output Codex produces. + +**Examples from other skills:** +- Brand styling: PowerPoint template files (.pptx), logo files +- Frontend builder: HTML/React boilerplate project directories +- Typography: Font files (.ttf, .woff2) + +**Appropriate for:** Templates, boilerplate code, document templates, images, icons, fonts, or any files meant to be copied or used in the final output. + +--- + +**Not every skill requires all three types of resources.** +""" + +EXAMPLE_SCRIPT = '''#!/usr/bin/env python3 +""" +Example helper script for {skill_name} + +This is a placeholder script that can be executed directly. +Replace with actual implementation or delete if not needed. + +Example real scripts from other skills: +- pdf/scripts/fill_fillable_fields.py - Fills PDF form fields +- pdf/scripts/convert_pdf_to_images.py - Converts PDF pages to images +""" + +def main(): + print("This is an example script for {skill_name}") + # TODO: Add actual script logic here + # This could be data processing, file conversion, API calls, etc. + +if __name__ == "__main__": + main() +''' + +EXAMPLE_REFERENCE = """# Reference Documentation for {skill_title} + +This is a placeholder for detailed reference documentation. +Replace with actual reference content or delete if not needed. + +Example real reference docs from other skills: +- product-management/references/communication.md - Comprehensive guide for status updates +- product-management/references/context_building.md - Deep-dive on gathering context +- bigquery/references/ - API references and query examples + +## When Reference Docs Are Useful + +Reference docs are ideal for: +- Comprehensive API documentation +- Detailed workflow guides +- Complex multi-step processes +- Information too lengthy for main SKILL.md +- Content that's only needed for specific use cases + +## Structure Suggestions + +### API Reference Example +- Overview +- Authentication +- Endpoints with examples +- Error codes +- Rate limits + +### Workflow Guide Example +- Prerequisites +- Step-by-step instructions +- Common patterns +- Troubleshooting +- Best practices +""" + +EXAMPLE_ASSET = """# Example Asset File + +This placeholder represents where asset files would be stored. +Replace with actual asset files (templates, images, fonts, etc.) or delete if not needed. + +Asset files are NOT intended to be loaded into context, but rather used within +the output Codex produces. + +Example asset files from other skills: +- Brand guidelines: logo.png, slides_template.pptx +- Frontend builder: hello-world/ directory with HTML/React boilerplate +- Typography: custom-font.ttf, font-family.woff2 +- Data: sample_data.csv, test_dataset.json + +## Common Asset Types + +- Templates: .pptx, .docx, boilerplate directories +- Images: .png, .jpg, .svg, .gif +- Fonts: .ttf, .otf, .woff, .woff2 +- Boilerplate code: Project directories, starter files +- Icons: .ico, .svg +- Data files: .csv, .json, .xml, .yaml + +Note: This is a text placeholder. Actual assets can be any file type. +""" + + +def normalize_skill_name(skill_name): + """Normalize a skill name to lowercase hyphen-case.""" + normalized = skill_name.strip().lower() + normalized = re.sub(r"[^a-z0-9]+", "-", normalized) + normalized = normalized.strip("-") + normalized = re.sub(r"-{2,}", "-", normalized) + return normalized + + +def title_case_skill_name(skill_name): + """Convert hyphenated skill name to Title Case for display.""" + return " ".join(word.capitalize() for word in skill_name.split("-")) + + +def parse_resources(raw_resources): + if not raw_resources: + return [] + resources = [item.strip() for item in raw_resources.split(",") if item.strip()] + invalid = sorted({item for item in resources if item not in ALLOWED_RESOURCES}) + if invalid: + allowed = ", ".join(sorted(ALLOWED_RESOURCES)) + print(f"[ERROR] Unknown resource type(s): {', '.join(invalid)}") + print(f" Allowed: {allowed}") + sys.exit(1) + deduped = [] + seen = set() + for resource in resources: + if resource not in seen: + deduped.append(resource) + seen.add(resource) + return deduped + + +def create_resource_dirs(skill_dir, skill_name, skill_title, resources, include_examples): + for resource in resources: + resource_dir = skill_dir / resource + resource_dir.mkdir(exist_ok=True) + if resource == "scripts": + if include_examples: + example_script = resource_dir / "example.py" + example_script.write_text(EXAMPLE_SCRIPT.format(skill_name=skill_name)) + example_script.chmod(0o755) + print("[OK] Created scripts/example.py") + else: + print("[OK] Created scripts/") + elif resource == "references": + if include_examples: + example_reference = resource_dir / "api_reference.md" + example_reference.write_text(EXAMPLE_REFERENCE.format(skill_title=skill_title)) + print("[OK] Created references/api_reference.md") + else: + print("[OK] Created references/") + elif resource == "assets": + if include_examples: + example_asset = resource_dir / "example_asset.txt" + example_asset.write_text(EXAMPLE_ASSET) + print("[OK] Created assets/example_asset.txt") + else: + print("[OK] Created assets/") + + +def init_skill(skill_name, path, resources, include_examples): + """ + Initialize a new skill directory with template SKILL.md. + + Args: + skill_name: Name of the skill + path: Path where the skill directory should be created + resources: Resource directories to create + include_examples: Whether to create example files in resource directories + + Returns: + Path to created skill directory, or None if error + """ + # Determine skill directory path + skill_dir = Path(path).resolve() / skill_name + + # Check if directory already exists + if skill_dir.exists(): + print(f"[ERROR] Skill directory already exists: {skill_dir}") + return None + + # Create skill directory + try: + skill_dir.mkdir(parents=True, exist_ok=False) + print(f"[OK] Created skill directory: {skill_dir}") + except Exception as e: + print(f"[ERROR] Error creating directory: {e}") + return None + + # Create SKILL.md from template + skill_title = title_case_skill_name(skill_name) + skill_content = SKILL_TEMPLATE.format(skill_name=skill_name, skill_title=skill_title) + + skill_md_path = skill_dir / "SKILL.md" + try: + skill_md_path.write_text(skill_content) + print("[OK] Created SKILL.md") + except Exception as e: + print(f"[ERROR] Error creating SKILL.md: {e}") + return None + + # Create resource directories if requested + if resources: + try: + create_resource_dirs(skill_dir, skill_name, skill_title, resources, include_examples) + except Exception as e: + print(f"[ERROR] Error creating resource directories: {e}") + return None + + # Print next steps + print(f"\n[OK] Skill '{skill_name}' initialized successfully at {skill_dir}") + print("\nNext steps:") + print("1. Edit SKILL.md to complete the TODO items and update the description") + if resources: + if include_examples: + print("2. Customize or delete the example files in scripts/, references/, and assets/") + else: + print("2. Add resources to scripts/, references/, and assets/ as needed") + else: + print("2. Create resource directories only if needed (scripts/, references/, assets/)") + print("3. Run the validator when ready to check the skill structure") + + return skill_dir + + +def main(): + parser = argparse.ArgumentParser( + description="Create a new skill directory with a SKILL.md template.", + ) + parser.add_argument("skill_name", help="Skill name (normalized to hyphen-case)") + parser.add_argument("--path", required=True, help="Output directory for the skill") + parser.add_argument( + "--resources", + default="", + help="Comma-separated list: scripts,references,assets", + ) + parser.add_argument( + "--examples", + action="store_true", + help="Create example files inside the selected resource directories", + ) + args = parser.parse_args() + + raw_skill_name = args.skill_name + skill_name = normalize_skill_name(raw_skill_name) + if not skill_name: + print("[ERROR] Skill name must include at least one letter or digit.") + sys.exit(1) + if len(skill_name) > MAX_SKILL_NAME_LENGTH: + print( + f"[ERROR] Skill name '{skill_name}' is too long ({len(skill_name)} characters). " + f"Maximum is {MAX_SKILL_NAME_LENGTH} characters." + ) + sys.exit(1) + if skill_name != raw_skill_name: + print(f"Note: Normalized skill name from '{raw_skill_name}' to '{skill_name}'.") + + resources = parse_resources(args.resources) + if args.examples and not resources: + print("[ERROR] --examples requires --resources to be set.") + sys.exit(1) + + path = args.path + + print(f"Initializing skill: {skill_name}") + print(f" Location: {path}") + if resources: + print(f" Resources: {', '.join(resources)}") + if args.examples: + print(" Examples: enabled") + else: + print(" Resources: none (create as needed)") + print() + + result = init_skill(skill_name, path, resources, args.examples) + + if result: + sys.exit(0) + else: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/nanobot/skills/skill-creator/scripts/package_skill.py b/nanobot/skills/skill-creator/scripts/package_skill.py new file mode 100755 index 0000000..aa4de89 --- /dev/null +++ b/nanobot/skills/skill-creator/scripts/package_skill.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +""" +Skill Packager - Creates a distributable .skill file of a skill folder + +Usage: + python utils/package_skill.py [output-directory] + +Example: + python utils/package_skill.py skills/public/my-skill + python utils/package_skill.py skills/public/my-skill ./dist +""" + +import sys +import zipfile +from pathlib import Path + +from quick_validate import validate_skill + + +def _is_within(path: Path, root: Path) -> bool: + try: + path.relative_to(root) + return True + except ValueError: + return False + + +def package_skill(skill_path, output_dir=None): + """ + Package a skill folder into a .skill file. + + Args: + skill_path: Path to the skill folder + output_dir: Optional output directory for the .skill file (defaults to current directory) + + Returns: + Path to the created .skill file, or None if error + """ + skill_path = Path(skill_path).resolve() + + # Validate skill folder exists + if not skill_path.exists(): + print(f"[ERROR] Skill folder not found: {skill_path}") + return None + + if not skill_path.is_dir(): + print(f"[ERROR] Path is not a directory: {skill_path}") + return None + + # Validate SKILL.md exists + skill_md = skill_path / "SKILL.md" + if not skill_md.exists(): + print(f"[ERROR] SKILL.md not found in {skill_path}") + return None + + # Run validation before packaging + print("Validating skill...") + valid, message = validate_skill(skill_path) + if not valid: + print(f"[ERROR] Validation failed: {message}") + print(" Please fix the validation errors before packaging.") + return None + print(f"[OK] {message}\n") + + # Determine output location + skill_name = skill_path.name + if output_dir: + output_path = Path(output_dir).resolve() + output_path.mkdir(parents=True, exist_ok=True) + else: + output_path = Path.cwd() + + skill_filename = output_path / f"{skill_name}.skill" + + EXCLUDED_DIRS = {".git", ".svn", ".hg", "__pycache__", "node_modules"} + + # Create the .skill file (zip format) + try: + with zipfile.ZipFile(skill_filename, "w", zipfile.ZIP_DEFLATED) as zipf: + # Walk through the skill directory + for file_path in skill_path.rglob("*"): + # Security: never follow or package symlinks. + if file_path.is_symlink(): + print(f"[WARN] Skipping symlink: {file_path}") + continue + + rel_parts = file_path.relative_to(skill_path).parts + if any(part in EXCLUDED_DIRS for part in rel_parts): + continue + + if file_path.is_file(): + resolved_file = file_path.resolve() + if not _is_within(resolved_file, skill_path): + print(f"[ERROR] File escapes skill root: {file_path}") + return None + # If output lives under skill_path, avoid writing archive into itself. + if resolved_file == skill_filename.resolve(): + print(f"[WARN] Skipping output archive: {file_path}") + continue + + # Calculate the relative path within the zip. + arcname = Path(skill_name) / file_path.relative_to(skill_path) + zipf.write(file_path, arcname) + print(f" Added: {arcname}") + + print(f"\n[OK] Successfully packaged skill to: {skill_filename}") + return skill_filename + + except Exception as e: + print(f"[ERROR] Error creating .skill file: {e}") + return None + + +def main(): + if len(sys.argv) < 2: + print("Usage: python utils/package_skill.py [output-directory]") + print("\nExample:") + print(" python utils/package_skill.py skills/public/my-skill") + print(" python utils/package_skill.py skills/public/my-skill ./dist") + sys.exit(1) + + skill_path = sys.argv[1] + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + print(f"Packaging skill: {skill_path}") + if output_dir: + print(f" Output directory: {output_dir}") + print() + + result = package_skill(skill_path, output_dir) + + if result: + sys.exit(0) + else: + sys.exit(1) + + +if __name__ == "__main__": + main()