19 KiB
Migration Plan: Moving Twitch Plugin to Clawdbot Core
Source Reference
- Original Plugin Directory:
/Users/jayden/projects/clawdbot-twitch.tidyup- Original Branch:
tidyup- Created By: Claude (Sonnet 4.5) on 2026-01-24
- Issue: External plugin cannot dispatch inbound messages to Clawdbot's agent system
Problem Statement
The Twitch plugin is currently an external plugin that cannot dispatch inbound messages to Clawdbot's agent system because:
- External plugins don't have access to
dispatchInboundMessagefrom the core - The
ChannelGatewayContextdoesn't provide a message dispatch mechanism - Only core channels (built into Clawdbot) can properly route inbound messages
Solution
Move the Twitch plugin into Clawdbot core as a built-in channel, following the same pattern as Discord, WhatsApp, Telegram, etc.
Files in External Plugin (to be migrated)
src/
├── actions.ts # Message action handlers
├── config.ts # Account config resolution
├── outbound.ts # Outbound message adapter
├── plugin.ts # Main plugin definition (twitchPlugin)
├── plugin-sdk.ts # ❌ DELETE - duplicated types, use core instead
├── probe.ts # Connection probing
├── resolver.ts # Username -> user ID resolution
├── send.ts # Send message functions
├── status.ts # Status issues collection
├── twitch-client.ts # Twitch IRC client wrapper
├── types.ts # ❌ DELETE - mostly duplicated types
├── index.ts # ❌ DELETE - plugin wrapper, not needed for core
├── access-control.ts # Access control (allowlist, roles)
├── cli/
│ └── test-connect.ts # CLI command for testing
└── utils/
├── markdown.ts # Markdown stripping for Twitch
└── twitch.ts # Twitch utilities
Tests (all need migration):
├── *.test.ts # All test files
Detailed Migration Steps
Phase 1: Preparation & Analysis
1.1 Audit Duplicated Types
Status: ✅ Complete
The following files contain duplicated types that should be removed and imported from core:
plugin-sdk.ts- Entire file is duplicated (ClawdbotPluginApi, config schema)types.ts- Contains many types duplicated from core:ChannelMeta,ChatCapabilities,ChannelOutboundAdapterChannelOutboundContext,OutboundDeliveryResultChannelPlugin,ChannelAccountSnapshotChannelStatusAdapter,ChannelGatewayAdapterChannelGatewayContext,ChannelLogSinkChannelMessageActionAdapter,ChannelMessageActionContext- Plus plugin-specific wrapper types (PluginAPI, CoreConfig stubs)
Keep in types.ts:
TwitchAccountConfig- Twitch-specific configTwitchChannelConfig- Twitch channel configTwitchRole- Twitch role enumTwitchTarget- Twitch send targetTwitchChatMessage- Twitch message structureTwitchPluginConfig- Plugin-specific config (if still needed)SendResult- Twitch send resultProviderLogger- Simple logger interface (or use core's)
1.2 Document Twitch-Specific Types to Preserve
// Twitch-specific types that MUST be kept
export type TwitchRole = "moderator" | "owner" | "vip" | "subscriber" | "all";
export interface TwitchAccountConfig {
username: string;
token: string;
clientId?: string;
channel?: string;
enabled?: boolean;
allowFrom?: Array<string>;
allowedRoles?: TwitchRole[];
requireMention?: boolean;
clientSecret?: string;
refreshToken?: string;
expiresIn?: number | null;
obtainmentTimestamp?: number;
}
export interface TwitchChatMessage {
username: string;
userId?: string;
message: string;
channel: string;
displayName?: string;
id?: string;
timestamp?: Date;
isMod?: boolean;
isOwner?: boolean;
isVip?: boolean;
isSub?: boolean;
chatType?: "direct" | "group";
}
Phase 2: Core Directory Structure
2.1 Create Directory Structure in Clawdbot Core
cd opensrc/repos/github.com/clawdbot/clawdbot
mkdir -p src/twitch
mkdir -p src/twitch/monitor
2.2 Target File Structure (following Discord pattern)
src/twitch/
├── channel.ts # Channel plugin definition (replaces plugin.ts)
├── config.ts # Account config (keep from plugin)
├── access-control.ts # Access control (keep from plugin)
├── probe.ts # Connection probing (keep from plugin)
├── resolver.ts # Username resolution (keep from plugin)
├── status.ts # Status issues (keep from plugin)
├── send.ts # Outbound send (keep from plugin)
├── send.types.ts # Send-specific types
├── actions.ts # Message actions (keep from plugin)
├── twitch-client.ts # Twitch client wrapper (keep from plugin)
├── types.ts # Twitch-specific types ONLY
├── utils/
│ ├── markdown.ts # Markdown utilities (keep from plugin)
│ └── twitch.ts # Twitch utilities (keep from plugin)
└── monitor/
├── provider.ts # Inbound message provider (NEW)
├── message-handler.ts # Message processing (NEW)
├── reply-delivery.ts # Reply delivery (NEW)
├── format.ts # Message formatting (NEW)
└── loggers.ts # Twitch-specific loggers (NEW)
Phase 3: Type Deduping
3.1 Remove plugin-sdk.ts Entirely
This file is 100% duplicated from core. Delete it after migration.
3.2 Refactor types.ts
BEFORE (current plugin):
// types.ts - ~465 lines with many duplicates
export interface ChannelMeta { /* duplicate */ }
export interface ChatCapabilities { /* duplicate */ }
export interface TwitchAccountConfig { /* keep */ }
export interface TwitchChatMessage { /* keep */ }
// ... many more duplicates
AFTER (core version):
// types.ts - ~100 lines, Twitch-specific only
import type {
ChannelMeta,
ChatCapabilities,
// ... import from core
} from "../channels/plugins/types.core.js";
export type TwitchRole = "moderator" | "owner" | "vip" | "subscriber" | "all";
export interface TwitchAccountConfig {
username: string;
token: string;
// ... Twitch-specific fields
}
export interface TwitchChatMessage {
username: string;
message: string;
// ... Twitch-specific fields
}
3.3 Update All Imports Across Files
Pattern for imports:
// OLD (plugin):
import type { ChannelMeta, CoreConfig } from "./types.js";
import type { ChannelOutboundAdapter } from "./types.js";
// NEW (core):
import type { ChannelMeta, ChatCapabilities } from "../channels/plugins/types.core.js";
import type { ChannelOutboundAdapter } from "../channels/plugins/types.adapters.js";
import type { ClawdbotConfig } from "../config/config.js";
import type { TwitchAccountConfig, TwitchChatMessage } from "./types.js";
Phase 4: Core Channel Registration
4.1 Update src/channels/registry.ts
// ADD "twitch" to CHAT_CHANNEL_ORDER
export const CHAT_CHANNEL_ORDER = [
"telegram",
"whatsapp",
"discord",
"twitch", // ← ADD HERE
"slack",
"signal",
"imessage",
] as const;
// ADD metadata
const CHAT_CHANNEL_META: Record<ChatChannelId, ChannelMeta> = {
// ... existing channels
twitch: {
id: "twitch",
label: "Twitch",
selectionLabel: "Twitch (Chat)",
detailLabel: "Twitch Chat",
docsPath: "/channels/twitch",
docsLabel: "twitch",
blurb: "connects to Twitch chat for live stream interaction.",
systemImage: "video.badge.plus",
},
// ... existing channels
};
4.2 Update src/utils/message-channel.ts
// Add "twitch" to INTERNAL_MESSAGE_CHANNEL union if needed
// Add to GatewayClientName if Twitch connects via gateway
4.3 Register Channel Plugin
Create or update channel plugin registration (typically in channel index file):
// src/twitch/index.ts or integration point
import { twitchPlugin } from "./channel.js";
import { registerChannelPlugin } from "../channels/plugins/index.js";
export function registerTwitchChannel() {
registerChannelPlugin("twitch", twitchPlugin);
}
Phase 5: Inbound Message Routing
5.1 Create Message Provider (NEW file)
src/twitch/monitor/provider.ts
This is the CRITICAL piece that was missing. Follow Discord's pattern:
// Reference: src/discord/monitor/provider.ts
import { twitchClientManager } from "../twitch-client.js";
import { createTwitchMessageHandler } from "./message-handler.js";
export async function monitorTwitchChannel(params: {
cfg: ClawdbotConfig;
accountId: string;
account: TwitchAccountConfig;
runtime: RuntimeEnv;
abortSignal: AbortSignal;
log?: ChannelLogSink;
}) {
const clientManager = new TwitchClientManager({
info: (msg) => params.log?.info(`[twitch] ${msg}`),
warn: (msg) => params.log?.warn(`[twitch] ${msg}`),
error: (msg) => params.log?.error(`[twitch] ${msg}`),
});
// Create message handler that calls dispatchInboundMessage
const messageHandler = createTwitchMessageHandler({
cfg: params.cfg,
accountId: params.accountId,
account: params.account,
runtime: params.runtime,
log: params.log,
});
// Set up message listener
clientManager.onMessage(params.account, async (message) => {
await messageHandler(message);
});
await clientManager.getClient(params.account);
return {
close: async () => {
await clientManager.close();
},
};
}
5.2 Create Message Handler (NEW file)
src/twitch/monitor/message-handler.ts
import { dispatchInboundMessage } from "../../auto-reply/dispatch.js";
import { createReplyDispatcher } from "../../auto-reply/reply/reply-dispatcher.js";
import type { MsgContext } from "../../auto-reply/templating.js";
import { resolveAgentRoute } from "../../routing/resolve-route.js";
import type { TwitchChatMessage } from "../types.js";
export async function createTwitchMessageHandler(params: {
cfg: ClawdbotConfig;
accountId: string;
account: TwitchAccountConfig;
runtime: RuntimeEnv;
log?: ChannelLogSink;
}) {
return async (message: TwitchChatMessage) => {
// 1. Resolve route
const route = resolveAgentRoute({
cfg: params.cfg,
channel: "twitch",
accountId: params.accountId,
peer: {
kind: "group", // Twitch chat is always group-like
id: message.channel,
},
});
// 2. Build MsgContext
const ctx: MsgContext = {
Body: message.message,
RawBody: message.message,
CommandBody: message.message,
From: `twitch:${message.userId ?? message.username}`,
To: route.sessionKey,
SessionKey: route.sessionKey,
AccountId: params.accountId,
ChatType: "group",
SenderName: message.displayName ?? message.username,
SenderId: message.userId ?? message.username,
SenderUsername: message.username,
Provider: "twitch" as const,
Surface: "twitch" as const,
WasMentioned: message.message.includes(params.account.username), // crude check
MessageSid: message.id ?? `${Date.now()}`,
Timestamp: message.timestamp?.getTime() ?? Date.now(),
CommandAuthorized: true, // TODO: access control check
CommandSource: "text" as const,
OriginatingChannel: "twitch" as const,
OriginatingTo: message.channel,
};
// 3. Create dispatcher for replies
const dispatcher = createReplyDispatcher({
responsePrefix: "", // TODO: from config
onError: (err) => {
params.log?.error(`[twitch] dispatch failed: ${String(err)}`);
},
deliver: async (payload) => {
// Send reply via Twitch client
// TODO: implement sendTwitchReply
params.log?.info(`[twitch] Reply: ${payload.text?.slice(0, 50)}...`);
},
});
// 4. Dispatch to agent
await dispatchInboundMessage({
ctx,
cfg: params.cfg,
dispatcher,
replyOptions: {
disableBlockStreaming: true, // Twitch doesn't support streaming
},
});
};
}
5.3 Update plugin.ts → channel.ts
Rename and refactor to use the new monitor:
// src/twitch/channel.ts (was plugin.ts)
export const twitchPlugin: ChannelPlugin<TwitchAccountConfig> = {
id: "twitch", // Use simple ID like core channels
meta: {
id: "twitch",
label: "Twitch",
// ...
},
// ... adapters (config, outbound, actions, resolver, status)
// GATEWAY - Use new monitor
gateway: {
startAccount: async (ctx) => {
const { monitorTwitchChannel } = await import("./monitor/provider.js");
const monitor = await monitorTwitchChannel({
cfg: ctx.cfg,
accountId: ctx.accountId,
account: ctx.account as TwitchAccountConfig,
runtime: ctx.runtime,
abortSignal: ctx.abortSignal,
log: ctx.log,
});
// Store monitor for cleanup
ctx.setStatus({ running: true, lastStartAt: Date.now() });
},
stopAccount: async (ctx) => {
// Cleanup monitor
ctx.setStatus({ running: false, lastStopAt: Date.now() });
},
},
};
Phase 6: Plugin Wrapper Removal
6.1 Delete index.ts (plugin wrapper)
The index.ts file contains plugin wrapper code that's not needed for core:
// DELETE THIS FILE:
const plugin = {
id: "clawdbot-twitch",
name: "Twitch",
configSchema: emptyPluginConfigSchema(),
register(api) { /* ... */ },
};
export default plugin;
6.2 Remove Plugin Config Schema
Delete emptyPluginConfigSchema() and all plugin config schema code. Core channels don't use this.
Phase 7: Dependencies & Package Updates
7.1 Review Dependencies in package.json
Current plugin dependencies:
{
"dependencies": {
"@twurple/cli": "^7.1.0",
"@twurple/auth": "^7.1.0",
"@twurple/chat": "^7.1.0",
"@twurple/common": "^7.1.0"
}
}
Action: Add these to Clawdbot core's package.json:
{
"dependencies": {
"@twurple/cli": "^7.1.0",
"@twurple/auth": "^7.1.0",
"@twurple/chat": "^7.1.0",
"@twurple/common": "^7.1.0"
}
}
7.2 Remove Plugin-Specific Files
DELETE:
- src/index.ts (plugin wrapper)
- src/plugin-sdk.ts (duplicated types)
- clawdbot.plugin.json (not needed for core)
Phase 8: Testing & Validation
8.1 Build Verification
cd opensrc/repos/github.com/clawdbot/clawdbot
pnpm build
8.2 Type Verification
pnpm run type-check # or tsc --noEmit
8.3 Lint Verification
pnpm lint
8.4 Test Verification
pnpm test -- src/twitch
8.5 Integration Testing
-
Update user config to use core Twitch channel:
{ "channels": { "twitch": { "enabled": true, "accounts": { "default": { "username": "clawdbot", "token": "...", "clientId": "...", "channel": "jaydencarey", "requireMention": true } } } } } -
Start gateway:
clawdbot gateway run -
Send test message in Twitch chat:
@clawdbot hello -
Verify:
- Gateway connects to Twitch
- Messages are received
- Messages are dispatched to agent
- Agent responds
- Response is sent back to Twitch chat
Phase 9: Documentation
9.1 Add Channel Documentation
Create docs/channels/twitch.md following the pattern of other channels:
# Twitch
## Setup
1. Create a Twitch application at [Twitch Dev Portal](https://dev.twitch.tv/console)
2. Get your Client ID and generate a token
3. Configure Clawdbot:
\`\`\`bash
clawdbot channels login --channel twitch
\`\`\`
## Configuration
\`\`\`json
{
"channels": {
"twitch": {
"accounts": {
"default": {
"username": "yourbot",
"token": "...",
"clientId": "...",
"channel": "yourchannel",
"requireMention": true
}
}
}
}
}
\`\`\`
## Features
- **Chat Types:** Group (channel chat)
- **Mentions:** Require @mention by default
- **Roles:** Support for mods, VIPs, subs
- **Access Control:** Allowlist by user ID or role
9.2 Update Main README
Add Twitch to the list of supported channels.
9.3 Update Channel Selection Docs
Update any documentation that lists available channels.
Summary of Key Changes
Files Deleted
src/index.ts- Plugin wrappersrc/plugin-sdk.ts- Duplicated typesclawdbot.plugin.json- Plugin manifest
Files Created (Core)
src/twitch/monitor/provider.ts- Inbound message providersrc/twitch/monitor/message-handler.ts- Message processing with dispatchInboundMessagesrc/twitch/monitor/reply-delivery.ts- Reply deliverysrc/twitch/monitor/format.ts- Message formattingsrc/twitch/monitor/loggers.ts- Twitch loggers
Files Modified
src/twitch/types.ts- Remove duplicated types, keep only Twitch-specificsrc/twitch/plugin.ts→src/twitch/channel.ts- Rename and refactor- All other
.tsfiles - Update imports to use core paths src/channels/registry.ts- Add "twitch" to channel listpackage.json- Add @twurple dependencies
Critical Change: Inbound Routing
Before (plugin):
// Only logged messages
console.info(`[twitch] Received message: ${message.message}`);
After (core):
// Actually routes messages to agent
await dispatchInboundMessage({ ctx, cfg, dispatcher, replyOptions });
Checklist
Pre-Migration
- Ensure Clawdbot core is at latest commit
- Create a new branch for the migration
- Document any Twitch plugin-specific features that need special handling
Migration
- Create
src/twitchdirectory structure - Move and adapt files from plugin to core
- Remove
plugin-sdk.ts(all duplicates) - Refactor
types.tsto only contain Twitch-specific types - Update all imports across files
- Delete
src/index.ts(plugin wrapper) - Rename
plugin.ts→channel.tsand refactor - Create
monitor/provider.tswith inbound routing - Create
monitor/message-handler.tswith dispatchInboundMessage - Update
src/channels/registry.tsto add "twitch" - Update
package.jsonwith @twurple dependencies - Move tests to core and update imports
Post-Migration
- Run
pnpm installto install new dependencies - Run
pnpm buildand fix any build errors - Run
pnpm lintand fix any lint errors - Run
pnpm testand fix any test failures - Test Twitch connection manually
- Test message routing with a real Twitch chat
- Add/update documentation
- Update user config to use core Twitch channel
- Clean up old plugin directory
- Submit PR to Clawdbot repo
Estimated Effort
- Type deduping: 2-3 hours
- File migration & import updates: 3-4 hours
- Inbound routing implementation: 4-5 hours (critical path)
- Testing & debugging: 3-4 hours
- Documentation: 1-2 hours
Total: ~13-18 hours
References
- Discord channel implementation:
opensrc/repos/github.com/clawdbot/clawdbot/src/discord/ - Core channel types:
opensrc/repos/github.com/clawdbot/clawdbot/src/channels/plugins/types.core.ts - Channel registry:
opensrc/repos/github.com/clawdbot/clawdbot/src/channels/registry.ts - Message dispatch:
opensrc/repos/github.com/clawdbot/clawdbot/src/auto-reply/dispatch.ts