Channel Plugin SDK
Channel Plugin SDK (@elftia/channel-sdk) defines all interfaces and types that Channel plugins must follow. Source code is located in packages/channel-sdk/src/.
ChannelPlugin Interface
Every Channel plugin must implement the ChannelPlugin interface:
interface ChannelPlugin {
/** Unique Channel type identifier, e.g. 'discord', 'telegram' */
readonly type: string;
// ─── Lifecycle (required) ───────────────────
/** Establish connection with decrypted credentials */
connect(
credentials: Record<string, string>,
options?: Record<string, unknown>,
): Promise<void>;
/** Disconnect and release resources */
disconnect(): Promise<void>;
/** Whether currently connected */
isConnected(): boolean;
// ─── Message Sending (required) ─────────────
/** Send text message to specified chat */
sendMessage(
chatId: string,
text: string,
options?: SendOptions,
): Promise<void>;
// ─── Optional Capabilities ──────────────────
/** Send typing indicator */
sendTyping?(chatId: string): Promise<void>;
/** Get list of available chats/channels */
getChats?(): Promise<ChatInfo[]>;
/** Send file attachment */
sendAttachment?(
chatId: string,
attachment: AttachmentInput,
): Promise<void>;
/** Validate credential validity (without establishing persistent connection) */
validateCredentials?(
credentials: Record<string, string>,
): Promise<{ valid: boolean; error?: string }>;
/** Dispose plugin instance and release all resources */
dispose?(): Promise<void>;
// ─── AI Context Injection ───────────────────
/**
* Return Channel-specific system prompt fragment.
* When a message comes from this Channel, the returned text will be appended
* to the Agent's base system prompt.
*/
getSystemPrompt?(
context: SystemPromptContext,
): string | undefined;
}
ChannelPluginFactory
The plugin's default export must be a factory function:
type ChannelPluginFactory = (context: ChannelPluginContext) => ChannelPlugin;
Elftia calls this function when creating a Channel instance, passing in ChannelPluginContext. The plugin communicates with the core system through Context.
ChannelPluginContext
Context is the sole communication bridge between the plugin and Elftia core:
interface ChannelPluginContext {
/** Channel instance ID */
readonly channelId: string;
/** Channel display name */
readonly displayName: string;
// ─── Message Reporting ──────────────────────
/** Report received inbound message */
emitMessage(msg: InboundMessage): void;
/** Report connection status change */
emitStatusChange(status: ChannelStatus): void;
/** Report error */
emitError(error: Error): void;
/** Send custom event to frontend (e.g. QR code pairing) */
emitEvent(eventType: string, data: unknown): void;
// ─── Logging ────────────────────────────────
/** Structured logging */
log: PluginLogger;
// ─── Storage ────────────────────────────────
/** Plugin-level K-V persistent storage */
storage: PluginStorage;
// ─── File System ─────────────────────────────
/**
* Plugin-specific data directory (persistent across sessions).
* Framework automatically creates it. Plugin can use for file downloads,
* caching, temporary files, etc.
* Example path: {userData}/channel-data/{channelId}/
*/
readonly dataDir: string;
}
PluginLogger
interface PluginLogger {
info(msg: string, meta?: Record<string, unknown>): void;
warn(msg: string, meta?: Record<string, unknown>): void;
error(msg: string, error?: Error): void;
debug(msg: string, meta?: Record<string, unknown>): void;
}
Log output automatically adds [Channel:{channelId}] prefix and integrates with Elftia's main logging system.
PluginStorage
interface PluginStorage {
get<T = unknown>(key: string): Promise<T | null>;
set(key: string, value: unknown): Promise<void>;
delete(key: string): Promise<void>;
}
Uses SQLite database for persistence at the bottom layer with in-memory caching for faster reads. Values are stored as JSON-serialized.
InboundMessage
Inbound message format reported by plugin via context.emitMessage():
interface InboundMessage {
/** Message unique ID (platform's original ID) */
id: string;
/** Chat/channel ID */
chatId: string;
/** Sender's platform ID */
senderId: string;
/** Sender's display name */
senderName: string;
/** Message text content */
content: string;
/** ISO 8601 timestamp */
timestamp: string;
/** Whether it is a message from the bot itself */
isFromMe: boolean;
/** Whether it is a group message */
isGroup: boolean;
/** ID of message being replied to */
replyToId?: string;
/** Attachment list */
attachments?: ChannelAttachment[];
/** Platform-specific metadata */
metadata?: Record<string, unknown>;
}
SendOptions
Optional parameters when sending messages:
interface SendOptions {
/** Reply to specified message */
replyToId?: string;
/** Message format */
format?: 'text' | 'markdown' | 'html';
/** Forum post/thread ID */
threadId?: string;
/** Send silently (no notification) */
silent?: boolean;
/** Caption text for media attachments */
caption?: string;
/** Platform-specific reply markup (inline keyboard, etc.) */
replyMarkup?: unknown;
}
AttachmentInput
Input format for sending attachments:
interface AttachmentInput {
type: 'image' | 'file' | 'audio' | 'video';
/** Attachment content: Buffer or URL string */
data: Buffer | string;
/** File name */
name: string;
/** MIME type */
mimeType?: string;
}
SystemPromptContext
Context passed to getSystemPrompt():
interface SystemPromptContext {
/** Chat type (e.g. 'group', 'dm') */
chatType: string;
/** Chat ID */
chatId: string;
/** Sender ID */
senderId: string;
/** Sender name */
senderName: string;
/** Whether it is a group */
isGroup: boolean;
/** Platform-specific metadata */
metadata?: Record<string, unknown>;
}
Manifest File (elftia-channel.json)
Every plugin must include an elftia-channel.json manifest file in its root directory:
{
"name": "@elftia/channel-discord",
"type": "discord",
"displayName": "Discord",
"version": "1.0.0",
"description": "Discord bot integration for Elftia",
"author": "Elftia Team",
"icon": "icon.svg",
"entry": "dist/index.cjs",
"credentials": [
{
"key": "botToken",
"label": "Bot Token",
"type": "password",
"required": true,
"placeholder": "MTA...",
"helpText": "Get from Discord Developer Portal",
"helpUrl": "https://discord.com/developers/applications"
}
],
"options": [
{
"key": "autoReconnect",
"label": "Auto Reconnect",
"type": "toggle",
"default": true,
"helpText": "Automatically try to reconnect when connection is lost"
}
],
"capabilities": {
"typing": true,
"reactions": true,
"attachments": true,
"threads": true,
"groupChat": true,
"sendOnly": false
},
"maxMessageLength": 2000,
"platformUrl": "https://discord.com",
"minElftiaVersion": "0.5.0"
}
Manifest Fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | npm package name (for identification and deduplication) |
type | string | Yes | Channel type identifier (e.g. discord) |
displayName | string | Yes | Human-readable platform name |
version | string | Yes | Semantic version |
description | string | No | Plugin description |
author | string | No | Author name |
icon | string | No | Icon path (relative to plugin directory) or SVG string |
entry | string | Yes | Entry file path (relative to plugin directory) |
credentials | CredentialField[] | Yes | Credential field definitions (for UI form rendering) |
options | OptionField[] | No | Non-credential configuration options (e.g. toggle switches) |
capabilities | ChannelCapabilities | No | Platform capability declarations |
maxMessageLength | number | No | Platform message length limit |
platformUrl | string | No | Platform's official website |
minElftiaVersion | string | No | Minimum required Elftia version |
CredentialField
interface CredentialField {
key: string; // Field key name
label: string; // Display label
type: 'text' | 'password' | 'textarea';
required: boolean;
placeholder?: string;
helpText?: string; // Input hint
helpUrl?: string; // Help link
}
OptionField
interface OptionField {
key: string;
label: string;
type: 'toggle'; // Currently only toggle type supported
default?: boolean;
helpText?: string;
}
ChannelCapabilities
interface ChannelCapabilities {
typing?: boolean; // Typing indicator
reactions?: boolean; // Emoji reactions
attachments?: boolean; // File attachments
threads?: boolean; // Posts/threads
groupChat?: boolean; // Group chat
sendOnly?: boolean; // Send-only (no inbound messages)
}
sendOnly is for desktop apps that cannot host Webhooks — the plugin can only actively send messages and cannot receive inbound messages.
Next Steps
- Message Routing and Security Pipeline — Complete processing flow from plugin to Agent
- Writing a Channel Plugin — Implement a Channel plugin from scratch