Initial commit
This commit is contained in:
396
skills/skill.create/skill_create.py
Normal file
396
skills/skill.create/skill_create.py
Normal file
@@ -0,0 +1,396 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Skill Create - Implementation Script
|
||||
Creates new Claude Code-compatible Skills inside the Betty Framework.
|
||||
|
||||
Usage:
|
||||
python skill_create.py <skill_name> "<description>" [--inputs input1,input2] [--outputs output1,output2]
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import yaml
|
||||
import json
|
||||
import argparse
|
||||
import subprocess
|
||||
from typing import List, Dict, Any, Optional
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
from betty.config import (
|
||||
BASE_DIR, SKILLS_DIR, get_skill_path, get_skill_manifest_path,
|
||||
get_skill_handler_path, ensure_directories,
|
||||
)
|
||||
from betty.enums import SkillStatus
|
||||
from betty.validation import validate_skill_name, ValidationError
|
||||
from betty.logging_utils import setup_logger
|
||||
from betty.errors import SkillNotFoundError, ManifestError, format_error_response
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
|
||||
def build_response(ok: bool, path: Optional[str], errors: Optional[List[str]] = None, details: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""Create a standardized response payload for CLI output."""
|
||||
|
||||
response: Dict[str, Any] = {
|
||||
"ok": ok,
|
||||
"status": "success" if ok else "failed",
|
||||
"errors": errors or [],
|
||||
"path": path,
|
||||
}
|
||||
|
||||
if details is not None:
|
||||
response["details"] = details
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def create_skill_manifest(
|
||||
skill_name: str,
|
||||
description: str,
|
||||
inputs: List[str],
|
||||
outputs: List[str]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a skill manifest dictionary.
|
||||
|
||||
Args:
|
||||
skill_name: Name of the skill
|
||||
description: Description of what the skill does
|
||||
inputs: List of input parameter names
|
||||
outputs: List of output parameter names
|
||||
|
||||
Returns:
|
||||
Skill manifest as dictionary
|
||||
"""
|
||||
return {
|
||||
"name": skill_name,
|
||||
"version": "0.1.0",
|
||||
"description": description,
|
||||
"inputs": inputs,
|
||||
"outputs": outputs,
|
||||
"dependencies": [],
|
||||
"status": SkillStatus.DRAFT.value,
|
||||
}
|
||||
|
||||
|
||||
def write_skill_yaml(manifest_path: str, manifest: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Write skill manifest to YAML file.
|
||||
|
||||
Args:
|
||||
manifest_path: Path to skill.yaml file
|
||||
manifest: Manifest dictionary
|
||||
"""
|
||||
with open(manifest_path, "w") as f:
|
||||
yaml.dump(manifest, f, sort_keys=False)
|
||||
logger.info(f"Created manifest: {manifest_path}")
|
||||
|
||||
|
||||
def write_skill_md(skill_path: str, skill_name: str, description: str) -> None:
|
||||
"""
|
||||
Create minimal SKILL.md documentation file.
|
||||
|
||||
Args:
|
||||
skill_path: Path to skill directory
|
||||
skill_name: Name of the skill
|
||||
description: Description of the skill
|
||||
"""
|
||||
skill_md_path = os.path.join(skill_path, "SKILL.md")
|
||||
content = f"""---
|
||||
name: {skill_name}
|
||||
description: {description}
|
||||
---
|
||||
|
||||
# {skill_name}
|
||||
|
||||
{description}
|
||||
|
||||
## Status
|
||||
|
||||
Auto-generated via `skill.create`.
|
||||
|
||||
## Usage
|
||||
|
||||
TODO: Add usage instructions
|
||||
|
||||
## Inputs
|
||||
|
||||
TODO: Document inputs
|
||||
|
||||
## Outputs
|
||||
|
||||
TODO: Document outputs
|
||||
|
||||
## Dependencies
|
||||
|
||||
TODO: List dependencies
|
||||
"""
|
||||
with open(skill_md_path, "w") as f:
|
||||
f.write(content)
|
||||
logger.info(f"Created documentation: {skill_md_path}")
|
||||
|
||||
|
||||
def create_skill_handler(skill_path: str, skill_name: str) -> None:
|
||||
"""
|
||||
Create a minimal skill handler Python script.
|
||||
|
||||
Args:
|
||||
skill_path: Path to skill directory
|
||||
skill_name: Name of the skill
|
||||
"""
|
||||
handler_name = skill_name.replace('.', '_') + '.py'
|
||||
handler_path = os.path.join(skill_path, handler_name)
|
||||
|
||||
content = f"""#!/usr/bin/env python3
|
||||
\"\"\"
|
||||
{skill_name} - Implementation Script
|
||||
Auto-generated by skill.create
|
||||
\"\"\"
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
|
||||
# Add Betty framework to path
|
||||
|
||||
from betty.logging_utils import setup_logger
|
||||
from betty.errors import format_error_response
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
|
||||
def main():
|
||||
\"\"\"Main entry point for {skill_name}.\"\"\"
|
||||
parser = argparse.ArgumentParser(description="{skill_name}")
|
||||
# TODO: Add arguments
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
logger.info("Executing {skill_name}...")
|
||||
# TODO: Implement skill logic
|
||||
result = {{"status": "success", "message": "Not yet implemented"}}
|
||||
print(json.dumps(result, indent=2))
|
||||
except Exception as e:
|
||||
logger.error(f"Error executing {skill_name}: {{e}}")
|
||||
print(json.dumps(format_error_response(e), indent=2))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
"""
|
||||
with open(handler_path, "w") as f:
|
||||
f.write(content)
|
||||
os.chmod(handler_path, 0o755) # Make executable
|
||||
logger.info(f"Created handler: {handler_path}")
|
||||
|
||||
|
||||
def run_validator(manifest_path: str) -> bool:
|
||||
"""
|
||||
Run skill.define validator if available.
|
||||
|
||||
Args:
|
||||
manifest_path: Path to skill manifest
|
||||
|
||||
Returns:
|
||||
True if validation succeeded or validator not found, False if validation failed
|
||||
"""
|
||||
validator_path = os.path.join(BASE_DIR, "skills", "skill.define", "skill_define.py")
|
||||
|
||||
if not os.path.exists(validator_path):
|
||||
logger.warning("skill_define.py not found — skipping validation.")
|
||||
return True
|
||||
|
||||
logger.info(f"Validating new skill with skill.define...")
|
||||
result = subprocess.run(
|
||||
[sys.executable, validator_path, manifest_path],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
logger.error(f"Validation failed: {result.stderr}")
|
||||
return False
|
||||
|
||||
logger.info("Validation succeeded")
|
||||
return True
|
||||
|
||||
|
||||
def update_registry(manifest_path: str) -> bool:
|
||||
"""
|
||||
Update registry using registry.update skill.
|
||||
|
||||
Args:
|
||||
manifest_path: Path to skill manifest
|
||||
|
||||
Returns:
|
||||
True if registry update succeeded, False otherwise
|
||||
"""
|
||||
registry_updater = os.path.join(BASE_DIR, "skills", "registry.update", "registry_update.py")
|
||||
|
||||
if not os.path.exists(registry_updater):
|
||||
logger.warning("registry.update not found — skipping registry update.")
|
||||
return False
|
||||
|
||||
logger.info("Updating registry via registry.update...")
|
||||
result = subprocess.run(
|
||||
[sys.executable, registry_updater, manifest_path],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
logger.error(f"Registry update failed: {result.stderr}")
|
||||
return False
|
||||
|
||||
logger.info("Registry updated successfully")
|
||||
return True
|
||||
|
||||
|
||||
def create_skill(
|
||||
skill_name: str,
|
||||
description: str,
|
||||
inputs: List[str],
|
||||
outputs: List[str]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Scaffold a new skill directory and manifest.
|
||||
|
||||
Args:
|
||||
skill_name: Name of the new skill
|
||||
description: Description of what the skill does
|
||||
inputs: List of input parameter names
|
||||
outputs: List of output parameter names
|
||||
|
||||
Returns:
|
||||
Result dictionary with status and created file paths
|
||||
|
||||
Raises:
|
||||
ValidationError: If skill_name is invalid
|
||||
ManifestError: If skill already exists or creation fails
|
||||
"""
|
||||
# Validate skill name
|
||||
validate_skill_name(skill_name)
|
||||
|
||||
# Ensure directories exist
|
||||
ensure_directories()
|
||||
|
||||
# Check if skill already exists
|
||||
skill_path = get_skill_path(skill_name)
|
||||
if os.path.exists(skill_path):
|
||||
raise ManifestError(
|
||||
f"Skill '{skill_name}' already exists",
|
||||
details={"skill_path": skill_path}
|
||||
)
|
||||
|
||||
try:
|
||||
# Create skill directory
|
||||
os.makedirs(skill_path, exist_ok=True)
|
||||
logger.info(f"Created skill directory: {skill_path}")
|
||||
|
||||
# Create manifest
|
||||
manifest = create_skill_manifest(skill_name, description, inputs, outputs)
|
||||
manifest_path = get_skill_manifest_path(skill_name)
|
||||
write_skill_yaml(manifest_path, manifest)
|
||||
|
||||
# Create SKILL.md
|
||||
write_skill_md(skill_path, skill_name, description)
|
||||
|
||||
# Create handler script
|
||||
create_skill_handler(skill_path, skill_name)
|
||||
|
||||
# Validate
|
||||
validation_success = run_validator(manifest_path)
|
||||
|
||||
# Update registry
|
||||
registry_success = update_registry(manifest_path)
|
||||
|
||||
result = {
|
||||
"status": "success",
|
||||
"skill_name": skill_name,
|
||||
"skill_path": skill_path,
|
||||
"manifest_path": manifest_path,
|
||||
"validation": "passed" if validation_success else "skipped",
|
||||
"registry_updated": registry_success,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat()
|
||||
}
|
||||
|
||||
logger.info(f"✅ Successfully created skill: {skill_name}")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create skill: {e}")
|
||||
raise ManifestError(f"Failed to create skill: {e}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main CLI entry point."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Create a new Betty Framework Skill.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
parser.add_argument(
|
||||
"skill_name",
|
||||
help="Name of the new skill (e.g., runtime.execute)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"description",
|
||||
help="Description of what the skill does."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--inputs",
|
||||
help="Comma-separated list of inputs",
|
||||
default=""
|
||||
)
|
||||
parser.add_argument(
|
||||
"--outputs",
|
||||
help="Comma-separated list of outputs",
|
||||
default=""
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
inputs = [i.strip() for i in args.inputs.split(",") if i.strip()]
|
||||
outputs = [o.strip() for o in args.outputs.split(",") if o.strip()]
|
||||
|
||||
try:
|
||||
details = create_skill(args.skill_name, args.description, inputs, outputs)
|
||||
response = build_response(
|
||||
True,
|
||||
path=details.get("skill_path"),
|
||||
errors=[],
|
||||
details=details,
|
||||
)
|
||||
print(json.dumps(response, indent=2))
|
||||
sys.exit(0)
|
||||
except (ValidationError, ManifestError) as e:
|
||||
logger.error(str(e))
|
||||
error_info = format_error_response(e)
|
||||
path = None
|
||||
if isinstance(e, ManifestError):
|
||||
path = e.details.get("skill_path") or e.details.get("manifest_path")
|
||||
response = build_response(
|
||||
False,
|
||||
path=path,
|
||||
errors=[error_info.get("message", str(e))],
|
||||
details={"error": error_info},
|
||||
)
|
||||
print(json.dumps(response, indent=2))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error: {e}")
|
||||
error_info = format_error_response(e, include_traceback=True)
|
||||
response = build_response(
|
||||
False,
|
||||
path=None,
|
||||
errors=[error_info.get("message", str(e))],
|
||||
details={"error": error_info},
|
||||
)
|
||||
print(json.dumps(response, indent=2))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user