> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orcbot.buzzchat.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Plugins

> Create hot-loadable TypeScript plugins to extend OrcBot with custom skills and capabilities

## Overview

OrcBot supports hot-loadable skills via TypeScript or JavaScript plugins. Plugins are dynamically loaded at runtime without requiring restarts.

## Plugin System Features

<Card title="Hot-Loading" icon="fire">
  * Zero restarts needed
  * Loaded at runtime from `~/.orcbot/plugins/`
  * Changes detected automatically
</Card>

<Card title="Self-Repair" icon="wrench">
  * If a plugin fails, OrcBot attempts automatic repair
  * Uses `self_repair_skill` to fix broken code
  * Error isolation prevents crashes
</Card>

<Card title="Security" icon="shield">
  * Allow/deny lists for plugin control
  * Sandbox execution context
  * Safe mode disables plugin loading
</Card>

## Plugin Locations

OrcBot scans these directories for plugins:

```
~/.orcbot/plugins/          # User plugins (preferred)
./plugins/                  # Project plugins
```

Plugin structure:

```
~/.orcbot/plugins/
  ├── my-custom-skill/
  │   ├── index.js          # Main entry point
  │   ├── SKILL.md          # Documentation (optional)
  │   └── package.json      # Dependencies (optional)
  ├── stripe-integration/
  │   ├── index.ts
  │   └── SKILL.md
  └── notion-api/
      ├── index.js
      └── config.json
```

## Creating Your First Plugin

### Simple Plugin Example

<Steps>
  <Step title="Create Plugin Directory">
    ```bash theme={null}
    mkdir -p ~/.orcbot/plugins/hello-world
    cd ~/.orcbot/plugins/hello-world
    ```
  </Step>

  <Step title="Create index.js">
    ```javascript theme={null}
    // ~/.orcbot/plugins/hello-world/index.js

    module.exports = [
      {
        name: 'hello_world',
        description: 'Returns a friendly greeting',
        usage: 'hello_world(name?)',
        handler: async (args) => {
          const name = args.name || 'World';
          return {
            success: true,
            message: `Hello, ${name}! This is a custom plugin.`
          };
        }
      }
    ];
    ```
  </Step>

  <Step title="Test the Plugin">
    ```bash theme={null}
    # Start OrcBot (or it will auto-reload if already running)
    orcbot run

    # In Telegram/Discord/WhatsApp:
    "Use the hello_world skill to greet Alice"

    # Bot responds:
    "Hello, Alice! This is a custom plugin."
    ```
  </Step>
</Steps>

### Plugin API Reference

```typescript theme={null}
interface PluginSkill {
  name: string;              // Skill name (e.g., 'web_search')
  description: string;       // What the skill does
  usage: string;             // How to call it
  handler: SkillHandler;     // Async function
  requiresAdmin?: boolean;   // Requires admin permission
  metadata?: any;            // Custom metadata
}

type SkillHandler = (args: any, context?: any) => Promise<any>;
```

**Handler parameters:**

* `args` - Object containing skill arguments
* `context` - Optional execution context (agent, config, memory)

**Handler return:**

```javascript theme={null}
{
  success: boolean,      // Required
  message?: string,      // Result message
  data?: any,            // Structured data
  error?: string,        // Error message if failed
  ...customFields        // Any additional fields
}
```

## Advanced Plugin Examples

### API Integration Plugin

```javascript theme={null}
// ~/.orcbot/plugins/weather-api/index.js

const fetch = require('node-fetch');

module.exports = [
  {
    name: 'get_weather',
    description: 'Get current weather for a location using OpenWeather API',
    usage: 'get_weather(location)',
    handler: async (args) => {
      const location = args.location;
      if (!location) {
        return { success: false, error: 'Missing location parameter' };
      }

      const apiKey = process.env.OPENWEATHER_API_KEY;
      if (!apiKey) {
        return { success: false, error: 'API key not configured' };
      }

      try {
        const url = `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(location)}&appid=${apiKey}&units=metric`;
        const response = await fetch(url);
        const data = await response.json();

        if (!response.ok) {
          return { success: false, error: data.message || 'API error' };
        }

        return {
          success: true,
          location: data.name,
          temperature: data.main.temp,
          description: data.weather[0].description,
          humidity: data.main.humidity,
          message: `Weather in ${data.name}: ${data.main.temp}°C, ${data.weather[0].description}`
        };
      } catch (e) {
        return { success: false, error: `Failed to fetch weather: ${e.message}` };
      }
    }
  }
];
```

### Database Plugin

