wip type changes
This commit is contained in:
parent
ebf7dcc054
commit
9693933b9e
@ -25,6 +25,7 @@ Text is supported everywhere; media and reactions vary by channel.
|
||||
- [Matrix](/channels/matrix) — Matrix protocol (plugin, installed separately).
|
||||
- [Nostr](/channels/nostr) — Decentralized DMs via NIP-04 (plugin, installed separately).
|
||||
- [Tlon](/channels/tlon) — Urbit-based messenger (plugin, installed separately).
|
||||
- [Twitch](/channels/twitch) — Twitch chat via IRC connection (plugin, installed separately).
|
||||
- [Zalo](/channels/zalo) — Zalo Bot API; Vietnam's popular messenger (plugin, installed separately).
|
||||
- [Zalo Personal](/channels/zalouser) — Zalo personal account via QR login (plugin, installed separately).
|
||||
- [WebChat](/web/webchat) — Gateway WebChat UI over WebSocket.
|
||||
|
||||
459
docs/channels/twitch.md
Normal file
459
docs/channels/twitch.md
Normal file
@ -0,0 +1,459 @@
|
||||
---
|
||||
summary: "Twitch chat bot support status, capabilities, and configuration"
|
||||
read_when:
|
||||
- Setting up Twitch chat integration for Clawdbot
|
||||
- Configuring Twitch bot permissions and access control
|
||||
---
|
||||
# Twitch (plugin)
|
||||
|
||||
Twitch chat support via IRC connection. Clawdbot connects as a Twitch user (bot account) to receive and send messages in channels. Requires a Twitch application with OAuth token.
|
||||
|
||||
Status: ready for Twitch chat via IRC connection with @twurple.
|
||||
|
||||
## Plugin required
|
||||
|
||||
Twitch ships as a plugin and is not bundled with the core install.
|
||||
|
||||
Install via CLI (npm registry):
|
||||
|
||||
```bash
|
||||
clawdbot plugins install @clawdbot/twitch
|
||||
```
|
||||
|
||||
Local checkout (when running from a git repo):
|
||||
|
||||
```bash
|
||||
clawdbot plugins install ./extensions/twitch
|
||||
```
|
||||
|
||||
Details: [Plugins](/plugin)
|
||||
|
||||
## Setup
|
||||
|
||||
1) Install the Twitch plugin:
|
||||
- From npm: `clawdbot plugins install @clawdbot/twitch`
|
||||
- From a local checkout: `clawdbot plugins install ./extensions/twitch`
|
||||
|
||||
2) Create a Twitch application:
|
||||
- Go to [Twitch Developer Console](https://dev.twitch.tv/console)
|
||||
- Click "Register Your Application"
|
||||
- Set Application Type to "Chat Bot"
|
||||
- Copy the Client ID
|
||||
|
||||
3) Generate your OAuth token:
|
||||
- Use [Twitch Token Generator](https://twitchtokengenerator.com/) or [TwitchApps TMI](https://twitchapps.com/tmi/)
|
||||
- Select scopes: `chat:read` and `chat:write`
|
||||
- Copy the token (starts with `oauth:`)
|
||||
|
||||
4) Configure credentials:
|
||||
- Env: `CLAWDBOT_TWITCH_ACCESS_TOKEN=...` (default account only)
|
||||
- Or config: `channels.twitch.accounts.default.token`
|
||||
- If both are set, config takes precedence (env fallback is default-account only)
|
||||
|
||||
5) Start the gateway. Twitch starts when a token is resolved (config first, env fallback).
|
||||
|
||||
6) Invite the bot to your channel (it will auto-join the channel specified in config).
|
||||
|
||||
Minimal config (default account):
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
twitch: {
|
||||
enabled: true,
|
||||
accounts: {
|
||||
default: {
|
||||
username: "mybot",
|
||||
token: "oauth:your_token_here",
|
||||
clientId: "your_client_id_here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Env-only setup:
|
||||
|
||||
```bash
|
||||
export CLAWDBOT_TWITCH_ACCESS_TOKEN=oauth:your_token_here
|
||||
```
|
||||
|
||||
With env, you still need `clientId` in config (or use the minimal config above without `token`).
|
||||
|
||||
## Token setup
|
||||
|
||||
### Option 1: Static token (simple)
|
||||
|
||||
Use [Twitch Token Generator](https://twitchtokengenerator.com/):
|
||||
|
||||
1. Select scopes: `chat:read` and `chat:write`
|
||||
2. Copy the token (starts with `oauth:`)
|
||||
3. Add to config:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
twitch: {
|
||||
accounts: {
|
||||
default: {
|
||||
username: "mybot",
|
||||
token: "oauth:abc123...",
|
||||
clientId: "your_client_id"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Option 2: Automatic token refresh (recommended)
|
||||
|
||||
For long-running bots, configure RefreshingAuthProvider:
|
||||
|
||||
1. Use [Twitch Token Generator](https://twitchtokengenerator.com/) with **"Include Refresh Token"** checked
|
||||
2. Get your Client Secret from [Twitch Developer Console](https://dev.twitch.tv/console)
|
||||
3. Add to config:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
twitch: {
|
||||
accounts: {
|
||||
default: {
|
||||
username: "mybot",
|
||||
token: "oauth:abc123...",
|
||||
clientId: "your_client_id",
|
||||
clientSecret: "your_client_secret",
|
||||
refreshToken: "your_refresh_token",
|
||||
expiresIn: 14400,
|
||||
obtainmentTimestamp: 1706092800000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The bot will automatically refresh tokens before they expire and log refresh events.
|
||||
|
||||
## Access control
|
||||
|
||||
### Allowlist by User ID (recommended)
|
||||
|
||||
Most secure: only allow specific Twitch user IDs:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
twitch: {
|
||||
accounts: {
|
||||
default: {
|
||||
username: "mybot",
|
||||
token: "oauth:...",
|
||||
clientId: "...",
|
||||
allowFrom: ["123456789", "987654321"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why user IDs instead of usernames?** Twitch usernames can change, which could allow someone to hijack another user's access. User IDs are permanent.
|
||||
|
||||
Find your Twitch user ID at: https://www.streamweasels.com/tools/convert-your-twitch-username-to-user-id/
|
||||
|
||||
### Role-based restrictions
|
||||
|
||||
Restrict access to specific roles:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
twitch: {
|
||||
accounts: {
|
||||
default: {
|
||||
username: "mybot",
|
||||
token: "oauth:...",
|
||||
clientId: "...",
|
||||
allowedRoles: ["moderator", "vip"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Available roles:**
|
||||
- `"moderator"` - Channel moderators
|
||||
- `"owner"` - Channel owner/broadcaster
|
||||
- `"vip"` - VIPs
|
||||
- `"subscriber"` - Subscribers
|
||||
- `"all"` - Anyone in chat
|
||||
|
||||
### Combined allowlist + roles
|
||||
|
||||
Users in `allowFrom` bypass role checks:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
twitch: {
|
||||
accounts: {
|
||||
default: {
|
||||
username: "mybot",
|
||||
token: "oauth:...",
|
||||
clientId: "...",
|
||||
allowFrom: ["123456789"],
|
||||
allowedRoles: ["moderator"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In this example:
|
||||
- User `123456789` can always message (bypasses role check)
|
||||
- All moderators can message
|
||||
- Everyone else is blocked
|
||||
|
||||
### Require @mention
|
||||
|
||||
Only respond when the bot is mentioned:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
twitch: {
|
||||
accounts: {
|
||||
default: {
|
||||
username: "mybot",
|
||||
token: "oauth:...",
|
||||
clientId: "...",
|
||||
requireMention: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Multiple accounts
|
||||
|
||||
Configure multiple Twitch channels:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
twitch: {
|
||||
accounts: {
|
||||
main: {
|
||||
username: "mybot",
|
||||
token: "oauth:...",
|
||||
clientId: "...",
|
||||
channel: "streamer1"
|
||||
},
|
||||
secondary: {
|
||||
username: "mybot",
|
||||
token: "oauth:...",
|
||||
clientId: "...",
|
||||
channel: "streamer2",
|
||||
allowedRoles: ["moderator"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Environment variables
|
||||
|
||||
For the default account, you can use environment variables instead of config:
|
||||
|
||||
- `CLAWDBOT_TWITCH_ACCESS_TOKEN` - OAuth token (with `oauth:` prefix)
|
||||
|
||||
Env fallback only works for the default account. For multi-account setups, use config.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
export CLAWDBOT_TWITCH_ACCESS_TOKEN=oauth:abc123def456...
|
||||
```
|
||||
|
||||
Config:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
twitch: {
|
||||
enabled: true,
|
||||
accounts: {
|
||||
default: {
|
||||
username: "mybot",
|
||||
clientId: "your_client_id"
|
||||
// token will be read from CLAWDBOT_TWITCH_ACCESS_TOKEN
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Priority: account config > base config > env var (for default account only).
|
||||
|
||||
## Plugin options
|
||||
|
||||
Control markdown stripping behavior:
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
twitch: {
|
||||
stripMarkdown: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `stripMarkdown` (default: `true`) - Remove markdown formatting before sending to Twitch
|
||||
|
||||
Twitch doesn't support markdown, so this is enabled by default. Disable if you want to send markdown as-is (it will appear as plain text with markdown symbols).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
First, run diagnostic commands:
|
||||
|
||||
```bash
|
||||
clawdbot doctor
|
||||
clawdbot channels status --probe
|
||||
```
|
||||
|
||||
### Bot doesn't respond to messages
|
||||
|
||||
**Check access control:**
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
twitch: {
|
||||
accounts: {
|
||||
default: {
|
||||
username: "mybot",
|
||||
token: "oauth:...",
|
||||
clientId: "...",
|
||||
// Temporary: allow everyone
|
||||
allowedRoles: ["all"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Check the bot is in the channel:** The bot must join the channel (either `channel: "target_channel"` or defaults to `username`).
|
||||
|
||||
### Token issues
|
||||
|
||||
**"Failed to connect" or authentication errors:**
|
||||
- Verify token starts with `oauth:`
|
||||
- Check token has `chat:read` and `chat:write` scopes
|
||||
- If using RefreshingAuthProvider, verify `clientSecret` and `refreshToken` are set
|
||||
|
||||
### Token refresh not working
|
||||
|
||||
**Check logs for refresh events:**
|
||||
```
|
||||
[twitch] Using env token source for mybot
|
||||
[twitch] Access token refreshed for user 123456 (expires in 14400s)
|
||||
```
|
||||
|
||||
If you see "token refresh disabled (no refresh token)":
|
||||
- Ensure `clientSecret` is provided
|
||||
- Ensure `refreshToken` is provided (from Twitch Token Generator with "Include Refresh Token" checked)
|
||||
|
||||
## Capabilities & limits
|
||||
|
||||
**Supported:**
|
||||
- ✅ Channel messages (group chat)
|
||||
- ✅ Whispers/DMs (received but replies not supported - Twitch doesn't allow bots to send whispers)
|
||||
- ✅ Markdown stripping (automatically applied)
|
||||
- ✅ Message chunking (500 char limit)
|
||||
- ✅ Access control (user ID allowlist, role-based)
|
||||
- ✅ @mention requirement
|
||||
- ✅ Automatic token refresh (with RefreshingAuthProvider)
|
||||
- ✅ Multi-account support
|
||||
|
||||
**Not supported:**
|
||||
- ❌ Native reactions
|
||||
- ❌ Threaded replies
|
||||
- ❌ Message editing
|
||||
- ❌ Message deletion
|
||||
- ❌ Rich embeds/media uploads (sends media URLs as text)
|
||||
|
||||
## Config reference
|
||||
|
||||
### Account config
|
||||
|
||||
```typescript
|
||||
{
|
||||
username: string, // Bot username (required)
|
||||
token: string, // OAuth token with chat:read and chat:write (required)
|
||||
clientId: string, // Twitch Client ID (required)
|
||||
channel?: string, // Channel to join (default: username)
|
||||
enabled?: boolean, // Enable this account (default: true)
|
||||
clientSecret?: string, // For RefreshingAuthProvider
|
||||
refreshToken?: string, // For RefreshingAuthProvider
|
||||
expiresIn?: number, // Token expiry in seconds
|
||||
obtainmentTimestamp?: number, // Token obtained timestamp
|
||||
allowFrom?: string[], // User ID allowlist
|
||||
allowedRoles?: TwitchRole[], // Role-based access control
|
||||
requireMention?: boolean // Require @mention (default: false)
|
||||
}
|
||||
```
|
||||
|
||||
**TwitchRole:** `"moderator"` | `"owner"` | `"vip"` | `"subscriber"` | `"all"`
|
||||
|
||||
### Plugin config
|
||||
|
||||
```typescript
|
||||
{
|
||||
stripMarkdown?: boolean // Strip markdown from outbound (default: true)
|
||||
}
|
||||
```
|
||||
|
||||
## Tool actions
|
||||
|
||||
The agent can call `twitch` with action:
|
||||
|
||||
- `send` - Send a message to a channel
|
||||
|
||||
Example:
|
||||
|
||||
```json5
|
||||
{
|
||||
"action": "twitch",
|
||||
"params": {
|
||||
"message": "Hello Twitch!",
|
||||
"to": "#mychannel"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Safety & ops
|
||||
|
||||
- **Treat tokens like passwords** - Never commit tokens to git
|
||||
- **Use RefreshingAuthProvider** for long-running bots
|
||||
- **Use user ID allowlists** instead of usernames for access control
|
||||
- **Monitor logs** for token refresh events and connection status
|
||||
- **Scope tokens minimally** - Only request `chat:read` and `chat:write`
|
||||
- **If stuck**: Restart the gateway after confirming no other process owns the session
|
||||
|
||||
## Message limits
|
||||
|
||||
- **500 characters** per message (Twitch limit)
|
||||
- Messages are automatically chunked at word boundaries
|
||||
- Markdown is stripped before chunking to avoid breaking patterns
|
||||
- No rate limiting (uses Twitch's built-in rate limits)
|
||||
18
extensions/twitch/index.ts
Normal file
18
extensions/twitch/index.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
||||
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
|
||||
|
||||
import { twitchPlugin } from "./src/plugin.js";
|
||||
import { setTwitchRuntime } from "./src/runtime.js";
|
||||
|
||||
const plugin = {
|
||||
id: "twitch",
|
||||
name: "Twitch",
|
||||
description: "Twitch channel plugin",
|
||||
configSchema: emptyPluginConfigSchema(),
|
||||
register(api: ClawdbotPluginApi) {
|
||||
setTwitchRuntime(api.runtime);
|
||||
api.registerChannel({ plugin: twitchPlugin });
|
||||
},
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
@ -9,9 +9,11 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@sinclair/typebox": "0.34.47",
|
||||
"@twurple/api": "^8.0.3",
|
||||
"@twurple/auth": "^8.0.3",
|
||||
"@twurple/chat": "^8.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"clawdbot": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
|
||||
import type {
|
||||
CoreConfig,
|
||||
TwitchAccountConfig,
|
||||
TwitchPluginConfig,
|
||||
} from "./types.js";
|
||||
@ -50,7 +50,7 @@ export function parsePluginConfig(value: unknown): TwitchPluginConfig {
|
||||
/**
|
||||
* List all configured account IDs
|
||||
*/
|
||||
export function listAccountIds(cfg: CoreConfig): string[] {
|
||||
export function listAccountIds(cfg: ClawdbotConfig): string[] {
|
||||
const accounts = (cfg as Record<string, unknown>).channels as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
|
||||
@ -6,6 +6,8 @@
|
||||
*/
|
||||
|
||||
import { checkTwitchAccessControl } from "./access-control.js";
|
||||
import { resolveAgentRoute } from "../../../src/routing/resolve-route.js";
|
||||
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
|
||||
import { twitchMessageActions } from "./actions.js";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
@ -19,14 +21,12 @@ import { collectTwitchStatusIssues } from "./status.js";
|
||||
import { TwitchClientManager } from "./twitch-client.js";
|
||||
import type {
|
||||
ChannelAccountSnapshot,
|
||||
ChannelCapabilities,
|
||||
ChannelLogSink,
|
||||
ChannelMeta,
|
||||
ChannelPlugin,
|
||||
ChannelResolveKind,
|
||||
ChannelResolveResult,
|
||||
ChatCapabilities,
|
||||
CoreConfig,
|
||||
PluginAPI,
|
||||
ProviderLogger,
|
||||
TwitchAccountConfig,
|
||||
} from "./types.js";
|
||||
|
||||
@ -63,16 +63,16 @@ export const twitchPlugin: ChannelPlugin<TwitchAccountConfig> = {
|
||||
/** Supported chat capabilities */
|
||||
capabilities: {
|
||||
chatTypes: ["group", "direct"],
|
||||
} satisfies ChatCapabilities,
|
||||
} satisfies ChannelCapabilities,
|
||||
|
||||
/** Account configuration management */
|
||||
config: {
|
||||
/** List all configured account IDs */
|
||||
listAccountIds: (cfg: CoreConfig): string[] => listAccountIds(cfg),
|
||||
listAccountIds: (cfg: ClawdbotConfig): string[] => listAccountIds(cfg),
|
||||
|
||||
/** Resolve an account config by ID */
|
||||
resolveAccount: (
|
||||
cfg: CoreConfig,
|
||||
cfg: ClawdbotConfig,
|
||||
accountId?: string | null,
|
||||
): TwitchAccountConfig | null =>
|
||||
getAccountConfig(cfg, accountId ?? DEFAULT_ACCOUNT_ID),
|
||||
@ -81,7 +81,7 @@ export const twitchPlugin: ChannelPlugin<TwitchAccountConfig> = {
|
||||
defaultAccountId: (): string => DEFAULT_ACCOUNT_ID,
|
||||
|
||||
/** Check if an account is configured */
|
||||
isConfigured: (_account: unknown, cfg: CoreConfig): boolean => {
|
||||
isConfigured: (_account: unknown, cfg: ClawdbotConfig): boolean => {
|
||||
const account = getAccountConfig(cfg, DEFAULT_ACCOUNT_ID);
|
||||
return isConfigured(account);
|
||||
},
|
||||
@ -113,11 +113,11 @@ export const twitchPlugin: ChannelPlugin<TwitchAccountConfig> = {
|
||||
kind,
|
||||
runtime,
|
||||
}: {
|
||||
cfg: CoreConfig;
|
||||
cfg: ClawdbotConfig;
|
||||
accountId?: string | null;
|
||||
inputs: string[];
|
||||
kind: ChannelResolveKind;
|
||||
runtime: { log?: ProviderLogger };
|
||||
runtime: { log?: ChannelLogSink };
|
||||
}): Promise<ChannelResolveResult[]> => {
|
||||
const account = getAccountConfig(cfg, accountId ?? DEFAULT_ACCOUNT_ID);
|
||||
|
||||
@ -177,7 +177,7 @@ export const twitchPlugin: ChannelPlugin<TwitchAccountConfig> = {
|
||||
probe,
|
||||
}: {
|
||||
account: TwitchAccountConfig;
|
||||
cfg: CoreConfig;
|
||||
cfg: ClawdbotConfig;
|
||||
runtime?: ChannelAccountSnapshot;
|
||||
probe?: unknown;
|
||||
}): ChannelAccountSnapshot => {
|
||||
@ -227,16 +227,33 @@ export const twitchPlugin: ChannelPlugin<TwitchAccountConfig> = {
|
||||
});
|
||||
|
||||
if (!access.allowed) {
|
||||
console.info(
|
||||
ctx.log?.info(
|
||||
`[twitch] Ignored message from ${message.username}: ${access.reason ?? "blocked"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Implement inbound message routing to Clawdbot
|
||||
// This requires integration with Clawdbot's message handling system
|
||||
console.info(
|
||||
`[twitch] Received message from ${message.username}: ${message.message}`,
|
||||
// Resolve route for this message
|
||||
const route = resolveAgentRoute({
|
||||
cfg: ctx.cfg,
|
||||
channel: "twitch",
|
||||
accountId,
|
||||
peer: {
|
||||
kind: "group", // Twitch chat is always group-like
|
||||
id: message.channel,
|
||||
},
|
||||
});
|
||||
|
||||
// Build message preview
|
||||
const preview = message.message.replace(/\s+/g, " ").slice(0, 160);
|
||||
|
||||
// Dispatch to agent system
|
||||
ctx.runtime.system.enqueueSystemEvent(
|
||||
`Twitch message from ${message.displayName ?? message.username}: ${preview}`,
|
||||
{
|
||||
sessionKey: route.sessionKey,
|
||||
contextKey: `twitch:message:${message.channel}:${message.id ?? "unknown"}`,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@ -253,7 +270,7 @@ export const twitchPlugin: ChannelPlugin<TwitchAccountConfig> = {
|
||||
);
|
||||
|
||||
try {
|
||||
await clientManager.getClient(account);
|
||||
await clientManager.getClient(account, ctx.cfg);
|
||||
ctx.log?.info(`[twitch] Connected to Twitch as ${account.username}`);
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
@ -287,18 +304,3 @@ export const twitchPlugin: ChannelPlugin<TwitchAccountConfig> = {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Extension entry point for registering the Twitch plugin with Clawdbot.
|
||||
*
|
||||
* @param api - Plugin API provided by Clawdbot
|
||||
*/
|
||||
export function registerTwitchPlugin(api: PluginAPI): void {
|
||||
api.registerChannel({ plugin: twitchPlugin });
|
||||
api.logger?.info("[twitch] Plugin registered");
|
||||
}
|
||||
|
||||
/**
|
||||
* Default export for CommonJS compatibility.
|
||||
*/
|
||||
export default twitchPlugin;
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
import { ApiClient } from "@twurple/api";
|
||||
import { StaticAuthProvider } from "@twurple/auth";
|
||||
import type { ChannelResolveKind, ChannelResolveResult } from "./types.js";
|
||||
import type { ProviderLogger, TwitchAccountConfig } from "./types.js";
|
||||
import type { ChannelLogSink, TwitchAccountConfig } from "./types.js";
|
||||
|
||||
/**
|
||||
* Normalize a Twitch username - strip @ prefix and convert to lowercase
|
||||
@ -24,7 +24,7 @@ function normalizeUsername(input: string): string {
|
||||
/**
|
||||
* Create a logger that includes the Twitch prefix
|
||||
*/
|
||||
function createLogger(logger?: ProviderLogger): ProviderLogger {
|
||||
function createLogger(logger?: ChannelLogSink): ChannelLogSink {
|
||||
return {
|
||||
info: (msg: string) => logger?.info(`[twitch] ${msg}`),
|
||||
warn: (msg: string) => logger?.warn(`[twitch] ${msg}`),
|
||||
@ -46,7 +46,7 @@ export async function resolveTwitchTargets(
|
||||
inputs: string[],
|
||||
account: TwitchAccountConfig,
|
||||
kind: ChannelResolveKind,
|
||||
logger?: ProviderLogger,
|
||||
logger?: ChannelLogSink,
|
||||
): Promise<ChannelResolveResult[]> {
|
||||
const log = createLogger(logger);
|
||||
|
||||
|
||||
14
extensions/twitch/src/runtime.ts
Normal file
14
extensions/twitch/src/runtime.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import type { PluginRuntime } from "clawdbot/plugin-sdk";
|
||||
|
||||
let runtime: PluginRuntime | null = null;
|
||||
|
||||
export function setTwitchRuntime(next: PluginRuntime) {
|
||||
runtime = next;
|
||||
}
|
||||
|
||||
export function getTwitchRuntime(): PluginRuntime {
|
||||
if (!runtime) {
|
||||
throw new Error("Twitch runtime not initialized");
|
||||
}
|
||||
return runtime;
|
||||
}
|
||||
@ -7,7 +7,7 @@
|
||||
|
||||
import { DEFAULT_ACCOUNT_ID, getAccountConfig } from "./config.js";
|
||||
import { TwitchClientManager } from "./twitch-client.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
|
||||
import { stripMarkdownForTwitch } from "./utils/markdown.js";
|
||||
import {
|
||||
generateMessageId,
|
||||
@ -138,7 +138,7 @@ export async function sendMessageTwitch(
|
||||
export async function sendMessageTwitchInternal(
|
||||
channel: string,
|
||||
text: string,
|
||||
cfg: CoreConfig,
|
||||
cfg: ClawdbotConfig,
|
||||
accountId: string = DEFAULT_ACCOUNT_ID,
|
||||
stripMarkdown: boolean = true,
|
||||
logger: Console = console,
|
||||
@ -188,6 +188,7 @@ export async function sendMessageTwitchInternal(
|
||||
account,
|
||||
normalizeTwitchChannel(normalizedChannel),
|
||||
cleanedText,
|
||||
cfg,
|
||||
);
|
||||
|
||||
if (!result.ok) {
|
||||
|
||||
77
extensions/twitch/src/token.ts
Normal file
77
extensions/twitch/src/token.ts
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Twitch token resolution with environment variable support.
|
||||
*
|
||||
* Supports reading Twitch OAuth tokens from config or environment variable.
|
||||
* The CLAWDBOT_TWITCH_ACCESS_TOKEN env var is only used for the default account.
|
||||
*/
|
||||
|
||||
import type { ClawdbotConfig } from "../../../src/config/config.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../src/routing/session-key.js";
|
||||
import type { TwitchAccountConfig } from "./types.js";
|
||||
|
||||
export type TwitchTokenSource = "env" | "config" | "none";
|
||||
|
||||
export type TwitchTokenResolution = {
|
||||
token: string;
|
||||
source: TwitchTokenSource;
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalize a Twitch OAuth token - ensure it has the oauth: prefix
|
||||
*/
|
||||
function normalizeTwitchToken(raw?: string | null): string | undefined {
|
||||
if (!raw) return undefined;
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return undefined;
|
||||
// Twitch tokens should have oauth: prefix
|
||||
return trimmed.startsWith("oauth:") ? trimmed : `oauth:${trimmed}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve Twitch token from config or environment variable.
|
||||
*
|
||||
* Priority:
|
||||
* 1. Account token: channels.twitch.accounts.{accountId}.token
|
||||
* 2. Base config token: channels.twitch.token (default account only)
|
||||
* 3. Environment variable: CLAWDBOT_TWITCH_ACCESS_TOKEN (default account only)
|
||||
*
|
||||
* @param cfg - Clawdbot config
|
||||
* @param opts - Options including accountId and optional envToken override
|
||||
* @returns Token resolution with source
|
||||
*/
|
||||
export function resolveTwitchToken(
|
||||
cfg?: ClawdbotConfig,
|
||||
opts: { accountId?: string | null; envToken?: string | null } = {},
|
||||
): TwitchTokenResolution {
|
||||
const accountId = normalizeAccountId(opts.accountId);
|
||||
const twitchCfg = cfg?.channels?.twitch;
|
||||
const accountCfg =
|
||||
accountId !== DEFAULT_ACCOUNT_ID
|
||||
? twitchCfg?.accounts?.[accountId]
|
||||
: twitchCfg?.accounts?.[DEFAULT_ACCOUNT_ID];
|
||||
|
||||
// 1. Account token (highest priority)
|
||||
const accountToken = normalizeTwitchToken(accountCfg?.token ?? undefined);
|
||||
if (accountToken) {
|
||||
return { token: accountToken, source: "config" };
|
||||
}
|
||||
|
||||
// 2. Base config token (default account only)
|
||||
const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
|
||||
const configToken = allowEnv
|
||||
? normalizeTwitchToken(twitchCfg?.token ?? undefined)
|
||||
: undefined;
|
||||
if (configToken) {
|
||||
return { token: configToken, source: "config" };
|
||||
}
|
||||
|
||||
// 3. Environment variable (default account only)
|
||||
const envToken = allowEnv
|
||||
? normalizeTwitchToken(opts.envToken ?? process.env.CLAWDBOT_TWITCH_ACCESS_TOKEN)
|
||||
: undefined;
|
||||
if (envToken) {
|
||||
return { token: envToken, source: "env" };
|
||||
}
|
||||
|
||||
return { token: "", source: "none" };
|
||||
}
|
||||
@ -12,7 +12,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { TwitchClientManager } from "./twitch-client.js";
|
||||
import type {
|
||||
ProviderLogger,
|
||||
ChannelLogSink,
|
||||
TwitchAccountConfig,
|
||||
TwitchChatMessage,
|
||||
} from "./types.js";
|
||||
@ -58,7 +58,7 @@ vi.mock("@twurple/auth", () => ({
|
||||
|
||||
describe("TwitchClientManager", () => {
|
||||
let manager: TwitchClientManager;
|
||||
let mockLogger: ProviderLogger;
|
||||
let mockLogger: ChannelLogSink;
|
||||
|
||||
const testAccount: TwitchAccountConfig = {
|
||||
username: "testbot",
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import { RefreshingAuthProvider, StaticAuthProvider } from "@twurple/auth";
|
||||
import { ChatClient, LogLevel } from "@twurple/chat";
|
||||
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
|
||||
import type {
|
||||
ProviderLogger,
|
||||
ChannelLogSink,
|
||||
TwitchAccountConfig,
|
||||
TwitchChatMessage,
|
||||
} from "./types.js";
|
||||
import { resolveTwitchToken } from "./token.js";
|
||||
|
||||
/**
|
||||
* Manages Twitch chat client connections
|
||||
@ -16,12 +18,15 @@ export class TwitchClientManager {
|
||||
(message: TwitchChatMessage) => void
|
||||
>();
|
||||
|
||||
constructor(private logger: ProviderLogger) {}
|
||||
constructor(private logger: ChannelLogSink) {}
|
||||
|
||||
/**
|
||||
* Get or create a chat client for an account
|
||||
*/
|
||||
async getClient(account: TwitchAccountConfig): Promise<ChatClient> {
|
||||
async getClient(
|
||||
account: TwitchAccountConfig,
|
||||
cfg?: ClawdbotConfig,
|
||||
): Promise<ChatClient> {
|
||||
const key = this.getAccountKey(account);
|
||||
|
||||
const existing = this.clients.get(key);
|
||||
@ -29,17 +34,33 @@ export class TwitchClientManager {
|
||||
return existing;
|
||||
}
|
||||
|
||||
if (!account.clientId || !account.token) {
|
||||
// Resolve token from config or environment
|
||||
const tokenResolution = resolveTwitchToken(cfg, {
|
||||
accountId: account.username,
|
||||
});
|
||||
|
||||
if (!tokenResolution.token) {
|
||||
this.logger.error(
|
||||
`[twitch] Missing Twitch client ID or token for account ${account.username}`,
|
||||
`[twitch] Missing Twitch token for account ${account.username} (set channels.twitch.accounts.${account.username}.token or CLAWDBOT_TWITCH_ACCESS_TOKEN for default)`,
|
||||
);
|
||||
throw new Error("Missing Twitch client ID or token");
|
||||
throw new Error("Missing Twitch token");
|
||||
}
|
||||
|
||||
this.logger.debug(
|
||||
`[twitch] Using ${tokenResolution.source} token source for ${account.username}`,
|
||||
);
|
||||
|
||||
if (!account.clientId) {
|
||||
this.logger.error(
|
||||
`[twitch] Missing Twitch client ID for account ${account.username}`,
|
||||
);
|
||||
throw new Error("Missing Twitch client ID");
|
||||
}
|
||||
|
||||
// Normalize token - strip oauth: prefix if present (Twurple doesn't need it)
|
||||
const normalizedToken = account.token.startsWith("oauth:")
|
||||
? account.token.slice(6)
|
||||
: account.token;
|
||||
const normalizedToken = tokenResolution.token.startsWith("oauth:")
|
||||
? tokenResolution.token.slice(6)
|
||||
: tokenResolution.token;
|
||||
|
||||
// Use RefreshingAuthProvider if clientSecret is provided (supports optional refresh tokens)
|
||||
let authProvider: StaticAuthProvider | RefreshingAuthProvider;
|
||||
@ -251,9 +272,10 @@ export class TwitchClientManager {
|
||||
account: TwitchAccountConfig,
|
||||
channel: string,
|
||||
message: string,
|
||||
cfg?: ClawdbotConfig,
|
||||
): Promise<{ ok: boolean; error?: string; messageId?: string }> {
|
||||
try {
|
||||
const client = await this.getClient(account);
|
||||
const client = await this.getClient(account, cfg);
|
||||
|
||||
// Generate a message ID (Twurple's say() doesn't return the message ID, so we generate one)
|
||||
const messageId = `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
||||
|
||||
@ -1,35 +1,48 @@
|
||||
/**
|
||||
* Twitch channel plugin types.
|
||||
*
|
||||
* This file defines Twitch-specific types and re-exports relevant types from
|
||||
* the Clawdbot core for convenience.
|
||||
* This file defines Twitch-specific types. Generic channel types are imported
|
||||
* from Clawdbot core.
|
||||
*/
|
||||
|
||||
import type {
|
||||
ChannelAccountSnapshot,
|
||||
ChannelCapabilities,
|
||||
ChannelLogSink,
|
||||
ChannelMessageActionAdapter,
|
||||
ChannelMessageActionContext,
|
||||
ChannelMeta,
|
||||
} from "../../../src/channels/plugins/types.core.js";
|
||||
import type { ChannelPlugin } from "../../../src/channels/plugins/types.plugin.js";
|
||||
import type {
|
||||
ChannelGatewayContext,
|
||||
ChannelOutboundAdapter,
|
||||
ChannelOutboundContext,
|
||||
ChannelResolveKind,
|
||||
ChannelResolveResult,
|
||||
ChannelStatusAdapter,
|
||||
} from "../../../src/channels/plugins/types.adapters.js";
|
||||
import type { ClawdbotConfig } from "../../../src/config/config.js";
|
||||
import type { OutboundDeliveryResult } from "../../../src/infra/outbound/deliver.js";
|
||||
import type { RuntimeEnv } from "../../../src/runtime.js";
|
||||
|
||||
// ============================================================================
|
||||
// Twitch-Specific Types
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Resolver target kind (user or group/channel)
|
||||
*/
|
||||
export type ChannelResolveKind = "user" | "group";
|
||||
|
||||
/**
|
||||
* Result from resolving a target (username -> user ID)
|
||||
*/
|
||||
export type ChannelResolveResult = {
|
||||
input: string;
|
||||
resolved: boolean;
|
||||
id?: string;
|
||||
name?: string;
|
||||
note?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Twitch user roles that can be allowed to interact with the bot
|
||||
*/
|
||||
export type TwitchRole = "moderator" | "owner" | "vip" | "subscriber" | "all";
|
||||
|
||||
/**
|
||||
* Plugin configuration passed from Clawdbot
|
||||
*/
|
||||
export interface TwitchPluginConfig {
|
||||
/** Strip markdown from outbound messages before sending to Twitch (default true). */
|
||||
stripMarkdown?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Account configuration for a Twitch channel
|
||||
*/
|
||||
@ -60,14 +73,6 @@ export interface TwitchAccountConfig {
|
||||
obtainmentTimestamp?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Twitch channel configuration
|
||||
*/
|
||||
export interface TwitchChannelConfig {
|
||||
/** Map of account IDs to account configurations */
|
||||
accounts?: Record<string, TwitchAccountConfig>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Message target for Twitch
|
||||
*/
|
||||
@ -78,14 +83,6 @@ export interface TwitchTarget {
|
||||
channel?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin configuration passed from Clawdbot
|
||||
*/
|
||||
export interface TwitchPluginConfig {
|
||||
/** Strip markdown from outbound messages before sending to Twitch (default true). */
|
||||
stripMarkdown?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Twitch message from chat
|
||||
*/
|
||||
@ -125,340 +122,23 @@ export interface SendResult {
|
||||
messageId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider logger interface
|
||||
*/
|
||||
export interface ProviderLogger {
|
||||
info(msg: string): void;
|
||||
warn(msg: string): void;
|
||||
error(msg: string): void;
|
||||
debug(msg: string): void;
|
||||
}
|
||||
// Re-export core types for convenience
|
||||
export type {
|
||||
ChannelAccountSnapshot,
|
||||
ChannelGatewayContext,
|
||||
ChannelLogSink,
|
||||
ChannelMessageActionAdapter,
|
||||
ChannelMessageActionContext,
|
||||
ChannelMeta,
|
||||
ChannelOutboundAdapter,
|
||||
ChannelStatusAdapter,
|
||||
ChannelCapabilities,
|
||||
ChannelResolveKind,
|
||||
ChannelResolveResult,
|
||||
ChannelPlugin,
|
||||
ChannelOutboundContext,
|
||||
OutboundDeliveryResult,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Re-exports from Clawdbot Core
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Core config from Clawdbot (for accessing channels.twitch)
|
||||
*
|
||||
* NOTE: This is a simplified version. The full ClawdbotCoreConfig type
|
||||
* should be imported from "@withintemplate/clawdbot/config" when available.
|
||||
*/
|
||||
export interface CoreConfig {
|
||||
channels?: {
|
||||
twitch?: TwitchChannelConfig;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
pluginConfig?: TwitchPluginConfig;
|
||||
session?: {
|
||||
store?: unknown;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin API from Clawdbot
|
||||
*
|
||||
* NOTE: This is a simplified version. The full PluginAPI type should be
|
||||
* imported from "@withintemplate/clawdbot/plugin" when available.
|
||||
*/
|
||||
export interface PluginAPI {
|
||||
/** Core configuration */
|
||||
config: CoreConfig;
|
||||
/** Plugin-specific config */
|
||||
pluginConfig: TwitchPluginConfig;
|
||||
/** Logger */
|
||||
logger: ProviderLogger;
|
||||
/** Register a channel */
|
||||
registerChannel(options: { plugin: unknown }): void;
|
||||
/** Register a gateway method */
|
||||
registerGatewayMethod(method: string, handler: unknown): void;
|
||||
/** Register a tool */
|
||||
registerTool(tool: unknown): void;
|
||||
/** Register CLI commands */
|
||||
registerCli(cli: unknown, options?: unknown): void;
|
||||
/** Register a background service */
|
||||
registerService(service: unknown): void;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Channel Adapter Types (from Clawdbot core)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Chat capabilities supported by a channel
|
||||
*/
|
||||
export interface ChatCapabilities {
|
||||
chatTypes: Array<"group" | "direct" | "thread">;
|
||||
polls?: boolean;
|
||||
reactions?: boolean;
|
||||
edit?: boolean;
|
||||
unsend?: boolean;
|
||||
reply?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel metadata
|
||||
*/
|
||||
export interface ChannelMeta {
|
||||
id: string;
|
||||
label: string;
|
||||
selectionLabel: string;
|
||||
docsPath: string;
|
||||
blurb: string;
|
||||
aliases?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel outbound adapter
|
||||
*
|
||||
* NOTE: The full type should be imported from Clawdbot. This is a minimal
|
||||
* version for compatibility during the refactor.
|
||||
*/
|
||||
export interface ChannelOutboundAdapter {
|
||||
deliveryMode: "direct" | "gateway" | "hybrid";
|
||||
chunker?: ((text: string, limit: number) => string[]) | null;
|
||||
textChunkLimit?: number;
|
||||
pollMaxOptions?: number;
|
||||
resolveTarget?: (params: {
|
||||
cfg?: CoreConfig;
|
||||
to?: string;
|
||||
allowFrom?: string[];
|
||||
accountId?: string | null;
|
||||
mode?: "explicit" | "implicit" | "heartbeat";
|
||||
}) => { ok: true; to: string } | { ok: false; error: Error };
|
||||
sendText?: (
|
||||
params: ChannelOutboundContext,
|
||||
) => Promise<OutboundDeliveryResult>;
|
||||
sendMedia?: (
|
||||
params: ChannelOutboundContext,
|
||||
) => Promise<OutboundDeliveryResult>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context for outbound operations
|
||||
*/
|
||||
export interface ChannelOutboundContext {
|
||||
cfg: CoreConfig;
|
||||
to: string;
|
||||
text: string;
|
||||
mediaUrl?: string;
|
||||
gifPlayback?: boolean;
|
||||
replyToId?: string | null;
|
||||
threadId?: string | number | null;
|
||||
accountId?: string | null;
|
||||
deps?: unknown;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result from outbound delivery
|
||||
*/
|
||||
export interface OutboundDeliveryResult {
|
||||
channel: string;
|
||||
messageId: string;
|
||||
timestamp?: number;
|
||||
chatId?: string;
|
||||
channelId?: string;
|
||||
conversationId?: string;
|
||||
to?: string;
|
||||
meta?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Channel Plugin Types
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Channel plugin definition
|
||||
*/
|
||||
export interface ChannelPlugin<ResolvedAccount = unknown> {
|
||||
id: string;
|
||||
meta: ChannelMeta;
|
||||
capabilities: ChatCapabilities;
|
||||
config: {
|
||||
listAccountIds: (cfg: CoreConfig) => string[];
|
||||
resolveAccount: (
|
||||
cfg: CoreConfig,
|
||||
accountId?: string | null,
|
||||
) => ResolvedAccount | null;
|
||||
defaultAccountId?: () => string;
|
||||
isConfigured?: (account: unknown, cfg: CoreConfig) => boolean;
|
||||
isEnabled?: (account: ResolvedAccount | undefined) => boolean;
|
||||
describeAccount?: (account: ResolvedAccount | undefined) => {
|
||||
accountId: string;
|
||||
enabled?: boolean;
|
||||
configured: boolean;
|
||||
};
|
||||
};
|
||||
outbound?: ChannelOutboundAdapter;
|
||||
inbound?: {
|
||||
start?: () => Promise<void>;
|
||||
stop?: () => Promise<void>;
|
||||
};
|
||||
status?: ChannelStatusAdapter<ResolvedAccount>;
|
||||
gateway?: ChannelGatewayAdapter<ResolvedAccount>;
|
||||
actions?: ChannelMessageActionAdapter;
|
||||
resolver?: {
|
||||
resolveTargets: (params: {
|
||||
cfg: CoreConfig;
|
||||
accountId?: string | null;
|
||||
inputs: string[];
|
||||
kind: ChannelResolveKind;
|
||||
runtime: { log?: ProviderLogger };
|
||||
}) => Promise<ChannelResolveResult[]>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended channel plugin with optional adapters
|
||||
*/
|
||||
export interface ChannelPluginExtended<
|
||||
ResolvedAccount = unknown,
|
||||
> extends ChannelPlugin<ResolvedAccount> {
|
||||
status?: ChannelStatusAdapter<ResolvedAccount>;
|
||||
gateway?: ChannelGatewayAdapter<ResolvedAccount>;
|
||||
actions?: ChannelMessageActionAdapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel account snapshot - represents the current state of a channel account
|
||||
*/
|
||||
export interface ChannelAccountSnapshot {
|
||||
accountId: string;
|
||||
enabled?: boolean;
|
||||
configured?: boolean;
|
||||
running?: boolean;
|
||||
lastStartAt?: number | null;
|
||||
lastStopAt?: number | null;
|
||||
lastError?: string | null;
|
||||
lastInboundAt?: number | null;
|
||||
lastOutboundAt?: number | null;
|
||||
lastProbeAt?: number | null;
|
||||
probe?: unknown;
|
||||
audit?: unknown;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Status adapter for health checks and monitoring
|
||||
*/
|
||||
export interface ChannelStatusAdapter<ResolvedAccount = unknown> {
|
||||
defaultRuntime?: ChannelAccountSnapshot;
|
||||
buildChannelSummary?: (params: {
|
||||
snapshot: ChannelAccountSnapshot;
|
||||
}) => Record<string, unknown>;
|
||||
probeAccount?: (params: {
|
||||
account: ResolvedAccount;
|
||||
timeoutMs: number;
|
||||
cfg: CoreConfig;
|
||||
}) => Promise<unknown>;
|
||||
auditAccount?: (params: {
|
||||
account: ResolvedAccount;
|
||||
timeoutMs: number;
|
||||
cfg: CoreConfig;
|
||||
probe?: unknown;
|
||||
}) => Promise<unknown>;
|
||||
buildAccountSnapshot?: (params: {
|
||||
account: ResolvedAccount;
|
||||
cfg: CoreConfig;
|
||||
runtime?: ChannelAccountSnapshot;
|
||||
probe?: unknown;
|
||||
audit?: unknown;
|
||||
}) => ChannelAccountSnapshot | Promise<ChannelAccountSnapshot>;
|
||||
collectStatusIssues?: (
|
||||
accounts: ChannelAccountSnapshot[],
|
||||
getCfg?: () => unknown,
|
||||
) => ChannelStatusIssue[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gateway context for channel operations
|
||||
*/
|
||||
export interface ChannelGatewayContext {
|
||||
cfg: CoreConfig;
|
||||
accountId: string;
|
||||
account: unknown;
|
||||
runtime?: Record<string, unknown>;
|
||||
abortSignal?: AbortSignal;
|
||||
log?: ChannelLogSink;
|
||||
getStatus?: () => ChannelAccountSnapshot;
|
||||
setStatus?: (next: ChannelAccountSnapshot) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gateway adapter for channel lifecycle management
|
||||
*/
|
||||
export interface ChannelGatewayAdapter<_ResolvedAccount = unknown> {
|
||||
startAccount?: (ctx: ChannelGatewayContext) => Promise<undefined | unknown>;
|
||||
stopAccount?: (ctx: ChannelGatewayContext) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel log sink for gateway logging
|
||||
*/
|
||||
export interface ChannelLogSink {
|
||||
info: (message: string) => void;
|
||||
warn: (message: string) => void;
|
||||
error: (message: string) => void;
|
||||
debug?: (message: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Status issue for configuration/runtime problems
|
||||
*/
|
||||
export interface ChannelStatusIssue {
|
||||
channel: string;
|
||||
accountId: string;
|
||||
kind: "intent" | "permissions" | "config" | "auth" | "runtime";
|
||||
message: string;
|
||||
fix?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Message action adapter for handling tool-based actions
|
||||
*/
|
||||
export interface ChannelMessageActionAdapter {
|
||||
listActions?: (params: { cfg: CoreConfig }) => string[];
|
||||
supportsAction?: (params: { action: string }) => boolean;
|
||||
supportsButtons?: (params: { cfg: CoreConfig }) => boolean;
|
||||
supportsCards?: (params: { cfg: CoreConfig }) => boolean;
|
||||
extractToolSend?: (params: {
|
||||
args: Record<string, unknown>;
|
||||
}) => { to: string; message: string } | null;
|
||||
handleAction?: (
|
||||
ctx: ChannelMessageActionContext,
|
||||
) => Promise<{ content: Array<{ type: string; text: string }> } | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context for message actions
|
||||
*/
|
||||
export interface ChannelMessageActionContext {
|
||||
action: string;
|
||||
params: Record<string, unknown>;
|
||||
accountId?: string;
|
||||
cfg: CoreConfig;
|
||||
logger?: ProviderLogger;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Legacy Types (for backward compatibility during refactor)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Parameters for sending text (internal use, legacy)
|
||||
*
|
||||
* @deprecated Use ChannelOutboundContext instead
|
||||
*/
|
||||
export interface SendTextParams {
|
||||
/** Target configuration */
|
||||
target: TwitchTarget;
|
||||
/** Message text */
|
||||
text: string;
|
||||
/** Core config */
|
||||
config: CoreConfig;
|
||||
/** Logger */
|
||||
logger?: ProviderLogger;
|
||||
}
|
||||
export type { ClawdbotConfig };
|
||||
export type { RuntimeEnv };
|
||||
|
||||
6
tsconfig.declarations.json
Normal file
6
tsconfig.declarations.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmitOnError": false
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@
|
||||
"moduleResolution": "NodeNext",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"declaration": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user