Plugin SDK
The SimpleClaw Plugin SDK enables you to extend the Gateway with custom channels, providers, and tools.Plugin Types
SimpleClaw supports two plugin categories:- Channel Plugins - Messaging platform integrations (WhatsApp, Telegram, etc.)
- Provider Plugins - LLM providers, memory systems, task runners
Installation
The Plugin SDK is included with SimpleClaw:import {
// Channel plugin types
type ChannelPlugin,
type ChannelConfigAdapter,
type ChannelOutboundAdapter,
// Provider plugin types
type SimpleClawPluginApi,
type ProviderAuthContext,
// Utilities
buildMediaPayload,
normalizeWebhookPath,
registerWebhookTarget,
} from "simpleclaw/plugin-sdk";
Channel Plugin Structure
Basic Example
import type { ChannelPlugin } from "simpleclaw/plugin-sdk";
export const customChannel: ChannelPlugin = {
id: "custom",
meta: {
label: "Custom Chat",
detailLabel: "Custom messaging platform",
systemImage: "message.circle",
},
capabilities: {
outbound: true,
groups: true,
threads: false,
media: ["image", "video", "audio"],
},
config: {
// Configuration adapter
resolveDefaultAccountId: (config) => "default",
resolveAccount: (config, accountId) => ({
accountId,
enabled: true,
}),
},
outbound: {
// Message sending
async send(ctx, message) {
// Send message to platform API
await fetch(`https://api.example.com/send`, {
method: "POST",
body: JSON.stringify({
to: message.to,
text: message.text,
}),
});
},
},
status: {
// Health checks
async buildAccountSnapshot(ctx, accountId) {
return {
accountId,
configured: true,
connected: true,
running: true,
};
},
},
};
Plugin SDK Exports
The SDK provides helpers for common plugin tasks:Media Handling
import { buildMediaPayload, type MediaPayload } from "simpleclaw/plugin-sdk";
// Convert media to standard format
const media: MediaPayload = buildMediaPayload({
url: "https://example.com/image.jpg",
mime: "image/jpeg",
size: 12345,
filename: "photo.jpg",
});
Webhook Registration
import {
registerWebhookTarget,
normalizeWebhookPath,
} from "simpleclaw/plugin-sdk";
// Register webhook endpoint
registerWebhookTarget({
channelId: "custom",
accountId: "default",
path: normalizeWebhookPath("/webhooks/custom"),
handler: async (req, res) => {
const body = await req.json();
// Process webhook
res.json({ ok: true });
},
});
Account Helpers
import { createAccountListHelpers } from "simpleclaw/plugin-sdk";
const helpers = createAccountListHelpers({
configPrefix: "channels.custom",
accountSchema: AccountConfigSchema,
});
// List all configured accounts
const accountIds = helpers.listAccountIds(config);
Status Helpers
import {
buildBaseAccountStatusSnapshot,
collectStatusIssuesFromLastError,
} from "simpleclaw/plugin-sdk";
// Build standard account snapshot
const snapshot = buildBaseAccountStatusSnapshot({
accountId: "default",
enabled: true,
configured: true,
connected: state.connected,
lastError: state.error?.message,
});
Configuration Schema
Define config schema with Zod:import { z } from "zod";
import { buildChannelConfigSchema } from "simpleclaw/plugin-sdk";
const AccountConfigSchema = z.object({
apiKey: z.string(),
webhookUrl: z.string().optional(),
});
const CustomConfigSchema = z.object({
enabled: z.boolean().default(false),
accounts: z.record(AccountConfigSchema),
});
export const configSchema = buildChannelConfigSchema({
schema: CustomConfigSchema.shape,
uiHints: {
"accounts.*.apiKey": {
label: "API Key",
sensitive: true,
help: "Get this from Custom Chat dashboard",
},
},
});
Outbound Message Delivery
import type { ChannelOutboundAdapter } from "simpleclaw/plugin-sdk";
export const outbound: ChannelOutboundAdapter = {
async send(ctx, message) {
const { to, text, media } = message;
const account = ctx.account;
// Send text message
if (text) {
await api.sendText({
apiKey: account.apiKey,
recipient: to,
message: text,
});
}
// Send media attachments
if (media?.length) {
for (const item of media) {
await api.sendMedia({
apiKey: account.apiKey,
recipient: to,
url: item.url,
caption: item.caption,
});
}
}
},
async sendReaction(ctx, params) {
await api.react({
apiKey: ctx.account.apiKey,
messageId: params.messageId,
emoji: params.emoji,
});
},
};
Security Adapter
Implement allowlist and DM policy:import type { ChannelSecurityAdapter } from "simpleclaw/plugin-sdk";
import { isNormalizedSenderAllowed } from "simpleclaw/plugin-sdk";
export const security: ChannelSecurityAdapter = {
resolveSenderAllowed: (ctx, sender) => {
const allowFrom = ctx.account.allowFrom || [];
return isNormalizedSenderAllowed({
senderId: sender.id,
allowFrom,
});
},
resolveDmPolicy: (ctx) => {
return ctx.account.dmPolicy || "pairing";
},
};
Gateway Adapter
Handle incoming messages:import type { ChannelGatewayAdapter } from "simpleclaw/plugin-sdk";
export const gateway: ChannelGatewayAdapter = {
async start(ctx) {
const account = ctx.account;
// Connect to platform API
const client = await connect(account.apiKey);
// Listen for incoming messages
client.on("message", async (msg) => {
await ctx.handleInbound({
from: msg.sender.id,
text: msg.text,
timestamp: msg.timestamp,
});
});
},
async stop(ctx) {
// Cleanup
await client?.disconnect();
},
};
Plugin Packaging
Create a plugin package:{
"name": "@simpleclaw/channel-custom",
"version": "1.0.0",
"main": "dist/index.js",
"simpleclaw": {
"extensions": [
{
"type": "channel",
"id": "custom",
"entry": "./dist/index.js"
}
]
},
"dependencies": {
"simpleclaw": "^2026.3.0"
}
}
Provider Plugin Example
import type {
SimpleClawPluginService,
SimpleClawPluginServiceContext,
} from "simpleclaw/plugin-sdk";
export const customProvider: SimpleClawPluginService = {
id: "custom-llm",
async init(ctx: SimpleClawPluginServiceContext) {
// Initialize provider
},
async auth(ctx) {
// OAuth flow or API key validation
return {
success: true,
profile: {
id: "user-123",
name: "User Name",
},
};
},
async invoke(ctx, request) {
// Handle LLM request
const response = await fetch("https://api.example.com/chat", {
method: "POST",
body: JSON.stringify(request),
});
return response.json();
},
};
Testing Plugins
import { describe, test, expect } from "vitest";
import { customChannel } from "./index.js";
describe("custom channel", () => {
test("sends messages", async () => {
const ctx = createMockContext();
await customChannel.outbound.send(ctx, {
to: "user-123",
text: "Hello!",
});
expect(mockApi.sendText).toHaveBeenCalledWith({
recipient: "user-123",
message: "Hello!",
});
});
});
Next Steps
Channel Plugins
Deep dive into channel plugin development
Provider Plugins
Build custom LLM and tool providers