```javascript theme={null}
// ~/.orcbot/plugins/postgres-query/index.js

const { Pool } = require('pg');

const pool = new Pool({
  host: process.env.DB_HOST || 'localhost',
  port: process.env.DB_PORT || 5432,
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD
});

module.exports = [
  {
    name: 'query_database',
    description: 'Execute a read-only SQL query against the database',
    usage: 'query_database(query)',
    requiresAdmin: true,  // Restrict to admin users
    handler: async (args) => {
      const query = args.query;
      if (!query) {
        return { success: false, error: 'Missing query parameter' };
      }

      // Safety: only allow SELECT queries
      if (!query.trim().toLowerCase().startsWith('select')) {
        return { success: false, error: 'Only SELECT queries are allowed' };
      }

      try {
        const result = await pool.query(query);
        return {
          success: true,
          rowCount: result.rowCount,
          rows: result.rows,
          message: `Query returned ${result.rowCount} row(s)`
        };
      } catch (e) {
        return { success: false, error: `Database error: ${e.message}` };
      }
    }
  },
  {
    name: 'get_user_stats',
    description: 'Get aggregated user statistics from the database',
    usage: 'get_user_stats()',
    handler: async () => {
      try {
        const result = await pool.query(
          'SELECT COUNT(*) as total, COUNT(CASE WHEN active THEN 1 END) as active FROM users'
        );
        const stats = result.rows[0];
        return {
          success: true,
          totalUsers: parseInt(stats.total),
          activeUsers: parseInt(stats.active),
          message: `Total users: ${stats.total}, Active: ${stats.active}`
        };
      } catch (e) {
        return { success: false, error: `Failed to fetch stats: ${e.message}` };
      }
    }
  }
];
```

### File Processing Plugin

```javascript theme={null}
// ~/.orcbot/plugins/csv-parser/index.js

const fs = require('fs');
const path = require('path');
const csv = require('csv-parse/sync');

module.exports = [
  {
    name: 'parse_csv',
    description: 'Parse a CSV file and return structured data',
    usage: 'parse_csv(filePath, options?)',
    handler: async (args) => {
      const filePath = args.filePath;
      if (!filePath) {
        return { success: false, error: 'Missing filePath parameter' };
      }

      if (!fs.existsSync(filePath)) {
        return { success: false, error: `File not found: ${filePath}` };
      }

      try {
        const content = fs.readFileSync(filePath, 'utf-8');
        const records = csv.parse(content, {
          columns: true,
          skip_empty_lines: true,
          ...args.options
        });

        return {
          success: true,
          rowCount: records.length,
          columns: Object.keys(records[0] || {}),
          data: records,
          message: `Parsed ${records.length} rows from ${path.basename(filePath)}`
        };
      } catch (e) {
        return { success: false, error: `CSV parsing failed: ${e.message}` };
      }
    }
  },
  {
    name: 'analyze_csv',
    description: 'Analyze CSV data and return statistics',
    usage: 'analyze_csv(filePath, column)',
    handler: async (args) => {
      const parseResult = await module.exports[0].handler(args);
      if (!parseResult.success) return parseResult;

      const column = args.column;
      const data = parseResult.data;

      if (!column || !data[0]?.[column]) {
        return { success: false, error: `Column '${column}' not found` };
      }

      const values = data.map(row => parseFloat(row[column])).filter(v => !isNaN(v));
      if (values.length === 0) {
        return { success: false, error: `No numeric values in column '${column}'` };
      }

      const sum = values.reduce((a, b) => a + b, 0);
      const avg = sum / values.length;
      const min = Math.min(...values);
      const max = Math.max(...values);

      return {
        success: true,
        column,
        count: values.length,
        sum,
        average: avg,
        min,
        max,
        message: `Column '${column}': avg=${avg.toFixed(2)}, min=${min}, max=${max}`
      };
    }
  }
];
```

### TypeScript Plugin

```typescript theme={null}
// ~/.orcbot/plugins/notion-integration/index.ts

import { Client } from '@notionhq/client';

interface PluginSkill {
  name: string;
  description: string;
  usage: string;
  handler: (args: any, context?: any) => Promise<any>;
}

const notion = new Client({
  auth: process.env.NOTION_API_KEY
});

const skills: PluginSkill[] = [
  {
    name: 'notion_create_page',
    description: 'Create a new page in Notion database',
    usage: 'notion_create_page(databaseId, title, content)',
    handler: async (args) => {
      const { databaseId, title, content } = args;
      
      if (!databaseId || !title) {
        return { success: false, error: 'Missing required parameters' };
      }

      try {
        const response = await notion.pages.create({
          parent: { database_id: databaseId },
          properties: {
            Name: {
              title: [
                {
                  text: { content: title }
                }
              ]
            }
          },
          children: content ? [
            {
              object: 'block',
              type: 'paragraph',
              paragraph: {
                rich_text: [
                  {
                    type: 'text',
                    text: { content }
                  }
                ]
              }
            }
          ] : []
        });

        return {
          success: true,
          pageId: response.id,
          url: (response as any).url,
          message: `Created Notion page: ${title}`
        };
      } catch (e: any) {
        return { success: false, error: `Notion API error: ${e.message}` };
      }
    }
  },
  {
    name: 'notion_search',
    description: 'Search Notion workspace',
    usage: 'notion_search(query)',
    handler: async (args) => {
      try {
        const response = await notion.search({
          query: args.query,
          page_size: 10
        });

        const results = response.results.map((page: any) => ({
          id: page.id,
          title: page.properties?.Name?.title?.[0]?.plain_text || 'Untitled',
          url: page.url
        }));

        return {
          success: true,
          count: results.length,
          results,
          message: `Found ${results.length} result(s) for '${args.query}'`
        };
      } catch (e: any) {
        return { success: false, error: `Search failed: ${e.message}` };
      }
    }
  }
];

export = skills;
```

