Auto-sync from website-creator
This commit is contained in:
196
skills/skill-creator/SKILL.md
Normal file
196
skills/skill-creator/SKILL.md
Normal file
@@ -0,0 +1,196 @@
|
||||
---
|
||||
name: skill-creator
|
||||
description: Create new OpenCode skills with proper structure, SKILL.md format, and script templates. Use this skill when you need to create a new OpenCode skill.
|
||||
---
|
||||
|
||||
# Skill Creator
|
||||
|
||||
Guide and tools for creating new OpenCode skills.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
python3 scripts/create_skill.py <skill-name> "<description>"
|
||||
```
|
||||
|
||||
## SKILL.md Format (Required)
|
||||
|
||||
Every skill must have a `SKILL.md` file with YAML frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: skill-name
|
||||
description: Brief description. Use when user wants to [specific action].
|
||||
---
|
||||
|
||||
# Skill Name
|
||||
|
||||
Brief explanation of what this skill does.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Args | Description |
|
||||
|---------|------|-------------|
|
||||
| `command1` | `<arg>` | What it does |
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Default | Range | Description |
|
||||
|--------|---------|-------|-------------|
|
||||
| `--option` | 100 | 1-1000 | What it does |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
python3 scripts/script.py command "arg" --option 50
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
- Success: `Result: filename [id]`
|
||||
- Error: `Error: message` (to stderr)
|
||||
|
||||
## Notes
|
||||
|
||||
- Required environment variables
|
||||
- Important constraints
|
||||
```
|
||||
|
||||
## Frontmatter Rules
|
||||
|
||||
| Field | Required | Rules |
|
||||
|-------|----------|-------|
|
||||
| `name` | Yes | 1-64 chars, lowercase alphanumeric + hyphens, no leading/trailing/consecutive hyphens |
|
||||
| `description` | Yes | 1-1024 chars, specific enough for agent to choose correctly |
|
||||
| `license` | No | e.g., MIT |
|
||||
| `compatibility` | No | e.g., opencode |
|
||||
| `metadata` | No | String-to-string map |
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
skills/
|
||||
└── skill-name/
|
||||
├── SKILL.md # Required: skill definition
|
||||
└── scripts/
|
||||
├── main_script.py # Executable script
|
||||
├── .env.example # Required: env var template
|
||||
└── requirements.txt # Optional: Python deps
|
||||
```
|
||||
|
||||
## Script Best Practices
|
||||
|
||||
### 1. Load Environment Variables
|
||||
|
||||
```python
|
||||
def load_env():
|
||||
env_path = Path(__file__).parent / ".env"
|
||||
if env_path.exists():
|
||||
for line in env_path.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#") and "=" in line:
|
||||
k, v = line.split("=", 1)
|
||||
os.environ.setdefault(k.strip(), v.strip().strip("\"'"))
|
||||
|
||||
load_env()
|
||||
API_TOKEN = os.environ.get("API_TOKEN")
|
||||
```
|
||||
|
||||
### 2. Handle API Responses (Binary + JSON)
|
||||
|
||||
APIs may return raw binary or JSON with base64. Handle both:
|
||||
|
||||
```python
|
||||
response = requests.post(url, headers=headers, json=payload, timeout=300)
|
||||
response.raise_for_status()
|
||||
|
||||
content_type = response.headers.get("Content-Type", "")
|
||||
|
||||
if "image/" in content_type or "application/octet-stream" in content_type:
|
||||
# Raw binary response
|
||||
data = response.content
|
||||
else:
|
||||
# JSON with base64
|
||||
result = response.json()
|
||||
if isinstance(result, list) and len(result) > 0:
|
||||
image_data = result[0].get("data", "")
|
||||
if image_data.startswith("data:"):
|
||||
data = base64.b64decode(image_data.split(",", 1)[1])
|
||||
else:
|
||||
data = base64.b64decode(image_data)
|
||||
```
|
||||
|
||||
### 3. Send Base64 (Plain, Not Data URI)
|
||||
|
||||
Some APIs expect plain base64, not data URI:
|
||||
|
||||
```python
|
||||
import base64
|
||||
|
||||
with open(image_path, "rb") as f:
|
||||
image_bytes = f.read()
|
||||
|
||||
# Plain base64 (no data: prefix)
|
||||
b64_string = base64.b64encode(image_bytes).decode("utf-8")
|
||||
```
|
||||
|
||||
### 4. Output Format
|
||||
|
||||
Follow OpenCode conventions:
|
||||
|
||||
```python
|
||||
# Success with ID
|
||||
print(f"Result: {filename} [{timestamp}]")
|
||||
|
||||
# Error to stderr
|
||||
print(f"Error: {message}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
```
|
||||
|
||||
### 5. CLI Arguments
|
||||
|
||||
Use argparse for clean CLI:
|
||||
|
||||
```python
|
||||
parser = argparse.ArgumentParser(description="What this does")
|
||||
parser.add_argument("required_arg", help="Description")
|
||||
parser.add_argument("--optional", type=int, default=100, help="Description")
|
||||
args = parser.parse_args()
|
||||
```
|
||||
|
||||
## .env.example Template
|
||||
|
||||
```
|
||||
# API credentials
|
||||
# Get your token from https://service.com/account
|
||||
#
|
||||
# WARNING: Never commit actual credentials!
|
||||
|
||||
API_TOKEN=your_api_token_here
|
||||
```
|
||||
|
||||
## Installation Paths
|
||||
|
||||
| Type | Path |
|
||||
|------|------|
|
||||
| Global | `~/.config/opencode/skills/<name>/SKILL.md` |
|
||||
| Project | `./.opencode/skills/<name>/SKILL.md` |
|
||||
|
||||
## Common Issues
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| 400 Bad Request | Check payload format - may need flat JSON, not nested |
|
||||
| Skill not found | Verify path is `skills/<name>/SKILL.md` (plural "skills") |
|
||||
| API token not loaded | Check .env is in same directory as script |
|
||||
| Binary response fails | Check Content-Type header, handle raw bytes |
|
||||
|
||||
## Checklist for New Skills
|
||||
|
||||
- [ ] `SKILL.md` with required frontmatter (name, description)
|
||||
- [ ] `scripts/` directory with main script
|
||||
- [ ] `scripts/.env.example` with placeholder credentials
|
||||
- [ ] `scripts/requirements.txt` if external deps needed
|
||||
- [ ] Script handles both binary and JSON responses
|
||||
- [ ] Output follows format: `Result: name [id]`
|
||||
- [ ] Errors go to stderr with `sys.exit(1)`
|
||||
2
skills/skill-creator/scripts/.env.example
Normal file
2
skills/skill-creator/scripts/.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
# No API credentials needed for skill creator
|
||||
# This tool creates skill scaffolds locally
|
||||
204
skills/skill-creator/scripts/create_skill.py
Executable file
204
skills/skill-creator/scripts/create_skill.py
Executable file
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create a new OpenCode skill with proper structure."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SKILL_TEMPLATE = """---
|
||||
name: {name}
|
||||
description: {description}
|
||||
---
|
||||
|
||||
# {title}
|
||||
|
||||
Brief description of what this skill does.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Args | Description |
|
||||
|---------|------|-------------|
|
||||
| `command1` | `<arg>` | Description |
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Default | Range | Description |
|
||||
|--------|---------|-------|-------------|
|
||||
| `--option` | 100 | 1-1000 | Description |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
python3 scripts/{script_name}.py command "arg" --option 50
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
- Success: `Result: filename [id]`
|
||||
- Error: `Error: message` (to stderr)
|
||||
|
||||
## Notes
|
||||
|
||||
- Required environment variables: API_KEY
|
||||
- Additional constraints or notes
|
||||
"""
|
||||
|
||||
|
||||
SCRIPT_TEMPLATE = """#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_env():
|
||||
env_path = Path(__file__).parent / ".env"
|
||||
if env_path.exists():
|
||||
for line in env_path.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#") and "=" in line:
|
||||
k, v = line.split("=", 1)
|
||||
os.environ.setdefault(k.strip(), v.strip().strip("\"'"))
|
||||
|
||||
|
||||
load_env()
|
||||
|
||||
API_KEY = os.environ.get("API_KEY")
|
||||
API_URL = "https://api.example.com/endpoint"
|
||||
|
||||
|
||||
def main_action(arg1, option1=100):
|
||||
if not API_KEY:
|
||||
print("Error: API_KEY not set in environment", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# TODO: Implement the main functionality
|
||||
|
||||
print(f"Result: output [1]")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="{title} skill")
|
||||
parser.add_argument("arg1", help="First argument")
|
||||
parser.add_argument("--option1", type=int, default=100, help="Option description")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
main_action(args.arg1, args.option1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
"""
|
||||
|
||||
|
||||
ENV_EXAMPLE_TEMPLATE = """# API credentials
|
||||
# Get your token from https://service.com/account
|
||||
#
|
||||
# WARNING: Never commit actual credentials!
|
||||
|
||||
API_KEY=your_api_key_here
|
||||
"""
|
||||
|
||||
|
||||
REQUIREMENTS_TEMPLATE = """requests>=2.28.0
|
||||
"""
|
||||
|
||||
|
||||
def validate_name(name):
|
||||
"""Validate skill name follows OpenCode rules."""
|
||||
import re
|
||||
|
||||
if not name:
|
||||
print("Error: Name cannot be empty", file=sys.stderr)
|
||||
return False
|
||||
|
||||
if len(name) > 64:
|
||||
print("Error: Name must be 64 characters or less", file=sys.stderr)
|
||||
return False
|
||||
|
||||
pattern = r"^[a-z0-9]+(-[a-z0-9]+)*$"
|
||||
if not re.match(pattern, name):
|
||||
print(
|
||||
"Error: Name must be lowercase alphanumeric with single hyphens",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(" - No leading/trailing hyphens", file=sys.stderr)
|
||||
print(" - No consecutive hyphens", file=sys.stderr)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def create_skill(name, description, output_dir):
|
||||
"""Create a new skill directory structure."""
|
||||
|
||||
if not validate_name(name):
|
||||
sys.exit(1)
|
||||
|
||||
title = name.replace("-", " ").title()
|
||||
script_name = name.replace("-", "_")
|
||||
|
||||
skill_dir = Path(output_dir) / name
|
||||
scripts_dir = skill_dir / "scripts"
|
||||
|
||||
if skill_dir.exists():
|
||||
print(f"Error: Skill '{name}' already exists at {skill_dir}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Create directories
|
||||
scripts_dir.mkdir(parents=True)
|
||||
|
||||
# Create SKILL.md
|
||||
skill_md = skill_dir / "SKILL.md"
|
||||
skill_md.write_text(
|
||||
SKILL_TEMPLATE.format(
|
||||
name=name, description=description, title=title, script_name=script_name
|
||||
)
|
||||
)
|
||||
|
||||
# Create script
|
||||
script_file = scripts_dir / f"{script_name}.py"
|
||||
script_file.write_text(SCRIPT_TEMPLATE.format(title=title))
|
||||
script_file.chmod(0o755)
|
||||
|
||||
# Create .env.example
|
||||
env_example = scripts_dir / ".env.example"
|
||||
env_example.write_text(ENV_EXAMPLE_TEMPLATE)
|
||||
|
||||
# Create requirements.txt
|
||||
requirements = scripts_dir / "requirements.txt"
|
||||
requirements.write_text(REQUIREMENTS_TEMPLATE)
|
||||
|
||||
print(f"Created skill: {name}")
|
||||
print(f" {skill_dir}/")
|
||||
print(f" {skill_dir}/SKILL.md")
|
||||
print(f" {scripts_dir}/{script_name}.py")
|
||||
print(f" {scripts_dir}/.env.example")
|
||||
print(f" {scripts_dir}/requirements.txt")
|
||||
print()
|
||||
print("Next steps:")
|
||||
print(f" 1. Edit {skill_dir}/SKILL.md to define commands")
|
||||
print(f" 2. Implement {scripts_dir}/{script_name}.py")
|
||||
print(f" 3. Update {scripts_dir}/.env.example with required env vars")
|
||||
print(f" 4. Run: ./scripts/install-skills.sh")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Create a new OpenCode skill")
|
||||
parser.add_argument("name", help="Skill name (lowercase, hyphens only)")
|
||||
parser.add_argument("description", help="Brief description of the skill")
|
||||
parser.add_argument(
|
||||
"--output", "-o", default="skills", help="Output directory (default: skills)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
create_skill(args.name, args.description, args.output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user