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
Create Plugin Directory
mkdir -p ~/.orcbot/plugins/hello-world
cd ~/.orcbot/plugins/hello-world
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.`
};
}
}
];
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
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:
Isolates the error
Attempts self_repair_skill to fix syntax/logic issues
Retries loading
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
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:
Create GitHub repo:
mkdir orcbot-plugin-weather
cd orcbot-plugin-weather
git init
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"
Publish to npm (optional):
npm publish --access public
Troubleshooting
Plugin Not Loading
Check Location
ls -la ~/.orcbot/plugins/my-plugin/
# Should have index.js
Check Syntax
node -c ~/.orcbot/plugins/my-plugin/index.js
# Should show no errors
Check Exports
node -e "console.log(require('~/.orcbot/plugins/my-plugin'))"
# Should show array of skills
Check Logs
tail -f ~/.orcbot/logs/orcbot.log | grep -i plugin
Skill Not Recognized
If OrcBot doesn’t recognize your skill:
Check skill name format (lowercase, underscores)
Verify module.exports is an array
Ensure name, description, usage, and handler are present
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