**Compile TypeScript plugins:**

```bash theme={null}
cd ~/.orcbot/plugins/notion-integration
npm install @notionhq/client
npx tsc index.ts --module commonjs --target es2020
```

## Plugin Documentation (SKILL.md)

Create a `SKILL.md` file for better agent understanding:

````markdown theme={null}
# get_weather

Get current weather conditions for any location worldwide.

## Usage

get_weather(location)

## Parameters

- location: City name or "City, Country" format (required)

## Examples

- get_weather(location="London")
- get_weather(location="New York, US")
- get_weather(location="Tokyo, Japan")

## Requirements

Requires `OPENWEATHER_API_KEY` environment variable.

## Returns

```json
{
  "success": true,
  "location": "London",
  "temperature": 15.5,
  "description": "partly cloudy",
  "humidity": 65
}
````

## Error Handling

* Returns error if location not found
* Returns error if API key not configured
* Returns error if API request fails

````

## Plugin Configuration

### Allow/Deny Lists

```yaml
# ~/.orcbot/orcbot.config.yaml

# Whitelist: only load these plugins
pluginAllowList:
  - hello-world
  - weather-api
  - csv-parser

# Blacklist: load all except these
pluginDenyList:
  - dangerous-plugin
  - experimental-plugin
````

### Disable Plugins

```yaml theme={null}
# Disable all plugins
safeMode: true

# Or disable individually via deny list
pluginDenyList:
  - plugin-name
```

## Plugin Metadata

Add custom metadata to your plugins:

```javascript theme={null}
module.exports = [
  {
    name: 'stripe_charge',
    description: 'Process a payment via Stripe',
    usage: 'stripe_charge(amount, customer)',
    requiresAdmin: true,
    metadata: {
      version: '1.0.0',
      author: 'Your Name',
      category: 'payments',
      rateLimit: {
        maxCalls: 10,
        perMinutes: 1
      },
      dependencies: ['stripe'],
      environment: {
        required: ['STRIPE_SECRET_KEY']
      }
    },
    handler: async (args) => {
      // Implementation
    }
  }
];
```

## Error Handling

### Plugin Load Errors

If a plugin fails to load:

```
SkillsManager: Failed to load plugin 'my-plugin': SyntaxError: Unexpected token
SkillsManager: Attempting self-repair for 'my-plugin'...
```

OrcBot automatically:

1. Isolates the error
2. Attempts `self_repair_skill` to fix syntax/logic issues
3. Retries loading
4. Falls back to disabling plugin if repair fails

### Runtime Errors

Handle errors gracefully in your plugin:

```javascript theme={null}
handler: async (args) => {
  try {
    // Your logic
    const result = await someAsyncOperation();
    return { success: true, data: result };
  } catch (e) {
    // Log error for debugging
    console.error(`Plugin error: ${e.message}`);
    
    // Return structured error
    return {
      success: false,
      error: e.message,
      code: e.code || 'UNKNOWN_ERROR'
    };
  }
}
```

## Testing Plugins

### Manual Testing

```bash theme={null}
# Start OrcBot
orcbot run

# Test via chat
"List all available skills"
# Should show your plugin

"Use the get_weather skill for London"
# Should execute your plugin
```

### Unit Testing

Create tests for your plugin:

```javascript theme={null}
// ~/.orcbot/plugins/weather-api/test.js

const assert = require('assert');
const plugin = require('./index');

(async () => {
  // Test successful call
  const result = await plugin[0].handler({ location: 'London' });
  assert(result.success, 'Should succeed for valid location');
  assert(result.temperature !== undefined, 'Should return temperature');
  
  // Test missing parameter
  const error = await plugin[0].handler({});
  assert(!error.success, 'Should fail without location');
  assert(error.error, 'Should return error message');
  
  console.log('All tests passed!');
})();
```

