ClawdHub Web UI Integration: - Add ClawdHub API client (search, details, install, updates) - Add Gateway RPC handlers with auth scopes - Add Marketplace tab with browse/installed views - Add state management and Lit HTML components Documentation: - Add feature maturity matrix classifying 100+ features - Add skills contribution guide (SKILL.md format, gating, publishing) - Add plugins contribution guide (manifest, API, distribution) - Update CONTRIBUTING.md with links to new guides - Add Contributing section to docs navigation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
640 lines
14 KiB
Markdown
640 lines
14 KiB
Markdown
---
|
|
title: Contributing Plugins
|
|
description: How to create, test, and distribute Clawdbot plugins
|
|
---
|
|
|
|
# Contributing Plugins
|
|
|
|
This guide walks you through creating plugins that extend Clawdbot with new channels, tools, commands, and services.
|
|
|
|
## What is a Plugin?
|
|
|
|
A plugin is a TypeScript module that extends Clawdbot at runtime. Plugins can:
|
|
|
|
- Register new messaging channels (like Matrix, Nostr, MS Teams)
|
|
- Add agent tools the AI can invoke
|
|
- Register Gateway RPC methods
|
|
- Add CLI commands
|
|
- Run background services
|
|
- Ship bundled skills
|
|
- Register auto-reply commands
|
|
|
|
Plugins run **in-process** with the Gateway, so treat them as trusted code.
|
|
|
|
## Quick Start
|
|
|
|
Create a minimal plugin in 5 minutes:
|
|
|
|
```bash
|
|
# Create plugin directory
|
|
mkdir -p ~/.clawdbot/extensions/hello-world
|
|
|
|
# Create the manifest
|
|
cat > ~/.clawdbot/extensions/hello-world/clawdbot.plugin.json << 'EOF'
|
|
{
|
|
"id": "hello-world",
|
|
"name": "Hello World",
|
|
"description": "A simple example plugin",
|
|
"configSchema": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"greeting": {
|
|
"type": "string",
|
|
"default": "Hello"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
EOF
|
|
|
|
# Create the plugin code
|
|
cat > ~/.clawdbot/extensions/hello-world/index.ts << 'EOF'
|
|
export default function register(api) {
|
|
const config = api.pluginConfig ?? {};
|
|
const greeting = config.greeting ?? "Hello";
|
|
|
|
// Register a Gateway RPC method
|
|
api.registerGatewayMethod("hello.greet", ({ params, respond }) => {
|
|
const name = params?.name ?? "World";
|
|
respond(true, { message: `${greeting}, ${name}!` });
|
|
});
|
|
|
|
// Register an auto-reply command
|
|
api.registerCommand({
|
|
name: "hello",
|
|
description: "Say hello",
|
|
handler: () => ({ text: `${greeting} from the plugin!` }),
|
|
});
|
|
|
|
api.logger.info("[hello-world] Plugin loaded");
|
|
}
|
|
EOF
|
|
```
|
|
|
|
Restart the Gateway to load your plugin.
|
|
|
|
## Plugin Structure
|
|
|
|
### Directory Layout
|
|
|
|
```
|
|
my-plugin/
|
|
├── clawdbot.plugin.json # Required: manifest
|
|
├── index.ts # Required: entry point
|
|
├── package.json # Optional: for npm distribution
|
|
├── src/ # Optional: source files
|
|
│ ├── cli.ts
|
|
│ ├── tools.ts
|
|
│ └── types.ts
|
|
├── skills/ # Optional: bundled skills
|
|
│ └── my-skill/
|
|
│ └── SKILL.md
|
|
└── README.md
|
|
```
|
|
|
|
### Plugin Manifest (clawdbot.plugin.json)
|
|
|
|
Every plugin **must** have a manifest:
|
|
|
|
```json
|
|
{
|
|
"id": "my-plugin",
|
|
"name": "My Plugin",
|
|
"description": "Does amazing things",
|
|
"version": "1.0.0",
|
|
"configSchema": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"apiKey": { "type": "string" },
|
|
"enabled": { "type": "boolean" }
|
|
}
|
|
},
|
|
"uiHints": {
|
|
"apiKey": { "label": "API Key", "sensitive": true },
|
|
"enabled": { "label": "Enable Feature" }
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Required Fields
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `id` | string | Unique plugin identifier |
|
|
| `configSchema` | object | JSON Schema for config validation |
|
|
|
|
#### Optional Fields
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `name` | string | Display name |
|
|
| `description` | string | Short summary |
|
|
| `version` | string | Semantic version |
|
|
| `kind` | string | Plugin category (e.g., `"memory"`) |
|
|
| `channels` | string[] | Channel IDs this plugin registers |
|
|
| `providers` | string[] | Provider IDs this plugin registers |
|
|
| `skills` | string[] | Skill directories to load |
|
|
| `uiHints` | object | UI labels and hints |
|
|
|
|
### Entry Point (index.ts)
|
|
|
|
Plugins export either a function or an object:
|
|
|
|
```typescript
|
|
// Function style (simple)
|
|
export default function register(api) {
|
|
// Registration code
|
|
}
|
|
|
|
// Object style (with metadata)
|
|
export default {
|
|
id: "my-plugin",
|
|
name: "My Plugin",
|
|
configSchema: { /* ... */ },
|
|
register(api) {
|
|
// Registration code
|
|
},
|
|
};
|
|
```
|
|
|
|
## Plugin API
|
|
|
|
The `api` object provides access to Clawdbot internals:
|
|
|
|
### Core Properties
|
|
|
|
```typescript
|
|
api.config // Full Clawdbot configuration
|
|
api.pluginConfig // This plugin's config (plugins.entries.<id>.config)
|
|
api.logger // Scoped logger instance
|
|
api.runtime // Runtime helpers (TTS, etc.)
|
|
```
|
|
|
|
### Registration Methods
|
|
|
|
#### Gateway RPC Methods
|
|
|
|
```typescript
|
|
api.registerGatewayMethod("myplugin.action", async ({ params, respond }) => {
|
|
// params: request parameters
|
|
// respond(ok: boolean, payload?: any): send response
|
|
|
|
try {
|
|
const result = await doSomething(params);
|
|
respond(true, { data: result });
|
|
} catch (err) {
|
|
respond(false, { error: err.message });
|
|
}
|
|
});
|
|
```
|
|
|
|
#### Agent Tools
|
|
|
|
```typescript
|
|
import { Type } from "@sinclair/typebox";
|
|
|
|
api.registerTool({
|
|
name: "my_tool",
|
|
description: "Does something useful",
|
|
inputSchema: Type.Object({
|
|
query: Type.String({ description: "Search query" }),
|
|
limit: Type.Optional(Type.Number({ description: "Max results" })),
|
|
}),
|
|
handler: async ({ params, context }) => {
|
|
const results = await search(params.query, params.limit);
|
|
return { results };
|
|
},
|
|
});
|
|
```
|
|
|
|
#### CLI Commands
|
|
|
|
```typescript
|
|
api.registerCli(({ program }) => {
|
|
program
|
|
.command("myplugin")
|
|
.description("My plugin command")
|
|
.option("-v, --verbose", "Verbose output")
|
|
.action((options) => {
|
|
console.log("Running with options:", options);
|
|
});
|
|
}, { commands: ["myplugin"] });
|
|
```
|
|
|
|
#### Auto-Reply Commands
|
|
|
|
Commands that respond without invoking the AI:
|
|
|
|
```typescript
|
|
api.registerCommand({
|
|
name: "mystatus",
|
|
description: "Show plugin status",
|
|
acceptsArgs: false,
|
|
requireAuth: true,
|
|
handler: (ctx) => ({
|
|
text: `Plugin active on ${ctx.channel}`,
|
|
}),
|
|
});
|
|
```
|
|
|
|
Handler context:
|
|
|
|
- `ctx.senderId` - Sender ID
|
|
- `ctx.channel` - Channel name
|
|
- `ctx.isAuthorizedSender` - Auth status
|
|
- `ctx.args` - Command arguments (if `acceptsArgs: true`)
|
|
- `ctx.config` - Clawdbot config
|
|
|
|
#### Background Services
|
|
|
|
```typescript
|
|
api.registerService({
|
|
id: "my-background-service",
|
|
start: async () => {
|
|
api.logger.info("Service starting");
|
|
// Initialize background work
|
|
},
|
|
stop: async () => {
|
|
api.logger.info("Service stopping");
|
|
// Cleanup
|
|
},
|
|
});
|
|
```
|
|
|
|
#### Messaging Channels
|
|
|
|
```typescript
|
|
api.registerChannel({
|
|
plugin: {
|
|
id: "mychannel",
|
|
meta: {
|
|
id: "mychannel",
|
|
label: "My Channel",
|
|
selectionLabel: "My Channel (API)",
|
|
docsPath: "/channels/mychannel",
|
|
blurb: "Custom messaging channel",
|
|
aliases: ["mc"],
|
|
},
|
|
capabilities: { chatTypes: ["direct", "group"] },
|
|
config: {
|
|
listAccountIds: (cfg) =>
|
|
Object.keys(cfg.channels?.mychannel?.accounts ?? {}),
|
|
resolveAccount: (cfg, accountId) =>
|
|
cfg.channels?.mychannel?.accounts?.[accountId ?? "default"],
|
|
},
|
|
outbound: {
|
|
deliveryMode: "direct",
|
|
sendText: async ({ text, target }) => {
|
|
await deliverMessage(text, target);
|
|
return { ok: true };
|
|
},
|
|
},
|
|
},
|
|
});
|
|
```
|
|
|
|
#### Model Providers
|
|
|
|
```typescript
|
|
api.registerProvider({
|
|
id: "acme",
|
|
label: "AcmeAI",
|
|
auth: [
|
|
{
|
|
id: "oauth",
|
|
label: "OAuth Login",
|
|
kind: "oauth",
|
|
run: async (ctx) => {
|
|
// OAuth flow
|
|
return {
|
|
profiles: [{ profileId: "acme:default", credential: { ... } }],
|
|
defaultModel: "acme/model-1",
|
|
};
|
|
},
|
|
},
|
|
],
|
|
});
|
|
```
|
|
|
|
## Configuration
|
|
|
|
### User Configuration
|
|
|
|
Users configure your plugin in `~/.clawdbot/clawdbot.json`:
|
|
|
|
```json5
|
|
{
|
|
plugins: {
|
|
entries: {
|
|
"my-plugin": {
|
|
enabled: true,
|
|
config: {
|
|
apiKey: "sk-xxx",
|
|
endpoint: "https://api.example.com"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Config Schema
|
|
|
|
Define validation rules in the manifest:
|
|
|
|
```json
|
|
{
|
|
"configSchema": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"required": ["apiKey"],
|
|
"properties": {
|
|
"apiKey": {
|
|
"type": "string",
|
|
"minLength": 1
|
|
},
|
|
"endpoint": {
|
|
"type": "string",
|
|
"format": "uri"
|
|
},
|
|
"timeout": {
|
|
"type": "integer",
|
|
"minimum": 1000,
|
|
"default": 30000
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### UI Hints
|
|
|
|
Help the Control UI render better forms:
|
|
|
|
```json
|
|
{
|
|
"uiHints": {
|
|
"apiKey": {
|
|
"label": "API Key",
|
|
"placeholder": "sk-...",
|
|
"sensitive": true
|
|
},
|
|
"endpoint": {
|
|
"label": "API Endpoint",
|
|
"help": "Custom endpoint URL",
|
|
"advanced": true
|
|
},
|
|
"timeout": {
|
|
"label": "Timeout (ms)",
|
|
"advanced": true
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
UI hint fields:
|
|
|
|
- `label` - Display label
|
|
- `placeholder` - Input placeholder
|
|
- `help` - Help text
|
|
- `sensitive` - Mark as password field
|
|
- `advanced` - Hide in basic view
|
|
|
|
## Bundling Skills
|
|
|
|
Plugins can ship skills:
|
|
|
|
```json
|
|
{
|
|
"id": "my-plugin",
|
|
"skills": ["./skills/my-skill"]
|
|
}
|
|
```
|
|
|
|
Skills are loaded when the plugin is enabled and follow standard skill precedence.
|
|
|
|
## Plugin Hooks
|
|
|
|
Plugins can register event hooks:
|
|
|
|
```typescript
|
|
import { registerPluginHooksFromDir } from "clawdbot/plugin-sdk";
|
|
|
|
export default function register(api) {
|
|
registerPluginHooksFromDir(api, "./hooks");
|
|
}
|
|
```
|
|
|
|
Hook directories follow the standard hook structure (`HOOK.md` + `handler.ts`).
|
|
|
|
## Testing
|
|
|
|
### Local Testing
|
|
|
|
1. Create your plugin in `~/.clawdbot/extensions/`
|
|
2. Restart the Gateway
|
|
3. Check it loaded: `clawdbot plugins list`
|
|
4. Test your features
|
|
|
|
### Development Mode
|
|
|
|
Link your plugin for live development:
|
|
|
|
```bash
|
|
clawdbot plugins install -l ./my-plugin
|
|
```
|
|
|
|
The `-l` flag creates a symlink instead of copying.
|
|
|
|
### Unit Tests
|
|
|
|
Ship tests with your plugin:
|
|
|
|
```typescript
|
|
// src/my-plugin.test.ts
|
|
import { describe, it, expect } from "vitest";
|
|
import { myFunction } from "./my-function.js";
|
|
|
|
describe("myFunction", () => {
|
|
it("should process input correctly", () => {
|
|
expect(myFunction("test")).toBe("expected");
|
|
});
|
|
});
|
|
```
|
|
|
|
Run with Vitest or your preferred test runner.
|
|
|
|
## Distribution
|
|
|
|
### npm Publishing
|
|
|
|
Package your plugin for npm:
|
|
|
|
```json
|
|
// package.json
|
|
{
|
|
"name": "@yourname/clawdbot-my-plugin",
|
|
"version": "1.0.0",
|
|
"main": "dist/index.js",
|
|
"types": "dist/index.d.ts",
|
|
"files": ["dist", "clawdbot.plugin.json"],
|
|
"clawdbot": {
|
|
"extensions": ["./dist/index.js"]
|
|
},
|
|
"scripts": {
|
|
"build": "tsc",
|
|
"prepublishOnly": "npm run build"
|
|
}
|
|
}
|
|
```
|
|
|
|
Users install with:
|
|
|
|
```bash
|
|
clawdbot plugins install @yourname/clawdbot-my-plugin
|
|
```
|
|
|
|
### Local Distribution
|
|
|
|
Distribute as a tarball or zip:
|
|
|
|
```bash
|
|
# Create archive
|
|
tar -czf my-plugin.tar.gz my-plugin/
|
|
|
|
# Install
|
|
clawdbot plugins install ./my-plugin.tar.gz
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### Naming
|
|
|
|
- Plugin ID: kebab-case (`my-plugin`)
|
|
- npm package: `@scope/clawdbot-*` or `clawdbot-*`
|
|
- RPC methods: `pluginid.action`
|
|
- Tools: `snake_case`
|
|
|
|
### Error Handling
|
|
|
|
```typescript
|
|
api.registerGatewayMethod("myplugin.action", async ({ params, respond }) => {
|
|
try {
|
|
// Validate required params
|
|
if (!params?.id) {
|
|
respond(false, { error: "id is required" });
|
|
return;
|
|
}
|
|
|
|
const result = await doWork(params.id);
|
|
respond(true, { data: result });
|
|
} catch (err) {
|
|
api.logger.error("[myplugin] Action failed:", err);
|
|
respond(false, {
|
|
error: err instanceof Error ? err.message : "Unknown error"
|
|
});
|
|
}
|
|
});
|
|
```
|
|
|
|
### Logging
|
|
|
|
Use the scoped logger:
|
|
|
|
```typescript
|
|
api.logger.debug("[myplugin] Debug info");
|
|
api.logger.info("[myplugin] Starting");
|
|
api.logger.warn("[myplugin] Warning message");
|
|
api.logger.error("[myplugin] Error:", error);
|
|
```
|
|
|
|
### Configuration Defaults
|
|
|
|
Handle missing config gracefully:
|
|
|
|
```typescript
|
|
const config = api.pluginConfig ?? {};
|
|
const timeout = config.timeout ?? 30000;
|
|
const endpoint = config.endpoint ?? "https://api.example.com";
|
|
```
|
|
|
|
### Deprecation Warnings
|
|
|
|
```typescript
|
|
if (config.oldOption !== undefined) {
|
|
api.logger.warn(
|
|
"[myplugin] 'oldOption' is deprecated; use 'newOption' instead"
|
|
);
|
|
}
|
|
```
|
|
|
|
### Cleanup
|
|
|
|
Always clean up resources:
|
|
|
|
```typescript
|
|
let interval: NodeJS.Timeout | null = null;
|
|
|
|
api.registerService({
|
|
id: "my-service",
|
|
start: () => {
|
|
interval = setInterval(checkStatus, 60000);
|
|
},
|
|
stop: () => {
|
|
if (interval) {
|
|
clearInterval(interval);
|
|
interval = null;
|
|
}
|
|
},
|
|
});
|
|
```
|
|
|
|
## Example Plugins
|
|
|
|
Browse official plugins for patterns:
|
|
|
|
| Plugin | Description | Key Patterns |
|
|
|--------|-------------|--------------|
|
|
| [voice-call](https://github.com/clawdbot/clawdbot/tree/main/extensions/voice-call) | Phone calls | RPC methods, tools, CLI, config schema |
|
|
| [matrix](https://github.com/clawdbot/clawdbot/tree/main/extensions/matrix) | Matrix channel | Channel registration, E2EE |
|
|
| [memory-lancedb](https://github.com/clawdbot/clawdbot/tree/main/extensions/memory-lancedb) | Vector memory | Plugin slots, background service |
|
|
| [lobster](https://github.com/clawdbot/clawdbot/tree/main/extensions/lobster) | Natural CLI | Tool registration |
|
|
|
|
## Troubleshooting
|
|
|
|
### Plugin not loading
|
|
|
|
1. Check `clawdbot plugins list`
|
|
2. Verify manifest exists: `clawdbot.plugin.json`
|
|
3. Check for syntax errors in manifest JSON
|
|
4. Look for errors in logs: `clawdbot logs -f`
|
|
5. Run doctor: `clawdbot doctor`
|
|
|
|
### Config validation errors
|
|
|
|
1. Verify JSON Schema in manifest
|
|
2. Check `additionalProperties: false` blocks unknown keys
|
|
3. Test config against schema with a JSON validator
|
|
|
|
### RPC method not found
|
|
|
|
1. Verify method name matches registration
|
|
2. Check plugin is enabled in config
|
|
3. Restart Gateway after changes
|
|
|
|
### Tool not appearing
|
|
|
|
1. Verify tool registration code runs
|
|
2. Check tool name doesn't conflict
|
|
3. Verify schema is valid (no `anyOf`/`oneOf`)
|
|
|
|
## Resources
|
|
|
|
- [Plugin reference](/plugin) - Full API documentation
|
|
- [Plugin manifest](/plugins/manifest) - Manifest schema
|
|
- [Plugin agent tools](/plugins/agent-tools) - Tool authoring guide
|
|
- [Voice Call example](/plugins/voice-call) - Complete plugin example
|
|
- [Feature maturity](/reference/feature-maturity) - Plugin system stability
|