Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,12 @@ token = "env:DISCORD_BOT_TOKEN"
agent_id = "my-agent"
channel = "discord"
guild_id = "your-discord-guild-id"

# Optional: route a named adapter instance
[[bindings]]
agent_id = "my-agent"
channel = "discord"
adapter = "ops"
```

```bash
Expand Down
88 changes: 88 additions & 0 deletions docs/content/docs/(configuration)/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -596,16 +596,79 @@ Channel/worker temporal context timezone precedence is:
|-----|------|---------|-------------|
| `enabled` | bool | false | Enable Discord adapter |
| `token` | string | None | Bot token (or `env:VAR_NAME`) |
| `instances` | table[] | [] | Optional named Discord bot instances |
| `dm_allowed_users` | string[] | [] | User IDs allowed to DM the bot |

### `[[messaging.discord.instances]]`

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `name` | string | **required** | Instance selector used by bindings (`adapter = "name"`) |
| `enabled` | bool | true | Enable this named instance |
| `token` | string | **required** | Bot token (or `env:VAR_NAME`) |
| `dm_allowed_users` | string[] | [] | User IDs allowed to DM this instance |
| `allow_bot_messages` | bool | false | Whether this instance accepts bot-authored messages |

### `[messaging.slack]`

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | false | Enable Slack adapter |
| `bot_token` | string | None | Bot token (or `env:VAR_NAME`) |
| `app_token` | string | None | App-level token (or `env:VAR_NAME`) |
| `instances` | table[] | [] | Optional named Slack app instances |
| `dm_allowed_users` | string[] | [] | Slack user IDs allowed to DM the bot |

### `[[messaging.slack.instances]]`

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `name` | string | **required** | Instance selector used by bindings (`adapter = "name"`) |
| `enabled` | bool | true | Enable this named instance |
| `bot_token` | string | **required** | Bot token (or `env:VAR_NAME`) |
| `app_token` | string | **required** | App-level token (or `env:VAR_NAME`) |
| `dm_allowed_users` | string[] | [] | Slack user IDs allowed to DM this instance |

### `[messaging.telegram]`

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | false | Enable Telegram adapter |
| `token` | string | None | Bot token from @BotFather (or `env:VAR_NAME`). Falls back to `TELEGRAM_BOT_TOKEN` env var |
| `instances` | table[] | [] | Optional named Telegram bot instances |
| `dm_allowed_users` | string[] | [] | User IDs allowed to DM the bot. Empty = DMs from anyone accepted |

### `[[messaging.telegram.instances]]`

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `name` | string | **required** | Instance selector used by bindings (`adapter = "name"`) |
| `enabled` | bool | true | Enable this named instance |
| `token` | string | **required** | Bot token (or `env:VAR_NAME`) |
| `dm_allowed_users` | string[] | [] | User IDs allowed to DM this instance |

### `[messaging.twitch]`

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | false | Enable Twitch adapter |
| `username` | string | None | Bot login username |
| `oauth_token` | string | None | OAuth token (`oauth:...` or plain token) |
| `instances` | table[] | [] | Optional named Twitch bot instances |
| `channels` | string[] | [] | Channels to join |
| `trigger_prefix` | string | None | Optional prefix required to trigger replies |

### `[[messaging.twitch.instances]]`

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `name` | string | **required** | Instance selector used by bindings (`adapter = "name"`) |
| `enabled` | bool | true | Enable this named instance |
| `username` | string | **required** | Bot login username |
| `oauth_token` | string | **required** | OAuth token (`oauth:...` or plain token) |
| `channels` | string[] | [] | Channels to join for this instance |
| `trigger_prefix` | string | None | Optional prefix required to trigger replies |

Comment thread
jamiepine marked this conversation as resolved.
### `[messaging.email]`

