# Skill Development Guide

This guide teaches you how to create skills for OCLAWMA.

## Table of Contents

- [What is a Skill?](#what-is-a-skill)
- [Quick Start](#quick-start)
- [Skill Types](#skill-types)
- [Creating a YAML Skill](#creating-a-yaml-skill)
- [Creating a Python Skill](#creating-a-python-skill)
- [Creating a Pip Package](#creating-a-pip-package)
- [Best Practices](#best-practices)
- [Examples](#examples)

## What is a Skill?

A skill is a collection of related tools that extend OCLAWMA's capabilities:

- **Docker skill**: Manage containers, images, networks
- **Git skill**: Repository operations, commits, branches
- **Kubernetes skill**: Pod management, deployments, logs

Skills can be:
- **Local**: YAML manifests in `~/.oclawma/skills/`
- **Pip-installed**: Distributed via PyPI

## Quick Start

Create your first skill in 5 minutes:

```bash
# Create skill directory
mkdir -p ~/.oclawma/skills/my_first_skill

# Create the manifest
cat > ~/.oclawma/skills/my_first_skill/skill.yaml << 'EOF'
name: my_first_skill
version: 1.0.0
description: My first OCLAWMA skill
author: Your Name
category: utilities
tools:
  - name: greet
    description: Say hello to someone
    parameters:
      - name: name
        type: string
        description: Name to greet
        required: false
    returns: string
    token_count: 50
EOF

# Create the implementation
cat > ~/.oclawma/skills/my_first_skill/my_first_skill.py << 'EOF'
from oclawma.skills import LazySkill, SkillMetadata

class MyFirstSkill(LazySkill):
    def _load(self) -> None:
        self._tools = {"greet": self._greet}
        self._loaded = True
    
    async def _greet(self, name: str = "World") -> dict:
        return {
            "success": True,
            "output": f"Hello, {name}! Welcome to OCLAWMA skills!"
        }
EOF

# Test it
oclawma run
# Then: Use the greet tool
```

## Skill Types

### 1. Local Skills (YAML + Python)

Stored in `~/.oclawma/skills/`:

```
~/.oclawma/skills/my_skill/
├── skill.yaml          # Manifest
└── my_skill.py         # Implementation
```

**Best for**: Personal skills, development, internal tools

### 2. Pip-Installable Skills

Distributed as Python packages:

```
oclawma-skill-my/
├── pyproject.toml      # Package config
├── skill.yaml          # Manifest
├── README.md           # Documentation
├── SKILL.md            # Skill specification
└── src/
    └── oclawma_skill_my/
        ├── __init__.py
        └── skill.py
```

**Best for**: Public skills, complex dependencies, version management

## Creating a YAML Skill

### Step 1: Create the Manifest

```yaml
# skill.yaml
name: weather
version: 1.0.0
description: Get weather information
author: Your Name
category: utilities
tags: [weather, api]

# Optional: Python dependencies
requirements:
  - requests>=2.28.0
  - python-dotenv

# Entry point (module:class)
entry_point: weather:WeatherSkill

# Tool definitions
tools:
  - name: current
    description: Get current weather for a location
    parameters:
      - name: location
        type: string
        description: City name or coordinates
        required: true
      - name: units
        type: string
        description: Temperature units
        required: false
        default: metric
        enum: [metric, imperial]
    returns: object
    token_count: 200
    
  - name: forecast
    description: Get weather forecast
    parameters:
      - name: location
        type: string
        required: true
      - name: days
        type: integer
        required: false
        default: 3
    returns: array
    token_count: 300
```

### Step 2: Implement the Skill

```python
# weather.py
import os
import requests
from oclawma.skills import LazySkill, SkillMetadata

class WeatherSkill(LazySkill):
    """Weather information skill."""
    
    def _load(self) -> None:
        """Initialize tools."""
        self._tools = {
            "current": self._current_weather,
            "forecast": self._weather_forecast,
        }
        self._loaded = True
        
        # Load API key
        self.api_key = os.getenv("OPENWEATHER_API_KEY")
    
    async def _current_weather(self, location: str, units: str = "metric") -> dict:
        """Get current weather."""
        if not self.api_key:
            return {
                "success": False,
                "error": "OPENWEATHER_API_KEY not set"
            }
        
        try:
            url = "https://api.openweathermap.org/data/2.5/weather"
            params = {
                "q": location,
                "units": units,
                "appid": self.api_key
            }
            response = requests.get(url, params=params, timeout=10)
            response.raise_for_status()
            data = response.json()
            
            return {
                "success": True,
                "output": {
                    "location": data["name"],
                    "temperature": data["main"]["temp"],
                    "humidity": data["main"]["humidity"],
                    "description": data["weather"][0]["description"],
                    "units": units
                }
            }
        except Exception as e:
            return {
                "success": False,
                "error": str(e)
            }
    
    async def _weather_forecast(self, location: str, days: int = 3) -> dict:
        """Get weather forecast."""
        # Implementation...
        return {"success": True, "output": []}
```

### Step 3: Test the Skill

```python
# test_weather.py
import asyncio
from oclawma.skills import SkillRegistry

async def test():
    registry = SkillRegistry()
    registry.discover_skills("~/.oclawma/skills")
    
    # Test the tool
    result = await registry.execute_tool(
        "weather", 
        "current", 
        location="London"
    )
    print(result)

if __name__ == "__main__":
    asyncio.run(test())
```

## Creating a Python Skill

### Base Classes

```python
from oclawma.skills import LazySkill, SkillMetadata

class MySkill(LazySkill):
    """
    Lazy-loaded skill base class.
    
    Skills are only loaded when their tools are invoked,
    minimizing startup time and memory usage.
    """
    
    def __init__(self, metadata: SkillMetadata) -> None:
        super().__init__(metadata)
        self._loaded = False
        self._tools = {}
    
    def _load(self) -> None:
        """Called when tools are first accessed.
        
        Override this to initialize your tools.
        """
        raise NotImplementedError
    
    def _on_unload(self) -> None:
        """Optional: cleanup when unloaded."""
        pass
```

### Tool Implementation Pattern

```python
async def _my_tool(self, param1: str, param2: int = 0) -> dict:
    """
    Tool implementation template.
    
    Returns:
        {
            "success": bool,
            "output": any,      # Result data on success
            "error": str,       # Error message on failure
            "metadata": {       # Optional execution metadata
                "execution_time": float,
                "token_count": int,
            }
        }
    """
    try:
        # Validate inputs
        if not param1:
            raise ValueError("param1 is required")
        
        # Do the work
        result = await self._do_something(param1, param2)
        
        return {
            "success": True,
            "output": result,
            "metadata": {
                "execution_time": 0.5,
            }
        }
        
    except Exception as e:
        return {
            "success": False,
            "error": str(e),
        }
```

## Creating a Pip Package

### Step 1: Project Structure

```
oclawma-skill-weather/
├── pyproject.toml
├── README.md
├── SKILL.md
├── LICENSE
└── src/
    └── oclawma_skill_weather/
        ├── __init__.py
        ├── skill.py
        └── py.typed
```

### Step 2: pyproject.toml

```toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "oclawma-skill-weather"
version = "1.0.0"
description = "Weather skill for OCLAWMA"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.9"
authors = [
    {name = "Your Name", email = "you@example.com"},
]
keywords = ["oclawma", "skill", "weather"]
classifiers = [
    "Development Status :: 4 - Beta",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: MIT License",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.9",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
]
dependencies = [
    "oclawma>=0.1.0",
    "requests>=2.28.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.0.0",
    "pytest-asyncio>=0.21.0",
    "black>=23.0.0",
    "ruff>=0.1.0",
    "mypy>=1.0.0",
]

# Entry point for skill discovery
[project.entry-points."oclawma.skills"]
weather = "oclawma_skill_weather:WeatherSkill"

[project.urls]
Homepage = "https://github.com/yourusername/oclawma-skill-weather"
Repository = "https://github.com/yourusername/oclawma-skill-weather"
Issues = "https://github.com/yourusername/oclawma-skill-weather/issues"

[tool.hatch.build.targets.wheel]
packages = ["src/oclawma_skill_weather"]

[tool.black]
line-length = 100

[tool.ruff]
line-length = 100
```

### Step 3: Implementation

```python
# src/oclawma_skill_weather/__init__.py
"""OCLAWMA Weather Skill"""

from .skill import WeatherSkill

__version__ = "1.0.0"
__all__ = ["WeatherSkill"]
```

```python
# src/oclawma_skill_weather/skill.py
import os
import requests
from oclawma.skills import LazySkill, SkillMetadata

class WeatherSkill(LazySkill):
    """Weather information skill."""
    
    def _load(self) -> None:
        self._tools = {
            "current": self._current,
            "forecast": self._forecast,
        }
        self._loaded = True
        self._api_key = os.getenv("OPENWEATHER_API_KEY")
    
    async def _current(self, location: str, units: str = "metric") -> dict:
        """Get current weather."""
        if not self._api_key:
            return {
                "success": False,
                "error": "Set OPENWEATHER_API_KEY environment variable"
            }
        
        try:
            url = "https://api.openweathermap.org/data/2.5/weather"
            response = requests.get(url, params={
                "q": location,
                "units": units,
                "appid": self._api_key
            }, timeout=10)
            response.raise_for_status()
            
            data = response.json()
            return {
                "success": True,
                "output": {
                    "location": data["name"],
                    "temp": data["main"]["temp"],
                    "description": data["weather"][0]["description"],
                }
            }
        except Exception as e:
            return {"success": False, "error": str(e)}
    
    async def _forecast(self, location: str, days: int = 3) -> dict:
        """Get weather forecast."""
        # Implementation...
        return {"success": True, "output": []}
```

### Step 4: SKILL.md

```markdown
# Skill: weather

## Metadata

| Field | Value |
|-------|-------|
| **Name** | `weather` |
| **Version** | `1.0.0` |
| **Package** | `oclawma-skill-weather` |
| **Category** | `utilities` |

## Description

Get weather information for any location.

## Installation

```bash
pip install oclawma-skill-weather
```

## Tools

### `current`

Get current weather for a location.

**Parameters:**

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `location` | `string` | Yes | - | City name |
| `units` | `string` | No | `metric` | `metric` or `imperial` |

**Returns:** `object`

## Environment Variables

| Variable | Required | Description |
|----------|----------|-------------|
| `OPENWEATHER_API_KEY` | Yes | OpenWeatherMap API key |
```

### Step 5: Publish to PyPI

```bash
# Build the package
python -m build

# Upload to PyPI
python -m twine upload dist/*
```

## Best Practices

### 1. Error Handling

Always return structured error responses:

```python
async def _safe_tool(self, path: str) -> dict:
    try:
        if not os.path.exists(path):
            return {
                "success": False,
                "error": f"Path not found: {path}"
            }
        # ... do work
    except PermissionError:
        return {
            "success": False,
            "error": f"Permission denied: {path}"
        }
    except Exception as e:
        return {
            "success": False,
            "error": f"Unexpected error: {e}"
        }
```

### 2. Input Validation

Validate inputs before processing:

```python
async def _create_resource(self, name: str, count: int) -> dict:
    # Validate name
    if not name or not name.strip():
        return {"success": False, "error": "Name is required"}
    
    # Validate count
    if not isinstance(count, int) or count < 1:
        return {"success": False, "error": "Count must be a positive integer"}
    
    if count > 100:
        return {"success": False, "error": "Count cannot exceed 100"}
    
    # ... proceed
```

### 3. Token Count Estimation

Provide accurate token estimates:

```python
# In skill.yaml
tools:
  - name: list_files
    description: List files in a directory
    token_count: 100  # Base overhead
    
  - name: read_file
    description: Read file contents
    token_count: 50  # Per-file overhead
```

### 4. Lazy Loading

Don't do heavy work in `__init__`:

```python
def _load(self) -> None:
    """Load only when tools are accessed."""
    # Good: Just set up tool mappings
    self._tools = {"list": self._list, "get": self._get}
    self._loaded = True
    
    # Good: Lazy initialization of clients
    self._client = None

@property
def client(self):
    """Lazy client initialization."""
    if self._client is None:
        self._client = self._create_client()
    return self._client
```

### 5. Testing

Write comprehensive tests:

```python
# tests/test_weather.py
import pytest
from oclawma_skill_weather import WeatherSkill
from oclawma.skills import SkillMetadata

@pytest.fixture
def skill():
    metadata = SkillMetadata(
        name="weather",
        version="1.0.0",
        description="Weather skill"
    )
    return WeatherSkill(metadata)

@pytest.mark.asyncio
async def test_current_missing_api_key(skill):
    result = await skill.execute_tool("current", location="London")
    assert result["success"] is False
    assert "OPENWEATHER_API_KEY" in result["error"]

@pytest.mark.asyncio
async def test_current_invalid_location(skill, monkeypatch):
    monkeypatch.setenv("OPENWEATHER_API_KEY", "test-key")
    skill._load()
    
    result = await skill.execute_tool("current", location="")
    assert result["success"] is False
```

## Examples

### File Operations Skill

```yaml
# skill.yaml
name: file_ops
version: 1.0.0
description: File operations with safety
tools:
  - name: find
    description: Find files matching pattern
    parameters:
      - name: pattern
        type: string
        required: true
      - name: path
        type: string
        required: false
        default: "."
    returns: array
    token_count: 100
```

```python
# file_ops.py
import os
import fnmatch
from pathlib import Path
from oclawma.skills import LazySkill, SkillMetadata

class FileOpsSkill(LazySkill):
    def _load(self) -> None:
        self._tools = {
            "find": self._find,
            "size": self._size,
        }
        self._loaded = True
    
    async def _find(self, pattern: str, path: str = ".") -> dict:
        """Find files matching pattern."""
        try:
            matches = []
            root_path = Path(path).resolve()
            
            for root, dirs, files in os.walk(root_path):
                for filename in files:
                    if fnmatch.fnmatch(filename, pattern):
                        full_path = Path(root) / filename
                        matches.append(str(full_path))
            
            return {
                "success": True,
                "output": matches,
                "metadata": {"count": len(matches)}
            }
        except Exception as e:
            return {"success": False, "error": str(e)}
    
    async def _size(self, path: str) -> dict:
        """Get file or directory size."""
        try:
            p = Path(path)
            if p.is_file():
                size = p.stat().st_size
            else:
                size = sum(f.stat().st_size for f in p.rglob("*") if f.is_file())
            
            return {
                "success": True,
                "output": {
                    "path": str(p),
                    "size_bytes": size,
                    "size_human": self._human_readable_size(size)
                }
            }
        except Exception as e:
            return {"success": False, "error": str(e)}
    
    def _human_readable_size(self, size_bytes: int) -> str:
        for unit in ["B", "KB", "MB", "GB", "TB"]:
            if size_bytes < 1024:
                return f"{size_bytes:.1f} {unit}"
            size_bytes /= 1024
        return f"{size_bytes:.1f} PB"
```

### API Integration Skill

```python
# api_skill.py
import httpx
from oclawma.skills import LazySkill, SkillMetadata

class APISkill(LazySkill):
    """Generic API integration skill."""
    
    def __init__(self, metadata: SkillMetadata) -> None:
        super().__init__(metadata)
        self._client: httpx.AsyncClient | None = None
    
    def _load(self) -> None:
        self._tools = {
            "get": self._get,
            "post": self._post,
        }
        self._loaded = True
    
    @property
    def client(self) -> httpx.AsyncClient:
        if self._client is None:
            self._client = httpx.AsyncClient(timeout=30)
        return self._client
    
    async def _get(self, url: str, headers: dict = None) -> dict:
        try:
            response = await self.client.get(url, headers=headers)
            response.raise_for_status()
            return {
                "success": True,
                "output": response.json()
            }
        except Exception as e:
            return {"success": False, "error": str(e)}
    
    async def _post(self, url: str, data: dict, headers: dict = None) -> dict:
        try:
            response = await self.client.post(url, json=data, headers=headers)
            response.raise_for_status()
            return {
                "success": True,
                "output": response.json()
            }
        except Exception as e:
            return {"success": False, "error": str(e)}
    
    def _on_unload(self) -> None:
        """Cleanup resources."""
        if self._client:
            import asyncio
            asyncio.create_task(self._client.aclose())
```

## Resources

- [Skill Template](../examples/skill-template/) - Complete working example
- [Docker Skill](../examples/skills/docker/) - Complex skill example
- [SKILL_PACKAGING.md](SKILL_PACKAGING.md) - Packaging specification
