Initial commit
This commit is contained in:
45
skills/skill.create/SKILL.md
Normal file
45
skills/skill.create/SKILL.md
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
name: Skill Create
|
||||
description: Generates a new Betty Framework Skill directory and manifest. Use when you need to bootstrap a new skill in the Betty Framework.
|
||||
---
|
||||
|
||||
# Skill Create
|
||||
|
||||
## Purpose
|
||||
This skill automates the creation of a new Claude Code-compatible Skill inside the Betty Framework. It scaffolds the directory structure, generates the `skill.yaml` manifest file, and registers the skill in the internal registry. Use this when you want to add a new skill quickly and consistently.
|
||||
|
||||
## Instructions
|
||||
1. Run the script `skill_create.py` with the following arguments:
|
||||
```bash
|
||||
python skill_create.py <skill_name> "<description>" [--inputs input1,input2] [--outputs output1,output2]
|
||||
2. The script will create a folder under /skills/<skill_name>/ with:
|
||||
* skill.yaml manifest (populated with version 0.1.0 and status draft)
|
||||
* SKILL.md containing the description
|
||||
* A registration entry added to registry/skills.json
|
||||
|
||||
3. The new manifest will be validated via the skill.define skill.
|
||||
|
||||
4. After creation, review the generated skill.yaml for correctness, edit if necessary, and then mark status: active when ready for use.
|
||||
|
||||
## Example
|
||||
|
||||
```bash
|
||||
python skill_create.py workflow.compose "Compose and orchestrate multi-step workflows" --inputs workflow.yaml,context.schema --outputs execution_plan.json
|
||||
```
|
||||
|
||||
This will generate:
|
||||
|
||||
```
|
||||
skills/
|
||||
workflow.compose/
|
||||
skill.yaml
|
||||
README.md
|
||||
registry/skills.json ← updated with workflow.compose entry
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
* The script uses forward-slash paths (e.g., `skills/workflow.compose/skill.yaml`) to remain cross-platform.
|
||||
* The manifest file format must include fields: `name`, `version`, `description`, `inputs`, `outputs`, `dependencies`, `status`.
|
||||
* This skill depends on `skill.define` (for validation) and `context.schema` (for input/output schema support).
|
||||
* After running, commit the changes to Git; version control provides traceability of new skills.
|
||||
1
skills/skill.create/__init__.py
Normal file
1
skills/skill.create/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Auto-generated package initializer for skills.
|
||||
47
skills/skill.create/skill.yaml
Normal file
47
skills/skill.create/skill.yaml
Normal file
@@ -0,0 +1,47 @@
|
||||
name: skill.create
|
||||
version: 0.1.0
|
||||
description: >
|
||||
Generates a new Betty Framework Skill directory and manifest.
|
||||
Used to bootstrap new Claude Code-compatible skills inside the Betty Framework.
|
||||
inputs:
|
||||
- skill_name
|
||||
- description
|
||||
- inputs
|
||||
- outputs
|
||||
outputs:
|
||||
- skill_directory
|
||||
- manifest_path
|
||||
- registration_record.json
|
||||
dependencies:
|
||||
- skill.define
|
||||
- context.schema
|
||||
status: active
|
||||
|
||||
entrypoints:
|
||||
- command: /skill/create
|
||||
handler: skill_create.py
|
||||
runtime: python
|
||||
description: >
|
||||
Scaffolds a new Betty Skill directory, generates its manifest,
|
||||
validates it with skill.define, and updates the registry.
|
||||
parameters:
|
||||
- name: skill_name
|
||||
type: string
|
||||
description: Name of the new skill (e.g., runtime.execute)
|
||||
required: true
|
||||
- name: description
|
||||
type: string
|
||||
description: Description of what the skill will do
|
||||
required: true
|
||||
- name: inputs
|
||||
type: string
|
||||
description: Comma-separated list of input parameters (optional)
|
||||
required: false
|
||||
- name: outputs
|
||||
type: string
|
||||
description: Comma-separated list of output parameters (optional)
|
||||
required: false
|
||||
permissions:
|
||||
- filesystem
|
||||
- read
|
||||
- write
|
||||
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