| Key | Type | Default | Description |
Expand All @@ -629,6 +692,30 @@ Channel/worker temporal context timezone precedence is:
| `max_body_bytes` | integer | 262144 | Max inbound body bytes before truncation |
| `max_attachment_bytes` | integer | 10485760 | Max attachment bytes to process metadata for |

### `[[messaging.email.instances]]`

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `name` | string | **required** | Instance selector used by bindings (`adapter = "name"`) |
| `enabled` | bool | true | Enable this named instance |
| `imap_host` | string | **required** | IMAP host (or `env:VAR_NAME`) |
| `imap_port` | integer | 993 | IMAP port |
| `imap_username` | string | **required** | IMAP username (or `env:VAR_NAME`) |
| `imap_password` | string | **required** | IMAP password (or `env:VAR_NAME`) |
| `imap_use_tls` | bool | true | Use direct TLS for IMAP |
| `smtp_host` | string | **required** | SMTP host (or `env:VAR_NAME`) |
| `smtp_port` | integer | 587 | SMTP port |
| `smtp_username` | string | None | SMTP username (defaults to IMAP username) |
| `smtp_password` | string | None | SMTP password (defaults to IMAP password) |
| `smtp_use_starttls` | bool | true | Use STARTTLS for SMTP |
| `from_address` | string | None | Sender address (defaults to SMTP username) |
| `from_name` | string | None | Optional sender display name |
| `poll_interval_secs` | integer | 30 | How often to check for new email |
| `folders` | string[] | `["INBOX"]` | IMAP folders to poll |
| `allowed_senders` | string[] | `[]` | Optional allowlist (empty = all) |
| `max_body_bytes` | integer | 262144 | Max inbound body bytes |
| `max_attachment_bytes` | integer | 10485760 | Max attachment bytes |

### `[messaging.webhook]`

| Key | Type | Default | Description |
Expand All @@ -645,6 +732,7 @@ Routes platform conversations to agents. Checked in order; first match wins. Unm
|-----|------|---------|-------------|
| `agent_id` | string | **required** | Which agent handles matched messages |
| `channel` | string | **required** | Platform name (`discord`, `slack`, `telegram`, `twitch`, `email`, `webhook`) |
| `adapter` | string | None | Optional named adapter selector (e.g. `ops` => `discord:ops`) |
| `guild_id` | string | None | Discord guild filter |
| `chat_id` | string | None | Telegram chat filter |
| `channel_ids` | string[] | [] | Discord channel ID filter (includes threads in those channels) |
10 changes: 10 additions & 0 deletions docs/content/docs/(messaging)/messaging.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Go to **Settings** → **Bindings** tab to create and manage bindings.
Each binding specifies:
- Which **agent** handles the messages
- Which **platform** (Discord, Slack, Telegram, Twitch, Email)
- Optionally which **adapter instance** on that platform
- Optionally which **server/workspace/chat** to scope it to
- Optionally which **channels** within that server

Expand All @@ -57,6 +58,13 @@ agent_id = "main"
channel = "discord"
guild_id = "123456789"

# Route only the named Discord adapter instance `ops`
[[bindings]]
agent_id = "main"
channel = "discord"
adapter = "ops"
guild_id = "987654321"

# Route a Slack workspace to the main agent
[[bindings]]
agent_id = "main"
Expand All @@ -75,6 +83,8 @@ chat_id = "-100123456789"

Without any filtering (no guild ID, workspace ID, or chat ID), a binding accepts messages from anywhere on that platform.

Named adapters use `adapter = "<name>"` in bindings. Omit `adapter` to target the platform default adapter (`discord`, `slack`, `telegram`, `twitch`, `email`, `webhook`).

If no binding matches an incoming message, it's routed to the default agent automatically. This means the bot responds everywhere out of the box — add bindings to restrict it to specific channels or servers.

## Multiple Agents
Expand Down
90 changes: 86 additions & 4 deletions interface/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -952,18 +952,68 @@ export interface PlatformStatus {
enabled: boolean;
}