Run tests:

```bash theme={null}
node ~/.orcbot/plugins/weather-api/test.js
```

## Debugging Plugins

### Enable Debug Logs

```yaml theme={null}
logLevel: debug
```

### Check Plugin Loading

```bash theme={null}
grep "SkillsManager" ~/.orcbot/logs/orcbot.log

# Should show:
SkillsManager: Loading plugin from ~/.orcbot/plugins/my-plugin
SkillsManager: Registered skill 'my_skill' from plugin 'my-plugin'
```

### View Loaded Plugins

```
User: "Show me all loaded skills"

Bot:  Skills & Tools (87 total)
      
      - Core: 75
      - Plugins: 12
      
      Active Plugins:
      • hello_world
      • get_weather
      • parse_csv
      ...
```

## Best Practices

<Card title="Error Handling" icon="triangle-exclamation">
  * Always return `{ success: boolean }` structure
  * Provide detailed error messages
  * Log errors for debugging
  * Validate input parameters
</Card>

<Card title="Security" icon="lock">
  * Never hardcode secrets in plugins
  * Use environment variables for sensitive data
  * Validate and sanitize all inputs
  * Use `requiresAdmin` for dangerous operations
  * Implement rate limiting for API calls
</Card>

<Card title="Performance" icon="bolt">
  * Use async/await for I/O operations
  * Cache expensive computations
  * Set reasonable timeouts
  * Clean up resources (close connections)
</Card>

<Card title="Documentation" icon="book">
  * Include clear `description` and `usage`
  * Create SKILL.md with examples
  * Document all parameters
  * Show example return values
  * List environment requirements
</Card>

## Advanced: Context Access

Access OrcBot internals from your plugin:

```javascript theme={null}
module.exports = [
  {
    name: 'advanced_skill',
    description: 'Skill with access to agent context',
    usage: 'advanced_skill()',
    handler: async (args, context) => {
      // Access agent instance
      const agent = context?.agent;
      if (!agent) {
        return { success: false, error: 'No agent context available' };
      }

      // Read memory
      const recentMemories = agent.memory.getRecentContext(10);
      
      // Access config
      const modelName = agent.config.get('modelName');
      
      // Call other skills
      const searchResult = await agent.skills.execute('web_search', {
        query: 'latest news'
      });
      
      // Push new task
      await agent.pushTask('Follow up on search results', 5);
      
      return {
        success: true,
        memories: recentMemories.length,
        model: modelName,
        searchResults: searchResult
      };
    }
  }
];
```

<Warning>
  Direct agent access is powerful but bypasses safety checks. Use responsibly.
</Warning>

## Publishing Plugins

Share your plugins with the community:

1. **Create GitHub repo:**
   ```bash theme={null}
   mkdir orcbot-plugin-weather
   cd orcbot-plugin-weather
   git init
   ```

2. **Add README:**

   ````markdown theme={null}
   # OrcBot Weather Plugin

   Get weather data from OpenWeather API.

   ## Installation

   ```bash
   cd ~/.orcbot/plugins
   git clone https://github.com/yourusername/orcbot-plugin-weather weather-api
   cd weather-api
   npm install
   ````

   ## Configuration

   Set environment variable:

   ```bash theme={null}
   export OPENWEATHER_API_KEY="your-key"
   ```

   ```
   ```

3. **Publish to npm (optional):**
   ```bash theme={null}
   npm publish --access public
   ```

## Troubleshooting

### Plugin Not Loading

<Steps>
  <Step title="Check Location">
    ```bash theme={null}
    ls -la ~/.orcbot/plugins/my-plugin/
    # Should have index.js
    ```
  </Step>

  <Step title="Check Syntax">
    ```bash theme={null}
    node -c ~/.orcbot/plugins/my-plugin/index.js
    # Should show no errors
    ```
  </Step>

  <Step title="Check Exports">
    ```bash theme={null}
    node -e "console.log(require('~/.orcbot/plugins/my-plugin'))"
    # Should show array of skills
    ```
  </Step>

  <Step title="Check Logs">
    ```bash theme={null}
    tail -f ~/.orcbot/logs/orcbot.log | grep -i plugin
    ```
  </Step>
</Steps>

### Skill Not Recognized

If OrcBot doesn't recognize your skill:

1. Check skill name format (lowercase, underscores)
2. Verify `module.exports` is an array
3. Ensure `name`, `description`, `usage`, and `handler` are present
4. Restart OrcBot or trigger hot-reload

### Dependencies Not Found

```bash theme={null}
# Install dependencies in plugin directory
cd ~/.orcbot/plugins/my-plugin
npm install

# Or add to package.json and install
npm init -y
npm install csv-parse
```
