openclaw/MIGRATION_PLAN.md
jaydenfyi 98639c909d wip
2026-01-25 16:30:22 +08:00

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:

  1. External plugins don't have access to dispatchInboundMessage from the core
  2. The ChannelGatewayContext doesn't provide a message dispatch mechanism
  3. 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, ChannelOutboundAdapter
    • ChannelOutboundContext, OutboundDeliveryResult
    • ChannelPlugin, ChannelAccountSnapshot
    • ChannelStatusAdapter, ChannelGatewayAdapter
    • ChannelGatewayContext, ChannelLogSink
    • ChannelMessageActionAdapter, ChannelMessageActionContext
    • Plus plugin-specific wrapper types (PluginAPI, CoreConfig stubs)

Keep in types.ts:

  • TwitchAccountConfig - Twitch-specific config
  • TwitchChannelConfig - Twitch channel config
  • TwitchRole - Twitch role enum
  • TwitchTarget - Twitch send target
  • TwitchChatMessage - Twitch message structure
  • TwitchPluginConfig - Plugin-specific config (if still needed)
  • SendResult - Twitch send result
  • ProviderLogger - 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.tschannel.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

  1. Update user config to use core Twitch channel:

    {
      "channels": {
        "twitch": {
          "enabled": true,
          "accounts": {
            "default": {
              "username": "clawdbot",
              "token": "...",
              "clientId": "...",
              "channel": "jaydencarey",
              "requireMention": true
            }
          }
        }
      }
    }
    
  2. Start gateway:

    clawdbot gateway run
    
  3. Send test message in Twitch chat:

    @clawdbot hello
    
  4. 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 wrapper
  • src/plugin-sdk.ts - Duplicated types
  • clawdbot.plugin.json - Plugin manifest

Files Created (Core)

  • src/twitch/monitor/provider.ts - Inbound message provider
  • src/twitch/monitor/message-handler.ts - Message processing with dispatchInboundMessage
  • src/twitch/monitor/reply-delivery.ts - Reply delivery
  • src/twitch/monitor/format.ts - Message formatting
  • src/twitch/monitor/loggers.ts - Twitch loggers

Files Modified

  • src/twitch/types.ts - Remove duplicated types, keep only Twitch-specific
  • src/twitch/plugin.tssrc/twitch/channel.ts - Rename and refactor
  • All other .ts files - Update imports to use core paths
  • src/channels/registry.ts - Add "twitch" to channel list
  • package.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/twitch directory structure
  • Move and adapt files from plugin to core
  • Remove plugin-sdk.ts (all duplicates)
  • Refactor types.ts to only contain Twitch-specific types
  • Update all imports across files
  • Delete src/index.ts (plugin wrapper)
  • Rename plugin.tschannel.ts and refactor
  • Create monitor/provider.ts with inbound routing
  • Create monitor/message-handler.ts with dispatchInboundMessage
  • Update src/channels/registry.ts to add "twitch"
  • Update package.json with @twurple dependencies
  • Move tests to core and update imports

Post-Migration

  • Run pnpm install to install new dependencies
  • Run pnpm build and fix any build errors
  • Run pnpm lint and fix any lint errors
  • Run pnpm test and 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