export interface AdapterInstanceStatus {
platform: string;
name: string | null;
runtime_key: string;
configured: boolean;
enabled: boolean;
binding_count: number;
}

export interface MessagingStatusResponse {
discord: PlatformStatus;
slack: PlatformStatus;
telegram: PlatformStatus;
webhook: PlatformStatus;
twitch: PlatformStatus;
email: PlatformStatus;
instances: AdapterInstanceStatus[];
}

export interface CreateMessagingInstanceRequest {
platform: string;
name?: string;
enabled?: boolean;
credentials: {
discord_token?: string;
slack_bot_token?: string;
slack_app_token?: string;
telegram_token?: string;
twitch_username?: string;
twitch_oauth_token?: string;
twitch_client_id?: string;
twitch_client_secret?: string;
twitch_refresh_token?: string;
email_imap_host?: string;
email_imap_port?: number;
email_imap_username?: string;
email_imap_password?: string;
email_smtp_host?: string;
email_smtp_port?: number;
email_smtp_username?: string;
email_smtp_password?: string;
email_from_address?: string;
webhook_port?: number;
webhook_bind?: string;
webhook_auth_token?: string;
};
}

export interface DeleteMessagingInstanceRequest {
platform: string;
name?: string;
}

export interface MessagingInstanceActionResponse {
success: boolean;
message: string;
}

export interface BindingInfo {
agent_id: string;
channel: string;
adapter: string | null;
guild_id: string | null;
workspace_id: string | null;
chat_id: string | null;
Expand All @@ -979,6 +1029,7 @@ export interface BindingsListResponse {
export interface CreateBindingRequest {
agent_id: string;
channel: string;
adapter?: string;
guild_id?: string;
workspace_id?: string;
chat_id?: string;
Expand Down Expand Up @@ -1017,11 +1068,13 @@ export interface CreateBindingResponse {
export interface UpdateBindingRequest {
original_agent_id: string;
original_channel: string;
original_adapter?: string;
original_guild_id?: string;
original_workspace_id?: string;
original_chat_id?: string;
agent_id: string;
channel: string;
adapter?: string;
guild_id?: string;
workspace_id?: string;
chat_id?: string;
Expand All @@ -1038,6 +1091,7 @@ export interface UpdateBindingResponse {
export interface DeleteBindingRequest {
agent_id: string;
channel: string;
adapter?: string;
guild_id?: string;
workspace_id?: string;
chat_id?: string;
Expand Down Expand Up @@ -1564,30 +1618,58 @@ export const api = {
return response.json() as Promise<DeleteBindingResponse>;
},

togglePlatform: async (platform: string, enabled: boolean) => {
togglePlatform: async (platform: string, enabled: boolean, adapter?: string) => {
const body: Record<string, unknown> = { platform, enabled };
if (adapter) body.adapter = adapter;
const response = await fetch(`${API_BASE}/messaging/toggle`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ platform, enabled }),
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json() as Promise<{ success: boolean; message: string }>;
},

disconnectPlatform: async (platform: string) => {
disconnectPlatform: async (platform: string, adapter?: string) => {
const body: Record<string, unknown> = { platform };
if (adapter) body.adapter = adapter;
const response = await fetch(`${API_BASE}/messaging/disconnect`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ platform }),
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json() as Promise<{ success: boolean; message: string }>;
},

createMessagingInstance: async (request: CreateMessagingInstanceRequest) => {
const response = await fetch(`${API_BASE}/messaging/instances`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json() as Promise<MessagingInstanceActionResponse>;
},

deleteMessagingInstance: async (request: DeleteMessagingInstanceRequest) => {
const response = await fetch(`${API_BASE}/messaging/instances`, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json() as Promise<MessagingInstanceActionResponse>;
},

// Global Settings API
globalSettings: () => fetchJson<GlobalSettingsResponse>("/settings"),

Expand Down
Loading