Skip to main content

Overview

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

Plugin System Features

Hot-Loading

  • Zero restarts needed
  • Loaded at runtime from ~/.orcbot/plugins/
  • Changes detected automatically

Self-Repair

  • If a plugin fails, OrcBot attempts automatic repair
  • Uses self_repair_skill to fix broken code
  • Error isolation prevents crashes

Security

  • Allow/deny lists for plugin control
  • Sandbox execution context
  • Safe mode disables plugin loading

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

1

Create Plugin Directory

mkdir -p ~/.orcbot/plugins/hello-world
cd ~/.orcbot/plugins/hello-world
2

Create index.js

// ~/.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.`
      };
    }
  }
];
3

Test the Plugin

# 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."

Plugin API Reference

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:
{
  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

// ~/.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

// ~/.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

// ~/.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

// ~/.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:
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:
# 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

# Disable all plugins
safeMode: true

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

Plugin Metadata

Add custom metadata to your plugins:
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:
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

# 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:
// ~/.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:
node ~/.orcbot/plugins/weather-api/test.js

Debugging Plugins

Enable Debug Logs

logLevel: debug

Check Plugin Loading

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

Error Handling

  • Always return { success: boolean } structure
  • Provide detailed error messages
  • Log errors for debugging
  • Validate input parameters

Security

  • 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

Performance

  • Use async/await for I/O operations
  • Cache expensive computations
  • Set reasonable timeouts
  • Clean up resources (close connections)

Documentation

  • Include clear description and usage
  • Create SKILL.md with examples
  • Document all parameters
  • Show example return values
  • List environment requirements

Advanced: Context Access

Access OrcBot internals from your plugin:
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
      };
    }
  }
];
Direct agent access is powerful but bypasses safety checks. Use responsibly.

Publishing Plugins

Share your plugins with the community:
  1. Create GitHub repo:
    mkdir orcbot-plugin-weather
    cd orcbot-plugin-weather
    git init
    
  2. Add README:
    # 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:
    export OPENWEATHER_API_KEY="your-key"
    
  3. Publish to npm (optional):
    npm publish --access public
    

Troubleshooting

Plugin Not Loading

1

Check Location

ls -la ~/.orcbot/plugins/my-plugin/
# Should have index.js
2

Check Syntax

node -c ~/.orcbot/plugins/my-plugin/index.js
# Should show no errors
3

Check Exports

node -e "console.log(require('~/.orcbot/plugins/my-plugin'))"
# Should show array of skills
4

Check Logs

tail -f ~/.orcbot/logs/orcbot.log | grep -i plugin

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

# 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