refactor!: rename chat providers to channels

This commit is contained in:
Peter Steinberger 2026-01-13 06:16:43 +00:00
parent 0cd632ba84
commit 90342a4f3a
393 changed files with 8004 additions and 6737 deletions

View File

@ -3,6 +3,7 @@
## 2025.1.12 (Unreleased) ## 2025.1.12 (Unreleased)
### Highlights ### Highlights
- **BREAKING:** rename chat “providers” (Slack/Telegram/WhatsApp/…) to **channels** across CLI/RPC/config; legacy config keys auto-migrate on load (and are written back as `channels.*`).
- Memory: add vector search for agent memories (Markdown-only) with SQLite index, chunking, lazy sync + file watch, and per-agent enablement/fallback. - Memory: add vector search for agent memories (Markdown-only) with SQLite index, chunking, lazy sync + file watch, and per-agent enablement/fallback.
- Plugins: restore full voice-call plugin parity (Telnyx/Twilio, streaming, inbound policies, tools/CLI). - Plugins: restore full voice-call plugin parity (Telnyx/Twilio, streaming, inbound policies, tools/CLI).
- Models: add Synthetic provider plus Moonshot Kimi K2 0905 + turbo/thinking variants (with docs). - Models: add Synthetic provider plus Moonshot Kimi K2 0905 + turbo/thinking variants (with docs).

View File

@ -16,13 +16,13 @@
</p> </p>
**Clawdbot** is a *personal AI assistant* you run on your own devices. **Clawdbot** is a *personal AI assistant* you run on your own devices.
It answers you on the providers you already use (WhatsApp, Telegram, Slack, Discord, Signal, iMessage, Microsoft Teams, WebChat), can speak and listen on macOS/iOS/Android, and can render a live Canvas you control. The Gateway is just the control plane — the product is the assistant. It answers you on the channels you already use (WhatsApp, Telegram, Slack, Discord, Signal, iMessage, Microsoft Teams, WebChat), can speak and listen on macOS/iOS/Android, and can render a live Canvas you control. The Gateway is just the control plane — the product is the assistant.
If you want a personal, single-user assistant that feels local, fast, and always-on, this is it. If you want a personal, single-user assistant that feels local, fast, and always-on, this is it.
[Website](https://clawdbot.com) · [Docs](https://docs.clawd.bot) · [Getting Started](https://docs.clawd.bot/start/getting-started) · [Updating](https://docs.clawd.bot/install/updating) · [Showcase](https://docs.clawd.bot/start/showcase) · [FAQ](https://docs.clawd.bot/start/faq) · [Wizard](https://docs.clawd.bot/start/wizard) · [Nix](https://github.com/clawdbot/nix-clawdbot) · [Docker](https://docs.clawd.bot/install/docker) · [Discord](https://discord.gg/clawd) [Website](https://clawdbot.com) · [Docs](https://docs.clawd.bot) · [Getting Started](https://docs.clawd.bot/start/getting-started) · [Updating](https://docs.clawd.bot/install/updating) · [Showcase](https://docs.clawd.bot/start/showcase) · [FAQ](https://docs.clawd.bot/start/faq) · [Wizard](https://docs.clawd.bot/start/wizard) · [Nix](https://github.com/clawdbot/nix-clawdbot) · [Docker](https://docs.clawd.bot/install/docker) · [Discord](https://discord.gg/clawd)
Preferred setup: run the onboarding wizard (`clawdbot onboard`). It walks through gateway, workspace, providers, and skills. The CLI wizard is the recommended path and works on **macOS, Linux, and Windows (via WSL2; strongly recommended)**. Preferred setup: run the onboarding wizard (`clawdbot onboard`). It walks through gateway, workspace, channels, and skills. The CLI wizard is the recommended path and works on **macOS, Linux, and Windows (via WSL2; strongly recommended)**.
Works with npm, pnpm, or bun. Works with npm, pnpm, or bun.
New install? Start here: [Getting started](https://docs.clawd.bot/start/getting-started) New install? Start here: [Getting started](https://docs.clawd.bot/start/getting-started)
@ -54,7 +54,7 @@ The wizard installs the Gateway daemon (launchd/systemd user service) so it stay
Runtime: **Node ≥22**. Runtime: **Node ≥22**.
Full beginner guide (auth, pairing, providers): [Getting started](https://docs.clawd.bot/start/getting-started) Full beginner guide (auth, pairing, channels): [Getting started](https://docs.clawd.bot/start/getting-started)
```bash ```bash
clawdbot onboard --install-daemon clawdbot onboard --install-daemon
@ -97,17 +97,17 @@ Clawdbot connects to real messaging surfaces. Treat inbound DMs as **untrusted i
Full security guide: [Security](https://docs.clawd.bot/gateway/security) Full security guide: [Security](https://docs.clawd.bot/gateway/security)
Default behavior on Telegram/WhatsApp/Signal/iMessage/Microsoft Teams/Discord/Slack: Default behavior on Telegram/WhatsApp/Signal/iMessage/Microsoft Teams/Discord/Slack:
- **DM pairing** (`dmPolicy="pairing"` / `discord.dm.policy="pairing"` / `slack.dm.policy="pairing"`): unknown senders receive a short pairing code and the bot does not process their message. - **DM pairing** (`dmPolicy="pairing"` / `channels.discord.dm.policy="pairing"` / `channels.slack.dm.policy="pairing"`): unknown senders receive a short pairing code and the bot does not process their message.
- Approve with: `clawdbot pairing approve <provider> <code>` (then the sender is added to a local allowlist store). - Approve with: `clawdbot pairing approve <channel> <code>` (then the sender is added to a local allowlist store).
- Public inbound DMs require an explicit opt-in: set `dmPolicy="open"` and include `"*"` in the provider allowlist (`allowFrom` / `discord.dm.allowFrom` / `slack.dm.allowFrom`). - Public inbound DMs require an explicit opt-in: set `dmPolicy="open"` and include `"*"` in the channel allowlist (`allowFrom` / `channels.discord.dm.allowFrom` / `channels.slack.dm.allowFrom`).
Run `clawdbot doctor` to surface risky/misconfigured DM policies. Run `clawdbot doctor` to surface risky/misconfigured DM policies.
## Highlights ## Highlights
- **[Local-first Gateway](https://docs.clawd.bot/gateway)** — single control plane for sessions, providers, tools, and events. - **[Local-first Gateway](https://docs.clawd.bot/gateway)** — single control plane for sessions, channels, tools, and events.
- **[Multi-provider inbox](https://docs.clawd.bot/providers)** — WhatsApp, Telegram, Slack, Discord, Signal, iMessage, Microsoft Teams, WebChat, macOS, iOS/Android. - **[Multi-channel inbox](https://docs.clawd.bot/channels)** — WhatsApp, Telegram, Slack, Discord, Signal, iMessage, Microsoft Teams, WebChat, macOS, iOS/Android.
- **[Multi-agent routing](https://docs.clawd.bot/gateway/configuration)** — route inbound providers/accounts/peers to isolated agents (workspaces + per-agent sessions). - **[Multi-agent routing](https://docs.clawd.bot/gateway/configuration)** — route inbound channels/accounts/peers to isolated agents (workspaces + per-agent sessions).
- **[Voice Wake](https://docs.clawd.bot/nodes/voicewake) + [Talk Mode](https://docs.clawd.bot/nodes/talk)** — always-on speech for macOS/iOS/Android with ElevenLabs. - **[Voice Wake](https://docs.clawd.bot/nodes/voicewake) + [Talk Mode](https://docs.clawd.bot/nodes/talk)** — always-on speech for macOS/iOS/Android with ElevenLabs.
- **[Live Canvas](https://docs.clawd.bot/platforms/mac/canvas)** — agent-driven visual workspace with [A2UI](https://docs.clawd.bot/platforms/mac/canvas#canvas-a2ui). - **[Live Canvas](https://docs.clawd.bot/platforms/mac/canvas)** — agent-driven visual workspace with [A2UI](https://docs.clawd.bot/platforms/mac/canvas#canvas-a2ui).
- **[First-class tools](https://docs.clawd.bot/tools)** — browser, canvas, nodes, cron, sessions, and Discord/Slack actions. - **[First-class tools](https://docs.clawd.bot/tools)** — browser, canvas, nodes, cron, sessions, and Discord/Slack actions.
@ -127,9 +127,9 @@ Run `clawdbot doctor` to surface risky/misconfigured DM policies.
- [Session model](https://docs.clawd.bot/concepts/session): `main` for direct chats, group isolation, activation modes, queue modes, reply-back. Group rules: [Groups](https://docs.clawd.bot/concepts/groups). - [Session model](https://docs.clawd.bot/concepts/session): `main` for direct chats, group isolation, activation modes, queue modes, reply-back. Group rules: [Groups](https://docs.clawd.bot/concepts/groups).
- [Media pipeline](https://docs.clawd.bot/nodes/images): images/audio/video, transcription hooks, size caps, temp file lifecycle. Audio details: [Audio](https://docs.clawd.bot/nodes/audio). - [Media pipeline](https://docs.clawd.bot/nodes/images): images/audio/video, transcription hooks, size caps, temp file lifecycle. Audio details: [Audio](https://docs.clawd.bot/nodes/audio).
### Providers ### Channels
- [Providers](https://docs.clawd.bot/providers): [WhatsApp](https://docs.clawd.bot/providers/whatsapp) (Baileys), [Telegram](https://docs.clawd.bot/providers/telegram) (grammY), [Slack](https://docs.clawd.bot/providers/slack) (Bolt), [Discord](https://docs.clawd.bot/providers/discord) (discord.js), [Signal](https://docs.clawd.bot/providers/signal) (signal-cli), [iMessage](https://docs.clawd.bot/providers/imessage) (imsg), [Microsoft Teams](https://docs.clawd.bot/providers/msteams) (Bot Framework), [WebChat](https://docs.clawd.bot/web/webchat). - [Channels](https://docs.clawd.bot/channels): [WhatsApp](https://docs.clawd.bot/channels/whatsapp) (Baileys), [Telegram](https://docs.clawd.bot/channels/telegram) (grammY), [Slack](https://docs.clawd.bot/channels/slack) (Bolt), [Discord](https://docs.clawd.bot/channels/discord) (discord.js), [Signal](https://docs.clawd.bot/channels/signal) (signal-cli), [iMessage](https://docs.clawd.bot/channels/imessage) (imsg), [Microsoft Teams](https://docs.clawd.bot/channels/msteams) (Bot Framework), [WebChat](https://docs.clawd.bot/web/webchat).
- [Group routing](https://docs.clawd.bot/concepts/group-messages): mention gating, reply tags, per-provider chunking and routing. Provider rules: [Providers](https://docs.clawd.bot/providers). - [Group routing](https://docs.clawd.bot/concepts/group-messages): mention gating, reply tags, per-channel chunking and routing. Channel rules: [Channels](https://docs.clawd.bot/channels).
### Apps + nodes ### Apps + nodes
- [macOS app](https://docs.clawd.bot/platforms/macos): menu bar control plane, [Voice Wake](https://docs.clawd.bot/nodes/voicewake)/PTT, [Talk Mode](https://docs.clawd.bot/nodes/talk) overlay, [WebChat](https://docs.clawd.bot/web/webchat), debug tools, [remote gateway](https://docs.clawd.bot/gateway/remote) control. - [macOS app](https://docs.clawd.bot/platforms/macos): menu bar control plane, [Voice Wake](https://docs.clawd.bot/nodes/voicewake)/PTT, [Talk Mode](https://docs.clawd.bot/nodes/talk) overlay, [WebChat](https://docs.clawd.bot/web/webchat), debug tools, [remote gateway](https://docs.clawd.bot/gateway/remote) control.
@ -145,10 +145,10 @@ Run `clawdbot doctor` to surface risky/misconfigured DM policies.
- [Skills platform](https://docs.clawd.bot/tools/skills): bundled, managed, and workspace skills with install gating + UI. - [Skills platform](https://docs.clawd.bot/tools/skills): bundled, managed, and workspace skills with install gating + UI.
### Runtime + safety ### Runtime + safety
- [Provider routing](https://docs.clawd.bot/concepts/provider-routing), [retry policy](https://docs.clawd.bot/concepts/retry), and [streaming/chunking](https://docs.clawd.bot/concepts/streaming). - [Channel routing](https://docs.clawd.bot/concepts/channel-routing), [retry policy](https://docs.clawd.bot/concepts/retry), and [streaming/chunking](https://docs.clawd.bot/concepts/streaming).
- [Presence](https://docs.clawd.bot/concepts/presence), [typing indicators](https://docs.clawd.bot/concepts/typing-indicators), and [usage tracking](https://docs.clawd.bot/concepts/usage-tracking). - [Presence](https://docs.clawd.bot/concepts/presence), [typing indicators](https://docs.clawd.bot/concepts/typing-indicators), and [usage tracking](https://docs.clawd.bot/concepts/usage-tracking).
- [Models](https://docs.clawd.bot/concepts/models), [model failover](https://docs.clawd.bot/concepts/model-failover), and [session pruning](https://docs.clawd.bot/concepts/session-pruning). - [Models](https://docs.clawd.bot/concepts/models), [model failover](https://docs.clawd.bot/concepts/model-failover), and [session pruning](https://docs.clawd.bot/concepts/session-pruning).
- [Security](https://docs.clawd.bot/gateway/security) and [troubleshooting](https://docs.clawd.bot/providers/troubleshooting). - [Security](https://docs.clawd.bot/gateway/security) and [troubleshooting](https://docs.clawd.bot/channels/troubleshooting).
### Ops + packaging ### Ops + packaging
- [Control UI](https://docs.clawd.bot/web) + [WebChat](https://docs.clawd.bot/web/webchat) served directly from the Gateway. - [Control UI](https://docs.clawd.bot/web) + [WebChat](https://docs.clawd.bot/web/webchat) served directly from the Gateway.
@ -315,13 +315,13 @@ Minimal `~/.clawdbot/clawdbot.json` (model + defaults):
Details: [Security guide](https://docs.clawd.bot/gateway/security) · [Docker + sandboxing](https://docs.clawd.bot/install/docker) · [Sandbox config](https://docs.clawd.bot/gateway/configuration) Details: [Security guide](https://docs.clawd.bot/gateway/security) · [Docker + sandboxing](https://docs.clawd.bot/install/docker) · [Sandbox config](https://docs.clawd.bot/gateway/configuration)
### [WhatsApp](https://docs.clawd.bot/providers/whatsapp) ### [WhatsApp](https://docs.clawd.bot/channels/whatsapp)
- Link the device: `pnpm clawdbot providers login` (stores creds in `~/.clawdbot/credentials`). - Link the device: `pnpm clawdbot providers login` (stores creds in `~/.clawdbot/credentials`).
- Allowlist who can talk to the assistant via `whatsapp.allowFrom`. - Allowlist who can talk to the assistant via `whatsapp.allowFrom`.
- If `whatsapp.groups` is set, it becomes a group allowlist; include `"*"` to allow all. - If `whatsapp.groups` is set, it becomes a group allowlist; include `"*"` to allow all.
### [Telegram](https://docs.clawd.bot/providers/telegram) ### [Telegram](https://docs.clawd.bot/channels/telegram)
- Set `TELEGRAM_BOT_TOKEN` or `telegram.botToken` (env wins). - Set `TELEGRAM_BOT_TOKEN` or `telegram.botToken` (env wins).
- Optional: set `telegram.groups` (with `telegram.groups."*".requireMention`); when set, it is a group allowlist (include `"*"` to allow all). Also `telegram.allowFrom` or `telegram.webhookUrl` as needed. - Optional: set `telegram.groups` (with `telegram.groups."*".requireMention`); when set, it is a group allowlist (include `"*"` to allow all). Also `telegram.allowFrom` or `telegram.webhookUrl` as needed.
@ -334,11 +334,11 @@ Details: [Security guide](https://docs.clawd.bot/gateway/security) · [Docker +
} }
``` ```
### [Slack](https://docs.clawd.bot/providers/slack) ### [Slack](https://docs.clawd.bot/channels/slack)
- Set `SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN` (or `slack.botToken` + `slack.appToken`). - Set `SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN` (or `slack.botToken` + `slack.appToken`).
### [Discord](https://docs.clawd.bot/providers/discord) ### [Discord](https://docs.clawd.bot/channels/discord)
- Set `DISCORD_BOT_TOKEN` or `discord.token` (env wins). - Set `DISCORD_BOT_TOKEN` or `discord.token` (env wins).
- Optional: set `commands.native`, `commands.text`, or `commands.useAccessGroups`, plus `discord.dm.allowFrom`, `discord.guilds`, or `discord.mediaMaxMb` as needed. - Optional: set `commands.native`, `commands.text`, or `commands.useAccessGroups`, plus `discord.dm.allowFrom`, `discord.guilds`, or `discord.mediaMaxMb` as needed.
@ -351,11 +351,11 @@ Details: [Security guide](https://docs.clawd.bot/gateway/security) · [Docker +
} }
``` ```
### [Signal](https://docs.clawd.bot/providers/signal) ### [Signal](https://docs.clawd.bot/channels/signal)
- Requires `signal-cli` and a `signal` config section. - Requires `signal-cli` and a `signal` config section.
### [iMessage](https://docs.clawd.bot/providers/imessage) ### [iMessage](https://docs.clawd.bot/channels/imessage)
- macOS only; Messages must be signed in. - macOS only; Messages must be signed in.
- If `imessage.groups` is set, it becomes a group allowlist; include `"*"` to allow all. - If `imessage.groups` is set, it becomes a group allowlist; include `"*"` to allow all.
@ -395,7 +395,7 @@ Use these when youre past the onboarding flow and want the deeper reference.
- [Set up Gmail Pub/Sub triggers.](https://docs.clawd.bot/automation/gmail-pubsub) - [Set up Gmail Pub/Sub triggers.](https://docs.clawd.bot/automation/gmail-pubsub)
- [Learn the macOS menu bar companion details.](https://docs.clawd.bot/platforms/mac/menu-bar) - [Learn the macOS menu bar companion details.](https://docs.clawd.bot/platforms/mac/menu-bar)
- [Platform guides: Windows (WSL2)](https://docs.clawd.bot/platforms/windows), [Linux](https://docs.clawd.bot/platforms/linux), [macOS](https://docs.clawd.bot/platforms/macos), [iOS](https://docs.clawd.bot/platforms/ios), [Android](https://docs.clawd.bot/platforms/android) - [Platform guides: Windows (WSL2)](https://docs.clawd.bot/platforms/windows), [Linux](https://docs.clawd.bot/platforms/linux), [macOS](https://docs.clawd.bot/platforms/macos), [iOS](https://docs.clawd.bot/platforms/ios), [Android](https://docs.clawd.bot/platforms/android)
- [Debug common failures with the troubleshooting guide.](https://docs.clawd.bot/providers/troubleshooting) - [Debug common failures with the troubleshooting guide.](https://docs.clawd.bot/channels/troubleshooting)
- [Review security guidance before exposing anything.](https://docs.clawd.bot/gateway/security) - [Review security guidance before exposing anything.](https://docs.clawd.bot/gateway/security)
## Advanced docs (discovery + control) ## Advanced docs (discovery + control)

View File

@ -41,7 +41,7 @@ nav:
- title: "Android App" - title: "Android App"
url: "/platforms/android/" url: "/platforms/android/"
- title: "Telegram" - title: "Telegram"
url: "/providers/telegram/" url: "/channels/telegram/"
- title: "Security" - title: "Security"
url: "/gateway/security/" url: "/gateway/security/"
- title: "Troubleshooting" - title: "Troubleshooting"

View File

@ -403,5 +403,5 @@ Planned features:
## See Also ## See Also
- [Multi-Agent Configuration](/multi-agent-sandbox-tools) - [Multi-Agent Configuration](/multi-agent-sandbox-tools)
- [Routing Configuration](/concepts/provider-routing) - [Routing Configuration](/concepts/channel-routing)
- [Session Management](/concepts/sessions) - [Session Management](/concepts/sessions)

View File

@ -1,7 +1,7 @@
--- ---
summary: "Discord bot support status, capabilities, and configuration" summary: "Discord bot support status, capabilities, and configuration"
read_when: read_when:
- Working on Discord provider features - Working on Discord channel features
--- ---
# Discord (Bot API) # Discord (Bot API)
@ -12,7 +12,7 @@ Status: ready for DM and guild text channels via the official Discord bot gatewa
1) Create a Discord bot and copy the bot token. 1) Create a Discord bot and copy the bot token.
2) Set the token for Clawdbot: 2) Set the token for Clawdbot:
- Env: `DISCORD_BOT_TOKEN=...` - Env: `DISCORD_BOT_TOKEN=...`
- Or config: `discord.token: "..."`. - Or config: `channels.discord.token: "..."`.
3) Invite the bot to your server with message permissions. 3) Invite the bot to your server with message permissions.
4) Start the gateway. 4) Start the gateway.
5) DM access is pairing by default; approve the pairing code on first contact. 5) DM access is pairing by default; approve the pairing code on first contact.
@ -20,9 +20,11 @@ Status: ready for DM and guild text channels via the official Discord bot gatewa
Minimal config: Minimal config:
```json5 ```json5
{ {
discord: { channels: {
enabled: true, discord: {
token: "YOUR_BOT_TOKEN" enabled: true,
token: "YOUR_BOT_TOKEN"
}
} }
} }
``` ```
@ -30,29 +32,29 @@ Minimal config:
## Goals ## Goals
- Talk to Clawdbot via Discord DMs or guild channels. - Talk to Clawdbot via Discord DMs or guild channels.
- Direct chats collapse into the agent's main session (default `agent:main:main`); guild channels stay isolated as `agent:<agentId>:discord:channel:<channelId>` (display names use `discord:<guildSlug>#<channelSlug>`). - Direct chats collapse into the agent's main session (default `agent:main:main`); guild channels stay isolated as `agent:<agentId>:discord:channel:<channelId>` (display names use `discord:<guildSlug>#<channelSlug>`).
- Group DMs are ignored by default; enable via `discord.dm.groupEnabled` and optionally restrict by `discord.dm.groupChannels`. - Group DMs are ignored by default; enable via `channels.discord.dm.groupEnabled` and optionally restrict by `channels.discord.dm.groupChannels`.
- Keep routing deterministic: replies always go back to the provider they arrived on. - Keep routing deterministic: replies always go back to the channel they arrived on.
## How it works ## How it works
1. Create a Discord application → Bot, enable the intents you need (DMs + guild messages + message content), and grab the bot token. 1. Create a Discord application → Bot, enable the intents you need (DMs + guild messages + message content), and grab the bot token.
2. Invite the bot to your server with the permissions required to read/send messages where you want to use it. 2. Invite the bot to your server with the permissions required to read/send messages where you want to use it.
3. Configure Clawdbot with `DISCORD_BOT_TOKEN` (or `discord.token` in `~/.clawdbot/clawdbot.json`). 3. Configure Clawdbot with `DISCORD_BOT_TOKEN` (or `channels.discord.token` in `~/.clawdbot/clawdbot.json`).
4. Run the gateway; it auto-starts the Discord provider when a token is available (env or config) and `discord.enabled` is not `false`. 4. Run the gateway; it auto-starts the Discord channel when a token is available (env or config) and `channels.discord.enabled` is not `false`.
- If you prefer env vars, set `DISCORD_BOT_TOKEN` (a config block is optional). - If you prefer env vars, set `DISCORD_BOT_TOKEN` (a config block is optional).
5. Direct chats: use `user:<id>` (or a `<@id>` mention) when delivering; all turns land in the shared `main` session. Bare numeric IDs are ambiguous and rejected. 5. Direct chats: use `user:<id>` (or a `<@id>` mention) when delivering; all turns land in the shared `main` session. Bare numeric IDs are ambiguous and rejected.
6. Guild channels: use `channel:<channelId>` for delivery. Mentions are required by default and can be set per guild or per channel. 6. Guild channels: use `channel:<channelId>` for delivery. Mentions are required by default and can be set per guild or per channel.
7. Direct chats: secure by default via `discord.dm.policy` (default: `"pairing"`). Unknown senders get a pairing code (expires after 1 hour); approve via `clawdbot pairing approve discord <code>`. 7. Direct chats: secure by default via `channels.discord.dm.policy` (default: `"pairing"`). Unknown senders get a pairing code (expires after 1 hour); approve via `clawdbot pairing approve discord <code>`.
- To keep old “open to anyone” behavior: set `discord.dm.policy="open"` and `discord.dm.allowFrom=["*"]`. - To keep old “open to anyone” behavior: set `channels.discord.dm.policy="open"` and `channels.discord.dm.allowFrom=["*"]`.
- To hard-allowlist: set `discord.dm.policy="allowlist"` and list senders in `discord.dm.allowFrom`. - To hard-allowlist: set `channels.discord.dm.policy="allowlist"` and list senders in `channels.discord.dm.allowFrom`.
- To ignore all DMs: set `discord.dm.enabled=false` or `discord.dm.policy="disabled"`. - To ignore all DMs: set `channels.discord.dm.enabled=false` or `channels.discord.dm.policy="disabled"`.
8. Group DMs are ignored by default; enable via `discord.dm.groupEnabled` and optionally restrict by `discord.dm.groupChannels`. 8. Group DMs are ignored by default; enable via `channels.discord.dm.groupEnabled` and optionally restrict by `channels.discord.dm.groupChannels`.
9. Optional guild rules: set `discord.guilds` keyed by guild id (preferred) or slug, with per-channel rules. 9. Optional guild rules: set `channels.discord.guilds` keyed by guild id (preferred) or slug, with per-channel rules.
10. Optional native commands: `commands.native` defaults to `"auto"` (on for Discord/Telegram, off for Slack). Override with `discord.commands.native: true|false|"auto"`; `false` clears previously registered commands. Text commands are controlled by `commands.text` and must be sent as standalone `/...` messages. Use `commands.useAccessGroups: false` to bypass access-group checks for commands. 10. Optional native commands: `commands.native` defaults to `"auto"` (on for Discord/Telegram, off for Slack). Override with `channels.discord.commands.native: true|false|"auto"`; `false` clears previously registered commands. Text commands are controlled by `commands.text` and must be sent as standalone `/...` messages. Use `commands.useAccessGroups: false` to bypass access-group checks for commands.
- Full command list + config: [Slash commands](/tools/slash-commands) - Full command list + config: [Slash commands](/tools/slash-commands)
11. Optional guild context history: set `discord.historyLimit` (default 20, falls back to `messages.groupChat.historyLimit`) to include the last N guild messages as context when replying to a mention. Set `0` to disable. 11. Optional guild context history: set `channels.discord.historyLimit` (default 20, falls back to `messages.groupChat.historyLimit`) to include the last N guild messages as context when replying to a mention. Set `0` to disable.
12. Reactions: the agent can trigger reactions via the `discord` tool (gated by `discord.actions.*`). 12. Reactions: the agent can trigger reactions via the `discord` tool (gated by `channels.discord.actions.*`).
- Reaction removal semantics: see [/tools/reactions](/tools/reactions). - Reaction removal semantics: see [/tools/reactions](/tools/reactions).
- The `discord` tool is only exposed when the current provider is Discord. - The `discord` tool is only exposed when the current channel is Discord.
13. Native commands use isolated session keys (`agent:<agentId>:discord:slash:<userId>`) rather than the shared `main` session. 13. Native commands use isolated session keys (`agent:<agentId>:discord:slash:<userId>`) rather than the shared `main` session.
Note: Discord does not provide a simple username → id lookup without extra guild context, so prefer ids or `<@id>` mentions for DM delivery targets. Note: Discord does not provide a simple username → id lookup without extra guild context, so prefer ids or `<@id>` mentions for DM delivery targets.
@ -117,37 +119,41 @@ Or via config:
```json5 ```json5
{ {
discord: { channels: {
enabled: true, discord: {
token: "YOUR_BOT_TOKEN" enabled: true,
token: "YOUR_BOT_TOKEN"
}
} }
} }
``` ```
Multi-account support: use `discord.accounts` with per-account tokens and optional `name`. See [`gateway/configuration`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern. Multi-account support: use `channels.discord.accounts` with per-account tokens and optional `name`. See [`gateway/configuration`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern.
#### Allowlist + channel routing #### Allowlist + channel routing
Example “single server, only allow me, only allow #help”: Example “single server, only allow me, only allow #help”:
```json5 ```json5
{ {
discord: { channels: {
enabled: true, discord: {
dm: { enabled: false }, enabled: true,
guilds: { dm: { enabled: false },
"YOUR_GUILD_ID": { guilds: {
users: ["YOUR_USER_ID"], "YOUR_GUILD_ID": {
requireMention: true, users: ["YOUR_USER_ID"],
channels: { requireMention: true,
help: { allow: true, requireMention: true } channels: {
help: { allow: true, requireMention: true }
}
} }
},
retry: {
attempts: 3,
minDelayMs: 500,
maxDelayMs: 30000,
jitter: 0.1
} }
},
retry: {
attempts: 3,
minDelayMs: 500,
maxDelayMs: 30000,
jitter: 0.1
} }
} }
} }
@ -158,8 +164,8 @@ Notes:
- `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) also count as mentions for guild messages. - `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) also count as mentions for guild messages.
- Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`. - Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`.
- If `channels` is present, any channel not listed is denied by default. - If `channels` is present, any channel not listed is denied by default.
- Bot-authored messages are ignored by default; set `discord.allowBots=true` to allow them (own messages remain filtered). - Bot-authored messages are ignored by default; set `channels.discord.allowBots=true` to allow them (own messages remain filtered).
- Warning: If you allow replies to other bots (`discord.allowBots=true`), prevent bot-to-bot reply loops with `requireMention`, `discord.guilds.*.channels.<id>.users` allowlists, and/or clear guardrails in `AGENTS.md` and `SOUL.md`. - Warning: If you allow replies to other bots (`channels.discord.allowBots=true`), prevent bot-to-bot reply loops with `requireMention`, `channels.discord.guilds.*.channels.<id>.users` allowlists, and/or clear guardrails in `AGENTS.md` and `SOUL.md`.
### 6) Verify it works ### 6) Verify it works
1. Start the gateway. 1. Start the gateway.
@ -167,7 +173,7 @@ Notes:
3. If nothing happens: check **Troubleshooting** below. 3. If nothing happens: check **Troubleshooting** below.
### Troubleshooting ### Troubleshooting
- First: run `clawdbot doctor` and `clawdbot providers status --probe` (actionable warnings + quick audits). - First: run `clawdbot doctor` and `clawdbot channels status --probe` (actionable warnings + quick audits).
- **“Used disallowed intents”**: enable **Message Content Intent** (and likely **Server Members Intent**) in the Developer Portal, then restart the gateway. - **“Used disallowed intents”**: enable **Message Content Intent** (and likely **Server Members Intent**) in the Developer Portal, then restart the gateway.
- **Bot connects but never replies in a guild channel**: - **Bot connects but never replies in a guild channel**:
- Missing **Message Content Intent**, or - Missing **Message Content Intent**, or
@ -175,78 +181,80 @@ Notes:
- Your config requires mentions and you didnt mention it, or - Your config requires mentions and you didnt mention it, or
- Your guild/channel allowlist denies the channel/user. - Your guild/channel allowlist denies the channel/user.
- **`requireMention: false` but still no replies**: - **`requireMention: false` but still no replies**:
- `discord.groupPolicy` defaults to **allowlist**; you must either set it to `"open"` or explicitly list the channel under `discord.guilds.<id>.channels`. - `channels.discord.groupPolicy` defaults to **allowlist**; set it to `"open"` or explicitly list channels under `channels.discord.guilds.<id>.channels`.
- `requireMention` must live under `discord.guilds` (or a specific channel). `discord.requireMention` at the top level is ignored. - `requireMention` must live under `channels.discord.guilds` (or a specific channel). `channels.discord.requireMention` at the top level is ignored.
- **Permission audits** (`providers status --probe`) only check numeric channel IDs. If you use slugs/names as `discord.guilds.*.channels` keys, the audit cant verify permissions. - **Permission audits** (`channels status --probe`) only check numeric channel IDs. If you use slugs/names as `channels.discord.guilds.*.channels` keys, the audit cant verify permissions.
- **DMs dont work**: `discord.dm.enabled=false`, `discord.dm.policy="disabled"`, or you havent been approved yet (`discord.dm.policy="pairing"`). - **DMs dont work**: `channels.discord.dm.enabled=false`, `channels.discord.dm.policy="disabled"`, or you havent been approved yet (`channels.discord.dm.policy="pairing"`).
## Capabilities & limits ## Capabilities & limits
- DMs and guild text channels (threads are treated as separate channels; voice not supported). - DMs and guild text channels (threads are treated as separate channels; voice not supported).
- Typing indicators sent best-effort; message chunking uses `discord.textChunkLimit` (default 2000) and splits tall replies by line count (`discord.maxLinesPerMessage`, default 17). - Typing indicators sent best-effort; message chunking uses `channels.discord.textChunkLimit` (default 2000) and splits tall replies by line count (`channels.discord.maxLinesPerMessage`, default 17).
- File uploads supported up to the configured `discord.mediaMaxMb` (default 8 MB). - File uploads supported up to the configured `channels.discord.mediaMaxMb` (default 8 MB).
- Mention-gated guild replies by default to avoid noisy bots. - Mention-gated guild replies by default to avoid noisy bots.
- Reply context is injected when a message references another message (quoted content + ids). - Reply context is injected when a message references another message (quoted content + ids).
- Native reply threading is **off by default**; enable with `discord.replyToMode` and reply tags. - Native reply threading is **off by default**; enable with `channels.discord.replyToMode` and reply tags.
## Retry policy ## Retry policy
Outbound Discord API calls retry on rate limits (429) using Discord `retry_after` when available, with exponential backoff and jitter. Configure via `discord.retry`. See [Retry policy](/concepts/retry). Outbound Discord API calls retry on rate limits (429) using Discord `retry_after` when available, with exponential backoff and jitter. Configure via `channels.discord.retry`. See [Retry policy](/concepts/retry).
## Config ## Config
```json5 ```json5
{ {
discord: { channels: {
enabled: true, discord: {
token: "abc.123",
groupPolicy: "allowlist",
guilds: {
"*": {
channels: {
general: { allow: true }
}
}
},
mediaMaxMb: 8,
actions: {
reactions: true,
stickers: true,
polls: true,
permissions: true,
messages: true,
threads: true,
pins: true,
search: true,
memberInfo: true,
roleInfo: true,
roles: false,
channelInfo: true,
voiceStatus: true,
events: true,
moderation: false
},
replyToMode: "off",
dm: {
enabled: true, enabled: true,
policy: "pairing", // pairing | allowlist | open | disabled token: "abc.123",
allowFrom: ["123456789012345678", "steipete"], groupPolicy: "allowlist",
groupEnabled: false, guilds: {
groupChannels: ["clawd-dm"] "*": {
}, channels: {
guilds: { general: { allow: true }
"*": { requireMention: true }, }
"123456789012345678": { }
slug: "friends-of-clawd", },
requireMention: false, mediaMaxMb: 8,
reactionNotifications: "own", actions: {
users: ["987654321098765432", "steipete"], reactions: true,
channels: { stickers: true,
general: { allow: true }, polls: true,
help: { permissions: true,
allow: true, messages: true,
requireMention: true, threads: true,
users: ["987654321098765432"], pins: true,
skills: ["search", "docs"], search: true,
systemPrompt: "Keep answers short." memberInfo: true,
roleInfo: true,
roles: false,
channelInfo: true,
voiceStatus: true,
events: true,
moderation: false
},
replyToMode: "off",
dm: {
enabled: true,
policy: "pairing", // pairing | allowlist | open | disabled
allowFrom: ["123456789012345678", "steipete"],
groupEnabled: false,
groupChannels: ["clawd-dm"]
},
guilds: {
"*": { requireMention: true },
"123456789012345678": {
slug: "friends-of-clawd",
requireMention: false,
reactionNotifications: "own",
users: ["987654321098765432", "steipete"],
channels: {
general: { allow: true },
help: {
allow: true,
requireMention: true,
users: ["987654321098765432"],
skills: ["search", "docs"],
systemPrompt: "Keep answers short."
}
} }
} }
} }
@ -323,7 +331,7 @@ To request a threaded reply, the model can include one tag in its output:
- `[[reply_to:<id>]]` — reply to a specific message id from context/history. - `[[reply_to:<id>]]` — reply to a specific message id from context/history.
Current message ids are appended to prompts as `[message_id: …]`; history entries already include ids. Current message ids are appended to prompts as `[message_id: …]`; history entries already include ids.
Behavior is controlled by `discord.replyToMode`: Behavior is controlled by `channels.discord.replyToMode`:
- `off`: ignore tags. - `off`: ignore tags.
- `first`: only the first outbound chunk/attachment is a reply. - `first`: only the first outbound chunk/attachment is a reply.
- `all`: every outbound chunk/attachment is a reply. - `all`: every outbound chunk/attachment is a reply.
@ -336,7 +344,7 @@ Allowlist matching notes:
Native command notes: Native command notes:
- The registered commands mirror Clawdbots chat commands. - The registered commands mirror Clawdbots chat commands.
- Native commands honor the same allowlists as DMs/guild messages (`discord.dm.allowFrom`, `discord.guilds`, per-channel rules). - Native commands honor the same allowlists as DMs/guild messages (`channels.discord.dm.allowFrom`, `channels.discord.guilds`, per-channel rules).
## Tool actions ## Tool actions
The agent can call `discord` with actions like: The agent can call `discord` with actions like:

View File

@ -14,11 +14,11 @@ read_when:
# What we shipped # What we shipped
- **Single client path:** fetch-based implementation removed; grammY is now the sole Telegram client (send + gateway) with the grammY throttler enabled by default. - **Single client path:** fetch-based implementation removed; grammY is now the sole Telegram client (send + gateway) with the grammY throttler enabled by default.
- **Gateway:** `monitorTelegramProvider` builds a grammY `Bot`, wires mention/allowlist gating, media download via `getFile`/`download`, and delivers replies with `sendMessage/sendPhoto/sendVideo/sendAudio/sendDocument`. Supports long-poll or webhook via `webhookCallback`. - **Gateway:** `monitorTelegramProvider` builds a grammY `Bot`, wires mention/allowlist gating, media download via `getFile`/`download`, and delivers replies with `sendMessage/sendPhoto/sendVideo/sendAudio/sendDocument`. Supports long-poll or webhook via `webhookCallback`.
- **Proxy:** optional `telegram.proxy` uses `undici.ProxyAgent` through grammYs `client.baseFetch`. - **Proxy:** optional `channels.telegram.proxy` uses `undici.ProxyAgent` through grammYs `client.baseFetch`.
- **Webhook support:** `webhook-set.ts` wraps `setWebhook/deleteWebhook`; `webhook.ts` hosts the callback with health + graceful shutdown. Gateway enables webhook mode when `telegram.webhookUrl` is set (otherwise it long-polls). - **Webhook support:** `webhook-set.ts` wraps `setWebhook/deleteWebhook`; `webhook.ts` hosts the callback with health + graceful shutdown. Gateway enables webhook mode when `channels.telegram.webhookUrl` is set (otherwise it long-polls).
- **Sessions:** direct chats collapse into the agent main session (`agent:<agentId>:<mainKey>`); groups use `agent:<agentId>:telegram:group:<chatId>`; replies route back to the same provider. - **Sessions:** direct chats collapse into the agent main session (`agent:<agentId>:<mainKey>`); groups use `agent:<agentId>:telegram:group:<chatId>`; replies route back to the same channel.
- **Config knobs:** `telegram.botToken`, `telegram.dmPolicy`, `telegram.groups` (allowlist + mention defaults), `telegram.allowFrom`, `telegram.groupAllowFrom`, `telegram.groupPolicy`, `telegram.mediaMaxMb`, `telegram.proxy`, `telegram.webhookSecret`, `telegram.webhookUrl`. - **Config knobs:** `channels.telegram.botToken`, `channels.telegram.dmPolicy`, `channels.telegram.groups` (allowlist + mention defaults), `channels.telegram.allowFrom`, `channels.telegram.groupAllowFrom`, `channels.telegram.groupPolicy`, `channels.telegram.mediaMaxMb`, `channels.telegram.proxy`, `channels.telegram.webhookSecret`, `channels.telegram.webhookUrl`.
- **Draft streaming:** optional `telegram.streamMode` uses `sendMessageDraft` in private topic chats (Bot API 9.3+). This is separate from provider block streaming. - **Draft streaming:** optional `channels.telegram.streamMode` uses `sendMessageDraft` in private topic chats (Bot API 9.3+). This is separate from channel block streaming.
- **Tests:** grammy mocks cover DM + group mention gating and outbound send; more media/webhook fixtures still welcome. - **Tests:** grammy mocks cover DM + group mention gating and outbound send; more media/webhook fixtures still welcome.
Open questions Open questions

View File

@ -13,31 +13,33 @@ Status: external CLI integration. Gateway spawns `imsg rpc` (JSON-RPC over stdio
1) Ensure Messages is signed in on this Mac. 1) Ensure Messages is signed in on this Mac.
2) Install `imsg`: 2) Install `imsg`:
- `brew install steipete/tap/imsg` - `brew install steipete/tap/imsg`
3) Configure Clawdbot with `imessage.cliPath` and `imessage.dbPath`. 3) Configure Clawdbot with `channels.imessage.cliPath` and `channels.imessage.dbPath`.
4) Start the gateway and approve any macOS prompts (Automation + Full Disk Access). 4) Start the gateway and approve any macOS prompts (Automation + Full Disk Access).
Minimal config: Minimal config:
```json5 ```json5
{ {
imessage: { channels: {
enabled: true, imessage: {
cliPath: "/usr/local/bin/imsg", enabled: true,
dbPath: "/Users/<you>/Library/Messages/chat.db" cliPath: "/usr/local/bin/imsg",
dbPath: "/Users/<you>/Library/Messages/chat.db"
}
} }
} }
``` ```
## What it is ## What it is
- iMessage provider backed by `imsg` on macOS. - iMessage channel backed by `imsg` on macOS.
- Deterministic routing: replies always go back to iMessage. - Deterministic routing: replies always go back to iMessage.
- DMs share the agent's main session; groups are isolated (`agent:<agentId>:imessage:group:<chat_id>`). - DMs share the agent's main session; groups are isolated (`agent:<agentId>:imessage:group:<chat_id>`).
- If a multi-participant thread arrives with `is_group=false`, you can still isolate it by `chat_id` using `imessage.groups` (see “Group-ish threads” below). - If a multi-participant thread arrives with `is_group=false`, you can still isolate it by `chat_id` using `channels.imessage.groups` (see “Group-ish threads” below).
## Requirements ## Requirements
- macOS with Messages signed in. - macOS with Messages signed in.
- Full Disk Access for Clawdbot + `imsg` (Messages DB access). - Full Disk Access for Clawdbot + `imsg` (Messages DB access).
- Automation permission when sending. - Automation permission when sending.
- `imessage.cliPath` can point to any command that proxies stdin/stdout (for example, a wrapper script that SSHes to another Mac and runs `imsg rpc`). - `channels.imessage.cliPath` can point to any command that proxies stdin/stdout (for example, a wrapper script that SSHes to another Mac and runs `imsg rpc`).
## Setup (fast path) ## Setup (fast path)
1) Ensure Messages is signed in on this Mac. 1) Ensure Messages is signed in on this Mac.
@ -54,7 +56,7 @@ If you want the bot to send from a **separate iMessage identity** (and keep your
5) Install `imsg`: 5) Install `imsg`:
- `brew install steipete/tap/imsg` - `brew install steipete/tap/imsg`
6) Set up SSH so `ssh <bot-macos-user>@localhost true` works without a password. 6) Set up SSH so `ssh <bot-macos-user>@localhost true` works without a password.
7) Point `imessage.accounts.bot.cliPath` at an SSH wrapper that runs `imsg` as the bot user. 7) Point `channels.imessage.accounts.bot.cliPath` at an SSH wrapper that runs `imsg` as the bot user.
First-run note: sending/receiving may require GUI approvals (Automation + Full Disk Access) in the *bot macOS user*. If `imsg rpc` looks stuck or exits, log into that user (Screen Sharing helps), run a one-time `imsg chats --limit 1` / `imsg send ...`, approve prompts, then retry. First-run note: sending/receiving may require GUI approvals (Automation + Full Disk Access) in the *bot macOS user*. If `imsg rpc` looks stuck or exits, log into that user (Screen Sharing helps), run a one-time `imsg chats --limit 1` / `imsg send ...`, approve prompts, then retry.
@ -72,24 +74,26 @@ exec /usr/bin/ssh -o BatchMode=yes -o ConnectTimeout=5 -T <bot-macos-user>@local
Example config: Example config:
```json5 ```json5
{ {
imessage: { channels: {
enabled: true, imessage: {
accounts: { enabled: true,
bot: { accounts: {
name: "Bot", bot: {
enabled: true, name: "Bot",
cliPath: "/path/to/imsg-bot", enabled: true,
dbPath: "/Users/<bot-macos-user>/Library/Messages/chat.db" cliPath: "/path/to/imsg-bot",
dbPath: "/Users/<bot-macos-user>/Library/Messages/chat.db"
}
} }
} }
} }
} }
``` ```
For single-account setups, use flat options (`imessage.cliPath`, `imessage.dbPath`) instead of the `accounts` map. For single-account setups, use flat options (`channels.imessage.cliPath`, `channels.imessage.dbPath`) instead of the `accounts` map.
### Remote/SSH variant (optional) ### Remote/SSH variant (optional)
If you want iMessage on another Mac, set `imessage.cliPath` to a wrapper that runs `imsg` on the remote macOS host over SSH. Clawdbot only needs stdio. If you want iMessage on another Mac, set `channels.imessage.cliPath` to a wrapper that runs `imsg` on the remote macOS host over SSH. Clawdbot only needs stdio.
Example wrapper: Example wrapper:
```bash ```bash
@ -97,11 +101,11 @@ Example wrapper:
exec ssh -T mac-mini imsg "$@" exec ssh -T mac-mini imsg "$@"
``` ```
Multi-account support: use `imessage.accounts` with per-account config and optional `name`. See [`gateway/configuration`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern. Dont commit `~/.clawdbot/clawdbot.json` (it often contains tokens). Multi-account support: use `channels.imessage.accounts` with per-account config and optional `name`. See [`gateway/configuration`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern. Dont commit `~/.clawdbot/clawdbot.json` (it often contains tokens).
## Access control (DMs + groups) ## Access control (DMs + groups)
DMs: DMs:
- Default: `imessage.dmPolicy = "pairing"`. - Default: `channels.imessage.dmPolicy = "pairing"`.
- Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour). - Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour).
- Approve via: - Approve via:
- `clawdbot pairing list imessage` - `clawdbot pairing list imessage`
@ -109,30 +113,32 @@ DMs:
- Pairing is the default token exchange for iMessage DMs. Details: [Pairing](/start/pairing) - Pairing is the default token exchange for iMessage DMs. Details: [Pairing](/start/pairing)
Groups: Groups:
- `imessage.groupPolicy = open | allowlist | disabled`. - `channels.imessage.groupPolicy = open | allowlist | disabled`.
- `imessage.groupAllowFrom` controls who can trigger in groups when `allowlist` is set. - `channels.imessage.groupAllowFrom` controls who can trigger in groups when `allowlist` is set.
- Mention gating uses `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) because iMessage has no native mention metadata. - Mention gating uses `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) because iMessage has no native mention metadata.
- Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`. - Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`.
## How it works (behavior) ## How it works (behavior)
- `imsg` streams message events; the gateway normalizes them into the shared provider envelope. - `imsg` streams message events; the gateway normalizes them into the shared channel envelope.
- Replies always route back to the same chat id or handle. - Replies always route back to the same chat id or handle.
## Group-ish threads (`is_group=false`) ## Group-ish threads (`is_group=false`)
Some iMessage threads can have multiple participants but still arrive with `is_group=false` depending on how Messages stores the chat identifier. Some iMessage threads can have multiple participants but still arrive with `is_group=false` depending on how Messages stores the chat identifier.
If you explicitly configure a `chat_id` under `imessage.groups`, Clawdbot treats that thread as a “group” for: If you explicitly configure a `chat_id` under `channels.imessage.groups`, Clawdbot treats that thread as a “group” for:
- session isolation (separate `agent:<agentId>:imessage:group:<chat_id>` session key) - session isolation (separate `agent:<agentId>:imessage:group:<chat_id>` session key)
- group allowlisting / mention gating behavior - group allowlisting / mention gating behavior
Example: Example:
```json5 ```json5
{ {
imessage: { channels: {
groupPolicy: "allowlist", imessage: {
groupAllowFrom: ["+15555550123"], groupPolicy: "allowlist",
groups: { groupAllowFrom: ["+15555550123"],
"42": { "requireMention": false } groups: {
"42": { "requireMention": false }
}
} }
} }
} }
@ -140,12 +146,12 @@ Example:
This is useful when you want an isolated personality/model for a specific thread (see [Multi-agent routing](/concepts/multi-agent)). For filesystem isolation, see [Sandboxing](/gateway/sandboxing). This is useful when you want an isolated personality/model for a specific thread (see [Multi-agent routing](/concepts/multi-agent)). For filesystem isolation, see [Sandboxing](/gateway/sandboxing).
## Media + limits ## Media + limits
- Optional attachment ingestion via `imessage.includeAttachments`. - Optional attachment ingestion via `channels.imessage.includeAttachments`.
- Media cap via `imessage.mediaMaxMb`. - Media cap via `channels.imessage.mediaMaxMb`.
## Limits ## Limits
- Outbound text is chunked to `imessage.textChunkLimit` (default 4000). - Outbound text is chunked to `channels.imessage.textChunkLimit` (default 4000).
- Media uploads are capped by `imessage.mediaMaxMb` (default 16). - Media uploads are capped by `channels.imessage.mediaMaxMb` (default 16).
## Addressing / delivery targets ## Addressing / delivery targets
Prefer `chat_id` for stable routing: Prefer `chat_id` for stable routing:
@ -163,20 +169,20 @@ imsg chats --limit 20
Full configuration: [Configuration](/gateway/configuration) Full configuration: [Configuration](/gateway/configuration)
Provider options: Provider options:
- `imessage.enabled`: enable/disable provider startup. - `channels.imessage.enabled`: enable/disable channel startup.
- `imessage.cliPath`: path to `imsg`. - `channels.imessage.cliPath`: path to `imsg`.
- `imessage.dbPath`: Messages DB path. - `channels.imessage.dbPath`: Messages DB path.
- `imessage.service`: `imessage | sms | auto`. - `channels.imessage.service`: `imessage | sms | auto`.
- `imessage.region`: SMS region. - `channels.imessage.region`: SMS region.
- `imessage.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing). - `channels.imessage.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing).
- `imessage.allowFrom`: DM allowlist (handles or `chat_id:*`). `open` requires `"*"`. - `channels.imessage.allowFrom`: DM allowlist (handles or `chat_id:*`). `open` requires `"*"`.
- `imessage.groupPolicy`: `open | allowlist | disabled` (default: allowlist). - `channels.imessage.groupPolicy`: `open | allowlist | disabled` (default: allowlist).
- `imessage.groupAllowFrom`: group sender allowlist. - `channels.imessage.groupAllowFrom`: group sender allowlist.
- `imessage.historyLimit` / `imessage.accounts.*.historyLimit`: max group messages to include as context (0 disables). - `channels.imessage.historyLimit` / `channels.imessage.accounts.*.historyLimit`: max group messages to include as context (0 disables).
- `imessage.groups`: per-group defaults + allowlist (use `"*"` for global defaults). - `channels.imessage.groups`: per-group defaults + allowlist (use `"*"` for global defaults).
- `imessage.includeAttachments`: ingest attachments into context. - `channels.imessage.includeAttachments`: ingest attachments into context.
- `imessage.mediaMaxMb`: inbound/outbound media cap (MB). - `channels.imessage.mediaMaxMb`: inbound/outbound media cap (MB).
- `imessage.textChunkLimit`: outbound chunk size (chars). - `channels.imessage.textChunkLimit`: outbound chunk size (chars).
Related global options: Related global options:
- `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`). - `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`).

30
docs/channels/index.md Normal file
View File

@ -0,0 +1,30 @@
---
summary: "Messaging platforms Clawdbot can connect to"
read_when:
- You want to choose a chat channel for Clawdbot
- You need a quick overview of supported messaging platforms
---
# Chat Channels
Clawdbot can talk to you on any chat app you already use. Each channel connects via the Gateway.
Text is supported everywhere; media and reactions vary by channel.
## Supported channels
- [WhatsApp](/channels/whatsapp) — Most popular; uses Baileys and requires QR pairing.
- [Telegram](/channels/telegram) — Bot API via grammY; supports groups.
- [Discord](/channels/discord) — Discord Bot API + Gateway; supports servers, channels, and DMs.
- [Slack](/channels/slack) — Bolt SDK; workspace apps.
- [Signal](/channels/signal) — signal-cli; privacy-focused.
- [iMessage](/channels/imessage) — macOS only; native integration.
- [Microsoft Teams](/channels/msteams) — Bot Framework; enterprise support.
- [WebChat](/web/webchat) — Gateway WebChat UI over WebSocket.
## Notes
- Channels can run simultaneously; configure multiple and Clawdbot will route per chat.
- Group behavior varies by channel; see [Groups](/concepts/groups).
- DM pairing and allowlists are enforced for safety; see [Security](/gateway/security).
- Telegram internals: [grammY notes](/channels/grammy).
- Troubleshooting: [Channel troubleshooting](/channels/troubleshooting).
- Model providers are documented separately; see [Model Providers](/providers/models).

View File

@ -1,13 +1,13 @@
--- ---
summary: "Inbound provider location parsing (Telegram + WhatsApp) and context fields" summary: "Inbound channel location parsing (Telegram + WhatsApp) and context fields"
read_when: read_when:
- Adding or modifying provider location parsing - Adding or modifying channel location parsing
- Using location context fields in agent prompts or tools - Using location context fields in agent prompts or tools
--- ---
# Provider location parsing # Channel location parsing
Clawdbot normalizes shared locations from chat providers into: Clawdbot normalizes shared locations from chat channels into:
- human-readable text appended to the inbound body, and - human-readable text appended to the inbound body, and
- structured fields in the auto-reply context payload. - structured fields in the auto-reply context payload.
@ -25,7 +25,7 @@ Locations are rendered as friendly lines without brackets:
- Live share: - Live share:
- `🛰 Live location: 48.858844, 2.294351 ±12m` - `🛰 Live location: 48.858844, 2.294351 ±12m`
If the provider includes a caption/comment, it is appended on the next line: If the channel includes a caption/comment, it is appended on the next line:
``` ```
📍 48.858844, 2.294351 ±12m 📍 48.858844, 2.294351 ±12m
Meet here Meet here
@ -41,6 +41,6 @@ When a location is present, these fields are added to `ctx`:
- `LocationSource` (`pin | place | live`) - `LocationSource` (`pin | place | live`)
- `LocationIsLive` (boolean) - `LocationIsLive` (boolean)
## Provider notes ## Channel notes
- **Telegram**: venues map to `LocationName/LocationAddress`; live locations use `live_period`. - **Telegram**: venues map to `LocationName/LocationAddress`; live locations use `live_period`.
- **WhatsApp**: `locationMessage.comment` and `liveLocationMessage.caption` are appended as the caption line. - **WhatsApp**: `locationMessage.comment` and `liveLocationMessage.caption` are appended as the caption line.

View File

@ -1,7 +1,7 @@
--- ---
summary: "Microsoft Teams bot support status, capabilities, and configuration" summary: "Microsoft Teams bot support status, capabilities, and configuration"
read_when: read_when:
- Working on MS Teams provider features - Working on MS Teams channel features
--- ---
# Microsoft Teams (Bot Framework) # Microsoft Teams (Bot Framework)
@ -21,39 +21,43 @@ Status: text + DM attachments are supported; channel/group attachments require M
Minimal config: Minimal config:
```json5 ```json5
{ {
msteams: { channels: {
enabled: true, msteams: {
appId: "<APP_ID>", enabled: true,
appPassword: "<APP_PASSWORD>", appId: "<APP_ID>",
tenantId: "<TENANT_ID>", appPassword: "<APP_PASSWORD>",
webhook: { port: 3978, path: "/api/messages" } tenantId: "<TENANT_ID>",
webhook: { port: 3978, path: "/api/messages" }
}
} }
} }
``` ```
Note: group chats are blocked by default (`msteams.groupPolicy: "allowlist"`). To allow group replies, set `msteams.groupAllowFrom` (or use `groupPolicy: "open"` to allow any member, mention-gated). Note: group chats are blocked by default (`channels.msteams.groupPolicy: "allowlist"`). To allow group replies, set `channels.msteams.groupAllowFrom` (or use `groupPolicy: "open"` to allow any member, mention-gated).
## Goals ## Goals
- Talk to Clawdbot via Teams DMs, group chats, or channels. - Talk to Clawdbot via Teams DMs, group chats, or channels.
- Keep routing deterministic: replies always go back to the provider they arrived on. - Keep routing deterministic: replies always go back to the channel they arrived on.
- Default to safe channel behavior (mentions required unless configured otherwise). - Default to safe channel behavior (mentions required unless configured otherwise).
## Access control (DMs + groups) ## Access control (DMs + groups)
**DM access** **DM access**
- Default: `msteams.dmPolicy = "pairing"`. Unknown senders are ignored until approved. - Default: `channels.msteams.dmPolicy = "pairing"`. Unknown senders are ignored until approved.
- `msteams.allowFrom` accepts AAD object IDs or UPNs. - `channels.msteams.allowFrom` accepts AAD object IDs or UPNs.
**Group access** **Group access**
- Default: `msteams.groupPolicy = "allowlist"` (blocked unless you add `groupAllowFrom`). - Default: `channels.msteams.groupPolicy = "allowlist"` (blocked unless you add `groupAllowFrom`).
- `msteams.groupAllowFrom` controls which senders can trigger in group chats/channels (falls back to `msteams.allowFrom`). - `channels.msteams.groupAllowFrom` controls which senders can trigger in group chats/channels (falls back to `channels.msteams.allowFrom`).
- Set `groupPolicy: "open"` to allow any member (still mentiongated by default). - Set `groupPolicy: "open"` to allow any member (still mentiongated by default).
Example: Example:
```json5 ```json5
{ {
msteams: { channels: {
groupPolicy: "allowlist", msteams: {
groupAllowFrom: ["user@org.com"] groupPolicy: "allowlist",
groupAllowFrom: ["user@org.com"]
}
} }
} }
``` ```
@ -189,10 +193,10 @@ This is often easier than hand-editing JSON manifests.
- `https://<host>:3978/api/messages` (or your chosen path/port). - `https://<host>:3978/api/messages` (or your chosen path/port).
5. **Run the gateway** 5. **Run the gateway**
- The Teams provider starts automatically when `msteams` config exists and credentials are set. - The Teams channel starts automatically when `msteams` config exists and credentials are set.
## History context ## History context
- `msteams.historyLimit` controls how many recent channel/group messages are wrapped into the prompt. - `channels.msteams.historyLimit` controls how many recent channel/group messages are wrapped into the prompt.
- Falls back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50). - Falls back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50).
## Current Teams RSC Permissions (Manifest) ## Current Teams RSC Permissions (Manifest)
@ -336,22 +340,22 @@ Teams markdown is more limited than Slack or Discord:
- Adaptive Cards are used for polls; other card types are not yet supported - Adaptive Cards are used for polls; other card types are not yet supported
## Configuration ## Configuration
Key settings (see `/gateway/configuration` for shared provider patterns): Key settings (see `/gateway/configuration` for shared channel patterns):
- `msteams.enabled`: enable/disable the provider. - `channels.msteams.enabled`: enable/disable the channel.
- `msteams.appId`, `msteams.appPassword`, `msteams.tenantId`: bot credentials. - `channels.msteams.appId`, `channels.msteams.appPassword`, `channels.msteams.tenantId`: bot credentials.
- `msteams.webhook.port` (default `3978`) - `channels.msteams.webhook.port` (default `3978`)
- `msteams.webhook.path` (default `/api/messages`) - `channels.msteams.webhook.path` (default `/api/messages`)
- `msteams.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing) - `channels.msteams.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing)
- `msteams.allowFrom`: allowlist for DMs (AAD object IDs or UPNs). - `channels.msteams.allowFrom`: allowlist for DMs (AAD object IDs or UPNs).
- `msteams.textChunkLimit`: outbound text chunk size. - `channels.msteams.textChunkLimit`: outbound text chunk size.
- `msteams.mediaAllowHosts`: allowlist for inbound attachment hosts (defaults to Microsoft/Teams domains). - `channels.msteams.mediaAllowHosts`: allowlist for inbound attachment hosts (defaults to Microsoft/Teams domains).
- `msteams.requireMention`: require @mention in channels/groups (default true). - `channels.msteams.requireMention`: require @mention in channels/groups (default true).
- `msteams.replyStyle`: `thread | top-level` (see [Reply Style](#reply-style-threads-vs-posts)). - `channels.msteams.replyStyle`: `thread | top-level` (see [Reply Style](#reply-style-threads-vs-posts)).
- `msteams.teams.<teamId>.replyStyle`: per-team override. - `channels.msteams.teams.<teamId>.replyStyle`: per-team override.
- `msteams.teams.<teamId>.requireMention`: per-team override. - `channels.msteams.teams.<teamId>.requireMention`: per-team override.
- `msteams.teams.<teamId>.channels.<conversationId>.replyStyle`: per-channel override. - `channels.msteams.teams.<teamId>.channels.<conversationId>.replyStyle`: per-channel override.
- `msteams.teams.<teamId>.channels.<conversationId>.requireMention`: per-channel override. - `channels.msteams.teams.<teamId>.channels.<conversationId>.requireMention`: per-channel override.
## Routing & Sessions ## Routing & Sessions
- Session keys follow the standard agent format (see [/concepts/session](/concepts/session)): - Session keys follow the standard agent format (see [/concepts/session](/concepts/session)):
@ -399,7 +403,7 @@ Teams recently introduced two channel UI styles over the same underlying data mo
- **Channels/groups:** Attachments live in M365 storage (SharePoint/OneDrive). The webhook payload only includes an HTML stub, not the actual file bytes. **Graph API permissions are required** to download channel attachments. - **Channels/groups:** Attachments live in M365 storage (SharePoint/OneDrive). The webhook payload only includes an HTML stub, not the actual file bytes. **Graph API permissions are required** to download channel attachments.
Without Graph permissions, channel messages with images will be received as text-only (the image content is not accessible to the bot). Without Graph permissions, channel messages with images will be received as text-only (the image content is not accessible to the bot).
By default, Clawdbot only downloads media from Microsoft/Teams hostnames. Override with `msteams.mediaAllowHosts` (use `["*"]` to allow any host). By default, Clawdbot only downloads media from Microsoft/Teams hostnames. Override with `channels.msteams.mediaAllowHosts` (use `["*"]` to allow any host).
## Polls (Adaptive Cards) ## Polls (Adaptive Cards)
Clawdbot sends Teams polls as Adaptive Cards (there is no native Teams poll API). Clawdbot sends Teams polls as Adaptive Cards (there is no native Teams poll API).
@ -458,7 +462,7 @@ Bots have limited support in private channels:
### Common issues ### Common issues
- **Images not showing in channels:** Graph permissions or admin consent missing. Reinstall the Teams app and fully quit/reopen Teams. - **Images not showing in channels:** Graph permissions or admin consent missing. Reinstall the Teams app and fully quit/reopen Teams.
- **No responses in channel:** mentions are required by default; set `msteams.requireMention=false` or configure per team/channel. - **No responses in channel:** mentions are required by default; set `channels.msteams.requireMention=false` or configure per team/channel.
- **Version mismatch (Teams still shows old manifest):** remove + re-add the app and fully quit Teams to refresh. - **Version mismatch (Teams still shows old manifest):** remove + re-add the app and fully quit Teams to refresh.
- **401 Unauthorized from webhook:** Expected when testing manually without Azure JWT - means endpoint is reachable but auth failed. Use Azure Web Chat to test properly. - **401 Unauthorized from webhook:** Expected when testing manually without Azure JWT - means endpoint is reachable but auth failed. Use Azure Web Chat to test properly.

123
docs/channels/signal.md Normal file
View File

@ -0,0 +1,123 @@
---
summary: "Signal support via signal-cli (JSON-RPC + SSE), setup, and number model"
read_when:
- Setting up Signal support
- Debugging Signal send/receive
---
# Signal (signal-cli)
Status: external CLI integration. Gateway talks to `signal-cli` over HTTP JSON-RPC + SSE.
## Quick setup (beginner)
1) Use a **separate Signal number** for the bot (recommended).
2) Install `signal-cli` (Java required).
3) Link the bot device and start the daemon:
- `signal-cli link -n "Clawdbot"`
4) Configure Clawdbot and start the gateway.
Minimal config:
```json5
{
channels: {
signal: {
enabled: true,
account: "+15551234567",
cliPath: "signal-cli",
dmPolicy: "pairing",
allowFrom: ["+15557654321"]
}
}
}
```
## What it is
- Signal channel via `signal-cli` (not embedded libsignal).
- Deterministic routing: replies always go back to Signal.
- DMs share the agent's main session; groups are isolated (`agent:<agentId>:signal:group:<groupId>`).
## The number model (important)
- The gateway connects to a **Signal device** (the `signal-cli` account).
- If you run the bot on **your personal Signal account**, it will ignore your own messages (loop protection).
- For "I text the bot and it replies," use a **separate bot number**.
## Setup (fast path)
1) Install `signal-cli` (Java required).
2) Link a bot account:
- `signal-cli link -n "Clawdbot"` then scan the QR in Signal.
3) Configure Signal and start the gateway.
Example:
```json5
{
channels: {
signal: {
enabled: true,
account: "+15551234567",
cliPath: "signal-cli",
dmPolicy: "pairing",
allowFrom: ["+15557654321"]
}
}
}
```
Multi-account support: use `channels.signal.accounts` with per-account config and optional `name`. See [`gateway/configuration`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern.
## Access control (DMs + groups)
DMs:
- Default: `channels.signal.dmPolicy = "pairing"`.
- Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour).
- Approve via:
- `clawdbot pairing list signal`
- `clawdbot pairing approve signal <CODE>`
- Pairing is the default token exchange for Signal DMs. Details: [Pairing](/start/pairing)
- UUID-only senders (from `sourceUuid`) are stored as `uuid:<id>` in `channels.signal.allowFrom`.
Groups:
- `channels.signal.groupPolicy = open | allowlist | disabled`.
- `channels.signal.groupAllowFrom` controls who can trigger in groups when `allowlist` is set.
## How it works (behavior)
- `signal-cli` runs as a daemon; the gateway reads events via SSE.
- Inbound messages are normalized into the shared channel envelope.
- Replies always route back to the same number or group.
## Media + limits
- Outbound text is chunked to `channels.signal.textChunkLimit` (default 4000).
- Attachments supported (base64 fetched from `signal-cli`).
- Default media cap: `channels.signal.mediaMaxMb` (default 8).
- Use `channels.signal.ignoreAttachments` to skip downloading media.
- Group history context uses `channels.signal.historyLimit` (or `channels.signal.accounts.*.historyLimit`), falling back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50).
## Delivery targets (CLI/cron)
- DMs: `signal:+15551234567` (or plain E.164).
- Groups: `signal:group:<groupId>`.
- Usernames: `username:<name>` (if supported by your Signal account).
## Configuration reference (Signal)
Full configuration: [Configuration](/gateway/configuration)
Provider options:
- `channels.signal.enabled`: enable/disable channel startup.
- `channels.signal.account`: E.164 for the bot account.
- `channels.signal.cliPath`: path to `signal-cli`.
- `channels.signal.httpUrl`: full daemon URL (overrides host/port).
- `channels.signal.httpHost`, `channels.signal.httpPort`: daemon bind (default 127.0.0.1:8080).
- `channels.signal.autoStart`: auto-spawn daemon (default true if `httpUrl` unset).
- `channels.signal.receiveMode`: `on-start | manual`.
- `channels.signal.ignoreAttachments`: skip attachment downloads.
- `channels.signal.ignoreStories`: ignore stories from the daemon.
- `channels.signal.sendReadReceipts`: forward read receipts.
- `channels.signal.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing).
- `channels.signal.allowFrom`: DM allowlist (E.164 or `uuid:<id>`). `open` requires `"*"`.
- `channels.signal.groupPolicy`: `open | allowlist | disabled` (default: allowlist).
- `channels.signal.groupAllowFrom`: group sender allowlist.
- `channels.signal.historyLimit`: max group messages to include as context (0 disables).
- `channels.signal.textChunkLimit`: outbound chunk size (chars).
- `channels.signal.mediaMaxMb`: inbound/outbound media cap (MB).
Related global options:
- `agents.list[].groupChat.mentionPatterns` (Signal does not support native mentions).
- `messages.groupChat.mentionPatterns` (global fallback).
- `messages.responsePrefix`.

View File

@ -13,16 +13,18 @@ read_when: "Setting up Slack or debugging Slack socket mode"
Minimal config: Minimal config:
```json5 ```json5
{ {
slack: { channels: {
enabled: true, slack: {
appToken: "xapp-...", enabled: true,
botToken: "xoxb-..." appToken: "xapp-...",
botToken: "xoxb-..."
}
} }
} }
``` ```
## Setup ## Setup
1) Create a Slack app (From scratch) in https://api.slack.com/apps. 1) Create a Slack app (From scratch) in https://api.channels.slack.com/apps.
2) **Socket Mode** → toggle on. Then go to **Basic Information****App-Level Tokens****Generate Token and Scopes** with scope `connections:write`. Copy the **App Token** (`xapp-...`). 2) **Socket Mode** → toggle on. Then go to **Basic Information****App-Level Tokens****Generate Token and Scopes** with scope `connections:write`. Copy the **App Token** (`xapp-...`).
3) **OAuth & Permissions** → add bot token scopes (use the manifest below). Click **Install to Workspace**. Copy the **Bot User OAuth Token** (`xoxb-...`). 3) **OAuth & Permissions** → add bot token scopes (use the manifest below). Click **Install to Workspace**. Copy the **Bot User OAuth Token** (`xoxb-...`).
4) **Event Subscriptions** → enable events and subscribe to: 4) **Event Subscriptions** → enable events and subscribe to:
@ -33,12 +35,12 @@ Minimal config:
- `channel_rename` - `channel_rename`
- `pin_added`, `pin_removed` - `pin_added`, `pin_removed`
5) Invite the bot to channels you want it to read. 5) Invite the bot to channels you want it to read.
6) Slash Commands → create `/clawd` if you use `slack.slashCommand`. If you enable native commands, add one slash command per built-in command (same names as `/help`). Native defaults to off for Slack unless you set `slack.commands.native: true` (global `commands.native` is `"auto"` which leaves Slack off). 6) Slash Commands → create `/clawd` if you use `channels.slack.slashCommand`. If you enable native commands, add one slash command per built-in command (same names as `/help`). Native defaults to off for Slack unless you set `channels.slack.commands.native: true` (global `commands.native` is `"auto"` which leaves Slack off).
7) App Home → enable the **Messages Tab** so users can DM the bot. 7) App Home → enable the **Messages Tab** so users can DM the bot.
Use the manifest below so scopes and events stay in sync. Use the manifest below so scopes and events stay in sync.
Multi-account support: use `slack.accounts` with per-account tokens and optional `name`. See [`gateway/configuration`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern. Multi-account support: use `channels.slack.accounts` with per-account tokens and optional `name`. See [`gateway/configuration`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern.
## Clawdbot config (minimal) ## Clawdbot config (minimal)
@ -50,16 +52,18 @@ Or via config:
```json5 ```json5
{ {
slack: { channels: {
enabled: true, slack: {
appToken: "xapp-...", enabled: true,
botToken: "xoxb-..." appToken: "xapp-...",
botToken: "xoxb-..."
}
} }
} }
``` ```
## History context ## History context
- `slack.historyLimit` (or `slack.accounts.*.historyLimit`) controls how many recent channel/group messages are wrapped into the prompt. - `channels.slack.historyLimit` (or `channels.slack.accounts.*.historyLimit`) controls how many recent channel/group messages are wrapped into the prompt.
- Falls back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50). - Falls back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50).
## Manifest (optional) ## Manifest (optional)
@ -138,42 +142,42 @@ Use this Slack app manifest to create the app quickly (adjust the name/command i
} }
``` ```
If you enable native commands, add one `slash_commands` entry per command you want to expose (matching the `/help` list). Override with `slack.commands.native`. If you enable native commands, add one `slash_commands` entry per command you want to expose (matching the `/help` list). Override with `channels.slack.commands.native`.
## Scopes (current vs optional) ## Scopes (current vs optional)
Slack's Conversations API is type-scoped: you only need the scopes for the Slack's Conversations API is type-scoped: you only need the scopes for the
conversation types you actually touch (channels, groups, im, mpim). See conversation types you actually touch (channels, groups, im, mpim). See
https://api.slack.com/docs/conversations-api for the overview. https://api.channels.slack.com/docs/conversations-api for the overview.
### Required scopes ### Required scopes
- `chat:write` (send/update/delete messages via `chat.postMessage`) - `chat:write` (send/update/delete messages via `chat.postMessage`)
https://api.slack.com/methods/chat.postMessage https://api.channels.slack.com/methods/chat.postMessage
- `im:write` (open DMs via `conversations.open` for user DMs) - `im:write` (open DMs via `conversations.open` for user DMs)
https://api.slack.com/methods/conversations.open https://api.channels.slack.com/methods/conversations.open
- `channels:history`, `groups:history`, `im:history`, `mpim:history` - `channels:history`, `groups:history`, `im:history`, `mpim:history`
https://api.slack.com/methods/conversations.history https://api.channels.slack.com/methods/conversations.history
- `channels:read`, `groups:read`, `im:read`, `mpim:read` - `channels:read`, `groups:read`, `im:read`, `mpim:read`
https://api.slack.com/methods/conversations.info https://api.channels.slack.com/methods/conversations.info
- `users:read` (user lookup) - `users:read` (user lookup)
https://api.slack.com/methods/users.info https://api.channels.slack.com/methods/users.info
- `reactions:read`, `reactions:write` (`reactions.get` / `reactions.add`) - `reactions:read`, `reactions:write` (`reactions.get` / `reactions.add`)
https://api.slack.com/methods/reactions.get https://api.channels.slack.com/methods/reactions.get
https://api.slack.com/methods/reactions.add https://api.channels.slack.com/methods/reactions.add
- `pins:read`, `pins:write` (`pins.list` / `pins.add` / `pins.remove`) - `pins:read`, `pins:write` (`pins.list` / `pins.add` / `pins.remove`)
https://api.slack.com/scopes/pins:read https://api.channels.slack.com/scopes/pins:read
https://api.slack.com/scopes/pins:write https://api.channels.slack.com/scopes/pins:write
- `emoji:read` (`emoji.list`) - `emoji:read` (`emoji.list`)
https://api.slack.com/scopes/emoji:read https://api.channels.slack.com/scopes/emoji:read
- `files:write` (uploads via `files.uploadV2`) - `files:write` (uploads via `files.uploadV2`)
https://api.slack.com/messaging/files/uploading https://api.channels.slack.com/messaging/files/uploading
### Not needed today (but likely future) ### Not needed today (but likely future)
- `mpim:write` (only if we add group-DM open/DM start via `conversations.open`) - `mpim:write` (only if we add group-DM open/DM start via `conversations.open`)
- `groups:write` (only if we add private-channel management: create/rename/invite/archive) - `groups:write` (only if we add private-channel management: create/rename/invite/archive)
- `chat:write.public` (only if we want to post to channels the bot isn't in) - `chat:write.public` (only if we want to post to channels the bot isn't in)
https://api.slack.com/scopes/chat:write.public https://api.channels.slack.com/scopes/chat:write.public
- `users:read.email` (only if we need email fields from `users.info`) - `users:read.email` (only if we need email fields from `users.info`)
https://api.slack.com/changelog/2017-04-narrowing-email-access https://api.channels.slack.com/changelog/2017-04-narrowing-email-access
- `files:read` (only if we start listing/reading file metadata) - `files:read` (only if we start listing/reading file metadata)
## Config ## Config
@ -234,11 +238,11 @@ Ack reactions are controlled globally via `messages.ackReaction` +
ack reaction after the bot replies. ack reaction after the bot replies.
## Limits ## Limits
- Outbound text is chunked to `slack.textChunkLimit` (default 4000). - Outbound text is chunked to `channels.slack.textChunkLimit` (default 4000).
- Media uploads are capped by `slack.mediaMaxMb` (default 20). - Media uploads are capped by `channels.slack.mediaMaxMb` (default 20).
## Reply threading ## Reply threading
By default, Clawdbot replies in the main channel. Use `slack.replyToMode` to control automatic threading: By default, Clawdbot replies in the main channel. Use `channels.slack.replyToMode` to control automatic threading:
| Mode | Behavior | | Mode | Behavior |
| --- | --- | | --- | --- |
@ -256,20 +260,20 @@ For fine-grained control, use these tags in agent responses:
## Sessions + routing ## Sessions + routing
- DMs share the `main` session (like WhatsApp/Telegram). - DMs share the `main` session (like WhatsApp/Telegram).
- Channels map to `agent:<agentId>:slack:channel:<channelId>` sessions. - Channels map to `agent:<agentId>:slack:channel:<channelId>` sessions.
- Slash commands use `agent:<agentId>:slack:slash:<userId>` sessions (prefix configurable via `slack.slashCommand.sessionPrefix`). - Slash commands use `agent:<agentId>:slack:slash:<userId>` sessions (prefix configurable via `channels.slack.slashCommand.sessionPrefix`).
- Native command registration uses `commands.native` (global default `"auto"` → Slack off) and can be overridden per-workspace with `slack.commands.native`. Text commands require standalone `/...` messages and can be disabled with `commands.text: false`. Slack slash commands are managed in the Slack app and are not removed automatically. Use `commands.useAccessGroups: false` to bypass access-group checks for commands. - Native command registration uses `commands.native` (global default `"auto"` → Slack off) and can be overridden per-workspace with `channels.slack.commands.native`. Text commands require standalone `/...` messages and can be disabled with `commands.text: false`. Slack slash commands are managed in the Slack app and are not removed automatically. Use `commands.useAccessGroups: false` to bypass access-group checks for commands.
- Full command list + config: [Slash commands](/tools/slash-commands) - Full command list + config: [Slash commands](/tools/slash-commands)
## DM security (pairing) ## DM security (pairing)
- Default: `slack.dm.policy="pairing"` — unknown DM senders get a pairing code (expires after 1 hour). - Default: `channels.slack.dm.policy="pairing"` — unknown DM senders get a pairing code (expires after 1 hour).
- Approve via: `clawdbot pairing approve slack <code>`. - Approve via: `clawdbot pairing approve slack <code>`.
- To allow anyone: set `slack.dm.policy="open"` and `slack.dm.allowFrom=["*"]`. - To allow anyone: set `channels.slack.dm.policy="open"` and `channels.slack.dm.allowFrom=["*"]`.
## Group policy ## Group policy
- `slack.groupPolicy` controls channel handling (`open|disabled|allowlist`). - `channels.slack.groupPolicy` controls channel handling (`open|disabled|allowlist`).
- `allowlist` requires channels to be listed in `slack.channels`. - `allowlist` requires channels to be listed in `channels.slack.channels`.
Channel options (`slack.channels.<id>` or `slack.channels.<name>`): Channel options (`channels.slack.channels.<id>` or `channels.slack.channels.<name>`):
- `allow`: allow/deny the channel when `groupPolicy="allowlist"`. - `allow`: allow/deny the channel when `groupPolicy="allowlist"`.
- `requireMention`: mention gating for the channel. - `requireMention`: mention gating for the channel.
- `allowBots`: allow bot-authored messages in this channel (default: false). - `allowBots`: allow bot-authored messages in this channel (default: false).
@ -284,7 +288,7 @@ Use these with cron/CLI sends:
- `channel:<id>` for channels - `channel:<id>` for channels
## Tool actions ## Tool actions
Slack tool actions can be gated with `slack.actions.*`: Slack tool actions can be gated with `channels.slack.actions.*`:
| Action group | Default | Notes | | Action group | Default | Notes |
| --- | --- | --- | | --- | --- | --- |
@ -295,10 +299,10 @@ Slack tool actions can be gated with `slack.actions.*`:
| emojiList | enabled | Custom emoji list | | emojiList | enabled | Custom emoji list |
## Notes ## Notes
- Mention gating is controlled via `slack.channels` (set `requireMention` to `true`); `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) also count as mentions. - Mention gating is controlled via `channels.slack.channels` (set `requireMention` to `true`); `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) also count as mentions.
- Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`. - Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`.
- Reaction notifications follow `slack.reactionNotifications` (use `reactionAllowlist` with mode `allowlist`). - Reaction notifications follow `channels.slack.reactionNotifications` (use `reactionAllowlist` with mode `allowlist`).
- Bot-authored messages are ignored by default; enable via `slack.allowBots` or `slack.channels.<id>.allowBots`. - Bot-authored messages are ignored by default; enable via `channels.slack.allowBots` or `channels.slack.channels.<id>.allowBots`.
- Warning: If you allow replies to other bots (`slack.allowBots=true` or `slack.channels.<id>.allowBots=true`), prevent bot-to-bot reply loops with `requireMention`, `slack.channels.<id>.users` allowlists, and/or clear guardrails in `AGENTS.md` and `SOUL.md`. - Warning: If you allow replies to other bots (`channels.slack.allowBots=true` or `channels.slack.channels.<id>.allowBots=true`), prevent bot-to-bot reply loops with `requireMention`, `channels.slack.channels.<id>.users` allowlists, and/or clear guardrails in `AGENTS.md` and `SOUL.md`.
- For the Slack tool, reaction removal semantics are in [/tools/reactions](/tools/reactions). - For the Slack tool, reaction removal semantics are in [/tools/reactions](/tools/reactions).
- Attachments are downloaded to the media store when permitted and under the size limit. - Attachments are downloaded to the media store when permitted and under the size limit.

View File

@ -12,24 +12,26 @@ Status: production-ready for bot DMs + groups via grammY. Long-polling by defaul
1) Create a bot with **@BotFather** and copy the token. 1) Create a bot with **@BotFather** and copy the token.
2) Set the token: 2) Set the token:
- Env: `TELEGRAM_BOT_TOKEN=...` - Env: `TELEGRAM_BOT_TOKEN=...`
- Or config: `telegram.botToken: "..."`. - Or config: `channels.telegram.botToken: "..."`.
3) Start the gateway. 3) Start the gateway.
4) DM access is pairing by default; approve the pairing code on first contact. 4) DM access is pairing by default; approve the pairing code on first contact.
Minimal config: Minimal config:
```json5 ```json5
{ {
telegram: { channels: {
enabled: true, telegram: {
botToken: "123:abc", enabled: true,
dmPolicy: "pairing" botToken: "123:abc",
dmPolicy: "pairing"
}
} }
} }
``` ```
## What it is ## What it is
- A Telegram Bot API provider owned by the Gateway. - A Telegram Bot API channel owned by the Gateway.
- Deterministic routing: replies go back to Telegram; the model never chooses providers. - Deterministic routing: replies go back to Telegram; the model never chooses channels.
- DMs share the agent's main session; groups stay isolated (`agent:<agentId>:telegram:group:<chatId>`). - DMs share the agent's main session; groups stay isolated (`agent:<agentId>:telegram:group:<chatId>`).
## Setup (fast path) ## Setup (fast path)
@ -47,22 +49,24 @@ Example:
```json5 ```json5
{ {
telegram: { channels: {
enabled: true, telegram: {
botToken: "123:abc", enabled: true,
dmPolicy: "pairing", botToken: "123:abc",
groups: { "*": { requireMention: true } } dmPolicy: "pairing",
groups: { "*": { requireMention: true } }
}
} }
} }
``` ```
Env option: `TELEGRAM_BOT_TOKEN=...` (works for the default account). Env option: `TELEGRAM_BOT_TOKEN=...` (works for the default account).
Multi-account support: use `telegram.accounts` with per-account tokens and optional `name`. See [`gateway/configuration`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern. Multi-account support: use `channels.telegram.accounts` with per-account tokens and optional `name`. See [`gateway/configuration`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern.
3) Start the gateway. Telegram starts when a token is resolved (env or config). 3) Start the gateway. Telegram starts when a token is resolved (env or config).
4) DM access defaults to pairing. Approve the code when the bot is first contacted. 4) DM access defaults to pairing. Approve the code when the bot is first contacted.
5) For groups: add the bot, decide privacy/admin behavior (below), then set `telegram.groups` to control mention gating + allowlists. 5) For groups: add the bot, decide privacy/admin behavior (below), then set `channels.telegram.groups` to control mention gating + allowlists.
## Token + privacy + permissions (Telegram side) ## Token + privacy + permissions (Telegram side)
@ -84,7 +88,7 @@ Admin status is set inside the group (Telegram UI). Admin bots always receive al
group messages, so use admin if you need full visibility. group messages, so use admin if you need full visibility.
## How it works (behavior) ## How it works (behavior)
- Inbound messages are normalized into the shared provider envelope with reply context and media placeholders. - Inbound messages are normalized into the shared channel envelope with reply context and media placeholders.
- Group replies require a mention by default (native @mention or `agents.list[].groupChat.mentionPatterns` / `messages.groupChat.mentionPatterns`). - Group replies require a mention by default (native @mention or `agents.list[].groupChat.mentionPatterns` / `messages.groupChat.mentionPatterns`).
- Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`. - Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`.
- Replies always route back to the same Telegram chat. - Replies always route back to the same Telegram chat.
@ -97,9 +101,9 @@ group messages, so use admin if you need full visibility.
- If Telegram rejects the HTML payload, Clawdbot retries the same message as plain text. - If Telegram rejects the HTML payload, Clawdbot retries the same message as plain text.
## Limits ## Limits
- Outbound text is chunked to `telegram.textChunkLimit` (default 4000). - Outbound text is chunked to `channels.telegram.textChunkLimit` (default 4000).
- Media downloads/uploads are capped by `telegram.mediaMaxMb` (default 5). - Media downloads/uploads are capped by `channels.telegram.mediaMaxMb` (default 5).
- Group history context uses `telegram.historyLimit` (or `telegram.accounts.*.historyLimit`), falling back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50). - Group history context uses `channels.telegram.historyLimit` (or `channels.telegram.accounts.*.historyLimit`), falling back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50).
## Group activation modes ## Group activation modes
@ -109,22 +113,26 @@ By default, the bot only responds to mentions in groups (`@botname` or patterns
```json5 ```json5
{ {
telegram: { channels: {
groups: { telegram: {
"-1001234567890": { requireMention: false } // always respond in this group groups: {
"-1001234567890": { requireMention: false } // always respond in this group
}
} }
} }
} }
``` ```
**Important:** Setting `telegram.groups` creates an **allowlist** - only listed groups (or `"*"`) will be accepted. **Important:** Setting `channels.telegram.groups` creates an **allowlist** - only listed groups (or `"*"`) will be accepted.
To allow all groups with always-respond: To allow all groups with always-respond:
```json5 ```json5
{ {
telegram: { channels: {
groups: { telegram: {
"*": { requireMention: false } // all groups, always respond groups: {
"*": { requireMention: false } // all groups, always respond
}
} }
} }
} }
@ -133,9 +141,11 @@ To allow all groups with always-respond:
To keep mention-only for all groups (default behavior): To keep mention-only for all groups (default behavior):
```json5 ```json5
{ {
telegram: { channels: {
groups: { telegram: {
"*": { requireMention: true } // or omit groups entirely groups: {
"*": { requireMention: true } // or omit groups entirely
}
} }
} }
} }
@ -162,49 +172,49 @@ Telegram forum topics include a `message_thread_id` per message. Clawdbot:
- Appends `:topic:<threadId>` to the Telegram group session key so each topic is isolated. - Appends `:topic:<threadId>` to the Telegram group session key so each topic is isolated.
- Sends typing indicators and replies with `message_thread_id` so responses stay in the topic. - Sends typing indicators and replies with `message_thread_id` so responses stay in the topic.
- Exposes `MessageThreadId` + `IsForum` in template context for routing/templating. - Exposes `MessageThreadId` + `IsForum` in template context for routing/templating.
- Topic-specific configuration is available under `telegram.groups.<chatId>.topics.<threadId>` (skills, allowlists, auto-reply, system prompts, disable). - Topic-specific configuration is available under `channels.telegram.groups.<chatId>.topics.<threadId>` (skills, allowlists, auto-reply, system prompts, disable).
Private chats can include `message_thread_id` in some edge cases. Clawdbot keeps the DM session key unchanged, but still uses the thread id for replies/draft streaming when it is present. Private chats can include `message_thread_id` in some edge cases. Clawdbot keeps the DM session key unchanged, but still uses the thread id for replies/draft streaming when it is present.
## Access control (DMs + groups) ## Access control (DMs + groups)
### DM access ### DM access
- Default: `telegram.dmPolicy = "pairing"`. Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour). - Default: `channels.telegram.dmPolicy = "pairing"`. Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour).
- Approve via: - Approve via:
- `clawdbot pairing list telegram` - `clawdbot pairing list telegram`
- `clawdbot pairing approve telegram <CODE>` - `clawdbot pairing approve telegram <CODE>`
- Pairing is the default token exchange used for Telegram DMs. Details: [Pairing](/start/pairing) - Pairing is the default token exchange used for Telegram DMs. Details: [Pairing](/start/pairing)
- `telegram.allowFrom` accepts numeric user IDs (recommended) or `@username` entries. It is **not** the bot username; use the human senders ID (get it from `@userinfobot` or the `from.id` field in the gateway log). - `channels.telegram.allowFrom` accepts numeric user IDs (recommended) or `@username` entries. It is **not** the bot username; use the human senders ID (get it from `@userinfobot` or the `from.id` field in the gateway log).
### Group access ### Group access
Two independent controls: Two independent controls:
**1. Which groups are allowed** (group allowlist via `telegram.groups`): **1. Which groups are allowed** (group allowlist via `channels.telegram.groups`):
- No `groups` config = all groups allowed - No `groups` config = all groups allowed
- With `groups` config = only listed groups or `"*"` are allowed - With `groups` config = only listed groups or `"*"` are allowed
- Example: `"groups": { "-1001234567890": {}, "*": {} }` allows all groups - Example: `"groups": { "-1001234567890": {}, "*": {} }` allows all groups
**2. Which senders are allowed** (sender filtering via `telegram.groupPolicy`): **2. Which senders are allowed** (sender filtering via `channels.telegram.groupPolicy`):
- `"open"` = all senders in allowed groups can message - `"open"` = all senders in allowed groups can message
- `"allowlist"` = only senders in `telegram.groupAllowFrom` can message - `"allowlist"` = only senders in `channels.telegram.groupAllowFrom` can message
- `"disabled"` = no group messages accepted at all - `"disabled"` = no group messages accepted at all
Default is `groupPolicy: "allowlist"` (blocked unless you add `groupAllowFrom`). Default is `groupPolicy: "allowlist"` (blocked unless you add `groupAllowFrom`).
Most users want: `groupPolicy: "allowlist"` + `groupAllowFrom` + specific groups listed in `telegram.groups` Most users want: `groupPolicy: "allowlist"` + `groupAllowFrom` + specific groups listed in `channels.telegram.groups`
## Long-polling vs webhook ## Long-polling vs webhook
- Default: long-polling (no public URL required). - Default: long-polling (no public URL required).
- Webhook mode: set `telegram.webhookUrl` (optionally `telegram.webhookSecret` + `telegram.webhookPath`). - Webhook mode: set `channels.telegram.webhookUrl` (optionally `channels.telegram.webhookSecret` + `channels.telegram.webhookPath`).
- The local listener binds to `0.0.0.0:8787` and serves `POST /telegram-webhook` by default. - The local listener binds to `0.0.0.0:8787` and serves `POST /telegram-webhook` by default.
- If your public URL is different, use a reverse proxy and point `telegram.webhookUrl` at the public endpoint. - If your public URL is different, use a reverse proxy and point `channels.telegram.webhookUrl` at the public endpoint.
## Reply threading ## Reply threading
Telegram supports optional threaded replies via tags: Telegram supports optional threaded replies via tags:
- `[[reply_to_current]]` -- reply to the triggering message. - `[[reply_to_current]]` -- reply to the triggering message.
- `[[reply_to:<id>]]` -- reply to a specific message id. - `[[reply_to:<id>]]` -- reply to a specific message id.
Controlled by `telegram.replyToMode`: Controlled by `channels.telegram.replyToMode`:
- `first` (default), `all`, `off`. - `first` (default), `all`, `off`.
## Audio messages (voice vs file) ## Audio messages (voice vs file)
@ -214,7 +224,7 @@ Clawdbot defaults to audio files for backward compatibility.
To force a voice note bubble in agent replies, include this tag anywhere in the reply: To force a voice note bubble in agent replies, include this tag anywhere in the reply:
- `[[audio_as_voice]]` — send audio as a voice note instead of a file. - `[[audio_as_voice]]` — send audio as a voice note instead of a file.
The tag is stripped from the delivered text. Other providers ignore this tag. The tag is stripped from the delivered text. Other channels ignore this tag.
## Streaming (drafts) ## Streaming (drafts)
Telegram can stream **draft bubbles** while the agent is generating a response. Telegram can stream **draft bubbles** while the agent is generating a response.
@ -227,32 +237,32 @@ Requirements (Telegram Bot API 9.3+):
- Streaming is ignored for groups/supergroups/channels. - Streaming is ignored for groups/supergroups/channels.
Config: Config:
- `telegram.streamMode: "off" | "partial" | "block"` (default: `partial`) - `channels.telegram.streamMode: "off" | "partial" | "block"` (default: `partial`)
- `partial`: update the draft bubble with the latest streaming text. - `partial`: update the draft bubble with the latest streaming text.
- `block`: update the draft bubble in larger blocks (chunked). - `block`: update the draft bubble in larger blocks (chunked).
- `off`: disable draft streaming. - `off`: disable draft streaming.
- Optional (only for `streamMode: "block"`): - Optional (only for `streamMode: "block"`):
- `telegram.draftChunk: { minChars?, maxChars?, breakPreference? }` - `channels.telegram.draftChunk: { minChars?, maxChars?, breakPreference? }`
- defaults: `minChars: 200`, `maxChars: 800`, `breakPreference: "paragraph"` (clamped to `telegram.textChunkLimit`). - defaults: `minChars: 200`, `maxChars: 800`, `breakPreference: "paragraph"` (clamped to `channels.telegram.textChunkLimit`).
Note: draft streaming is separate from **block streaming** (provider messages). Note: draft streaming is separate from **block streaming** (channel messages).
Block streaming is off by default and requires `telegram.blockStreaming: true` Block streaming is off by default and requires `channels.telegram.blockStreaming: true`
if you want early Telegram messages instead of draft updates. if you want early Telegram messages instead of draft updates.
Reasoning stream (Telegram only): Reasoning stream (Telegram only):
- `/reasoning stream` streams reasoning into the draft bubble while the reply is - `/reasoning stream` streams reasoning into the draft bubble while the reply is
generating, then sends the final answer without reasoning. generating, then sends the final answer without reasoning.
- If `telegram.streamMode` is `off`, reasoning stream is disabled. - If `channels.telegram.streamMode` is `off`, reasoning stream is disabled.
More context: [Streaming + chunking](/concepts/streaming). More context: [Streaming + chunking](/concepts/streaming).
## Retry policy ## Retry policy
Outbound Telegram API calls retry on transient network/429 errors with exponential backoff and jitter. Configure via `telegram.retry`. See [Retry policy](/concepts/retry). Outbound Telegram API calls retry on transient network/429 errors with exponential backoff and jitter. Configure via `channels.telegram.retry`. See [Retry policy](/concepts/retry).
## Agent tool (messages + reactions) ## Agent tool (messages + reactions)
- Tool: `telegram` with `sendMessage` action (`to`, `content`, optional `mediaUrl`, `replyToMessageId`, `messageThreadId`). - Tool: `telegram` with `sendMessage` action (`to`, `content`, optional `mediaUrl`, `replyToMessageId`, `messageThreadId`).
- Tool: `telegram` with `react` action (`chatId`, `messageId`, `emoji`). - Tool: `telegram` with `react` action (`chatId`, `messageId`, `emoji`).
- Reaction removal semantics: see [/tools/reactions](/tools/reactions). - Reaction removal semantics: see [/tools/reactions](/tools/reactions).
- Tool gating: `telegram.actions.reactions` and `telegram.actions.sendMessage` (default: enabled). - Tool gating: `channels.telegram.actions.reactions` and `channels.telegram.actions.sendMessage` (default: enabled).
## Delivery targets (CLI/cron) ## Delivery targets (CLI/cron)
- Use a chat id (`123456789`) or a username (`@name`) as the target. - Use a chat id (`123456789`) or a username (`@name`) as the target.
@ -261,59 +271,59 @@ Outbound Telegram API calls retry on transient network/429 errors with exponenti
## Troubleshooting ## Troubleshooting
**Bot doesnt respond to non-mention messages in a group:** **Bot doesnt respond to non-mention messages in a group:**
- If you set `telegram.groups.*.requireMention=false`, Telegrams Bot API **privacy mode** must be disabled. - If you set `channels.telegram.groups.*.requireMention=false`, Telegrams Bot API **privacy mode** must be disabled.
- BotFather: `/setprivacy`**Disable** (then remove + re-add the bot to the group) - BotFather: `/setprivacy`**Disable** (then remove + re-add the bot to the group)
- `clawdbot providers status` shows a warning when config expects unmentioned group messages. - `clawdbot channels status` shows a warning when config expects unmentioned group messages.
- `clawdbot providers status --probe` can additionally check membership for explicit numeric group IDs (it cant audit wildcard `"*"` rules). - `clawdbot channels status --probe` can additionally check membership for explicit numeric group IDs (it cant audit wildcard `"*"` rules).
- Quick test: `/activation always` (session-only; use config for persistence) - Quick test: `/activation always` (session-only; use config for persistence)
**Bot not seeing group messages at all:** **Bot not seeing group messages at all:**
- If `telegram.groups` is set, the group must be listed or use `"*"` - If `channels.telegram.groups` is set, the group must be listed or use `"*"`
- Check Privacy Settings in @BotFather → "Group Privacy" should be **OFF** - Check Privacy Settings in @BotFather → "Group Privacy" should be **OFF**
- Verify bot is actually a member (not just an admin with no read access) - Verify bot is actually a member (not just an admin with no read access)
- Check gateway logs: `clawdbot logs --follow` (look for "skipping group message") - Check gateway logs: `clawdbot logs --follow` (look for "skipping group message")
**Bot responds to mentions but not `/activation always`:** **Bot responds to mentions but not `/activation always`:**
- The `/activation` command updates session state but doesn't persist to config - The `/activation` command updates session state but doesn't persist to config
- For persistent behavior, add group to `telegram.groups` with `requireMention: false` - For persistent behavior, add group to `channels.telegram.groups` with `requireMention: false`
**Commands like `/status` don't work:** **Commands like `/status` don't work:**
- Make sure your Telegram user ID is authorized (via pairing or `telegram.allowFrom`) - Make sure your Telegram user ID is authorized (via pairing or `channels.telegram.allowFrom`)
- Commands require authorization even in groups with `groupPolicy: "open"` - Commands require authorization even in groups with `groupPolicy: "open"`
## Configuration reference (Telegram) ## Configuration reference (Telegram)
Full configuration: [Configuration](/gateway/configuration) Full configuration: [Configuration](/gateway/configuration)
Provider options: Provider options:
- `telegram.enabled`: enable/disable provider startup. - `channels.telegram.enabled`: enable/disable channel startup.
- `telegram.botToken`: bot token (BotFather). - `channels.telegram.botToken`: bot token (BotFather).
- `telegram.tokenFile`: read token from file path. - `channels.telegram.tokenFile`: read token from file path.
- `telegram.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing). - `channels.telegram.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing).
- `telegram.allowFrom`: DM allowlist (ids/usernames). `open` requires `"*"`. - `channels.telegram.allowFrom`: DM allowlist (ids/usernames). `open` requires `"*"`.
- `telegram.groupPolicy`: `open | allowlist | disabled` (default: allowlist). - `channels.telegram.groupPolicy`: `open | allowlist | disabled` (default: allowlist).
- `telegram.groupAllowFrom`: group sender allowlist (ids/usernames). - `channels.telegram.groupAllowFrom`: group sender allowlist (ids/usernames).
- `telegram.groups`: per-group defaults + allowlist (use `"*"` for global defaults). - `channels.telegram.groups`: per-group defaults + allowlist (use `"*"` for global defaults).
- `telegram.groups.<id>.requireMention`: mention gating default. - `channels.telegram.groups.<id>.requireMention`: mention gating default.
- `telegram.groups.<id>.skills`: skill filter (omit = all skills, empty = none). - `channels.telegram.groups.<id>.skills`: skill filter (omit = all skills, empty = none).
- `telegram.groups.<id>.allowFrom`: per-group sender allowlist override. - `channels.telegram.groups.<id>.allowFrom`: per-group sender allowlist override.
- `telegram.groups.<id>.systemPrompt`: extra system prompt for the group. - `channels.telegram.groups.<id>.systemPrompt`: extra system prompt for the group.
- `telegram.groups.<id>.enabled`: disable the group when `false`. - `channels.telegram.groups.<id>.enabled`: disable the group when `false`.
- `telegram.groups.<id>.topics.<threadId>.*`: per-topic overrides (same fields as group). - `channels.telegram.groups.<id>.topics.<threadId>.*`: per-topic overrides (same fields as group).
- `telegram.groups.<id>.topics.<threadId>.requireMention`: per-topic mention gating override. - `channels.telegram.groups.<id>.topics.<threadId>.requireMention`: per-topic mention gating override.
- `telegram.replyToMode`: `off | first | all` (default: `first`). - `channels.telegram.replyToMode`: `off | first | all` (default: `first`).
- `telegram.textChunkLimit`: outbound chunk size (chars). - `channels.telegram.textChunkLimit`: outbound chunk size (chars).
- `telegram.streamMode`: `off | partial | block` (draft streaming). - `channels.telegram.streamMode`: `off | partial | block` (draft streaming).
- `telegram.mediaMaxMb`: inbound/outbound media cap (MB). - `channels.telegram.mediaMaxMb`: inbound/outbound media cap (MB).
- `telegram.retry`: retry policy for outbound Telegram API calls (attempts, minDelayMs, maxDelayMs, jitter). - `channels.telegram.retry`: retry policy for outbound Telegram API calls (attempts, minDelayMs, maxDelayMs, jitter).
- `telegram.proxy`: proxy URL for Bot API calls (SOCKS/HTTP). - `channels.telegram.proxy`: proxy URL for Bot API calls (SOCKS/HTTP).
- `telegram.webhookUrl`: enable webhook mode. - `channels.telegram.webhookUrl`: enable webhook mode.
- `telegram.webhookSecret`: webhook secret (optional). - `channels.telegram.webhookSecret`: webhook secret (optional).
- `telegram.webhookPath`: local webhook path (default `/telegram-webhook`). - `channels.telegram.webhookPath`: local webhook path (default `/telegram-webhook`).
- `telegram.actions.reactions`: gate Telegram tool reactions. - `channels.telegram.actions.reactions`: gate Telegram tool reactions.
- `telegram.actions.sendMessage`: gate Telegram tool message sends. - `channels.telegram.actions.sendMessage`: gate Telegram tool message sends.
Related global options: Related global options:
- `agents.list[].groupChat.mentionPatterns` (mention gating patterns). - `agents.list[].groupChat.mentionPatterns` (mention gating patterns).
- `messages.groupChat.mentionPatterns` (global fallback). - `messages.groupChat.mentionPatterns` (global fallback).
- `commands.native` (defaults to `"auto"` → on for Telegram/Discord, off for Slack), `commands.text`, `commands.useAccessGroups` (command behavior). Override with `telegram.commands.native`. - `commands.native` (defaults to `"auto"` → on for Telegram/Discord, off for Slack), `commands.text`, `commands.useAccessGroups` (command behavior). Override with `channels.telegram.commands.native`.
- `messages.responsePrefix`, `messages.ackReaction`, `messages.ackReactionScope`, `messages.removeAckAfterReply`. - `messages.responsePrefix`, `messages.ackReaction`, `messages.ackReactionScope`, `messages.removeAckAfterReply`.

View File

@ -0,0 +1,21 @@
---
summary: "Channel-specific troubleshooting shortcuts (Discord/Telegram/WhatsApp)"
read_when:
- A channel connects but messages dont flow
- Investigating channel misconfiguration (intents, permissions, privacy mode)
---
# Channel troubleshooting
Start with:
```bash
clawdbot doctor
clawdbot channels status --probe
```
`channels status --probe` prints warnings when it can detect common channel misconfigurations, and includes small live checks (credentials, some permissions/membership).
## Channels
- Discord: [/channels/discord#troubleshooting](/channels/discord#troubleshooting)
- Telegram: [/channels/telegram#troubleshooting](/channels/telegram#troubleshooting)
- WhatsApp: [/channels/whatsapp#troubleshooting-quick](/channels/whatsapp#troubleshooting-quick)

View File

@ -1,9 +1,9 @@
--- ---
summary: "WhatsApp (web provider) integration: login, inbox, replies, media, and ops" summary: "WhatsApp (web channel) integration: login, inbox, replies, media, and ops"
read_when: read_when:
- Working on WhatsApp/web provider behavior or inbox routing - Working on WhatsApp/web channel behavior or inbox routing
--- ---
# WhatsApp (web provider) # WhatsApp (web channel)
Status: WhatsApp Web via Baileys only. Gateway owns the session(s). Status: WhatsApp Web via Baileys only. Gateway owns the session(s).
@ -11,15 +11,17 @@ Status: WhatsApp Web via Baileys only. Gateway owns the session(s).
## Quick setup (beginner) ## Quick setup (beginner)
1) Use a **separate phone number** if possible (recommended). 1) Use a **separate phone number** if possible (recommended).
2) Configure WhatsApp in `~/.clawdbot/clawdbot.json`. 2) Configure WhatsApp in `~/.clawdbot/clawdbot.json`.
3) Run `clawdbot providers login` to scan the QR code (Linked Devices). 3) Run `clawdbot channels login` to scan the QR code (Linked Devices).
4) Start the gateway. 4) Start the gateway.
Minimal config: Minimal config:
```json5 ```json5
{ {
whatsapp: { channels: {
dmPolicy: "allowlist", whatsapp: {
allowFrom: ["+15551234567"] dmPolicy: "allowlist",
allowFrom: ["+15551234567"]
}
} }
} }
``` ```
@ -44,17 +46,19 @@ Use a **separate phone number** for Clawdbot. Best UX, clean routing, no self-ch
**WhatsApp Business:** You can use WhatsApp Business on the same device with a different number. Great for keeping your personal WhatsApp separate — install WhatsApp Business and register the Clawdbot number there. **WhatsApp Business:** You can use WhatsApp Business on the same device with a different number. Great for keeping your personal WhatsApp separate — install WhatsApp Business and register the Clawdbot number there.
**Sample config (dedicated number, single-user allowlist):** **Sample config (dedicated number, single-user allowlist):**
```json ```json5
{ {
"whatsapp": { channels: {
"dmPolicy": "allowlist", whatsapp: {
"allowFrom": ["+15551234567"] dmPolicy: "allowlist",
allowFrom: ["+15551234567"]
}
} }
} }
``` ```
**Pairing mode (optional):** **Pairing mode (optional):**
If you want pairing instead of allowlist, set `whatsapp.dmPolicy` to `pairing`. Unknown senders get a pairing code; approve with: If you want pairing instead of allowlist, set `channels.whatsapp.dmPolicy` to `pairing`. Unknown senders get a pairing code; approve with:
`clawdbot pairing approve whatsapp <code>` `clawdbot pairing approve whatsapp <code>`
### Personal number (fallback) ### Personal number (fallback)
@ -96,13 +100,13 @@ on outbound replies.
- Result: unreliable delivery and frequent blocks, so support was removed. - Result: unreliable delivery and frequent blocks, so support was removed.
## Login + credentials ## Login + credentials
- Login command: `clawdbot providers login` (QR via Linked Devices). - Login command: `clawdbot channels login` (QR via Linked Devices).
- Multi-account login: `clawdbot providers login --account <id>` (`<id>` = `accountId`). - Multi-account login: `clawdbot channels login --account <id>` (`<id>` = `accountId`).
- Default account (when `--account` is omitted): `default` if present, otherwise the first configured account id (sorted). - Default account (when `--account` is omitted): `default` if present, otherwise the first configured account id (sorted).
- Credentials stored in `~/.clawdbot/credentials/whatsapp/<accountId>/creds.json`. - Credentials stored in `~/.clawdbot/credentials/whatsapp/<accountId>/creds.json`.
- Backup copy at `creds.json.bak` (restored on corruption). - Backup copy at `creds.json.bak` (restored on corruption).
- Legacy compatibility: older installs stored Baileys files directly in `~/.clawdbot/credentials/`. - Legacy compatibility: older installs stored Baileys files directly in `~/.clawdbot/credentials/`.
- Logout: `clawdbot providers logout` (or `--account <id>`) deletes WhatsApp auth state (but keeps shared `oauth.json`). - Logout: `clawdbot channels logout` (or `--account <id>`) deletes WhatsApp auth state (but keeps shared `oauth.json`).
- Logged-out socket => error instructs re-link. - Logged-out socket => error instructs re-link.
## Inbound flow (DM + group) ## Inbound flow (DM + group)
@ -110,17 +114,17 @@ on outbound replies.
- Inbox listeners are detached on shutdown to avoid accumulating event handlers in tests/restarts. - Inbox listeners are detached on shutdown to avoid accumulating event handlers in tests/restarts.
- Status/broadcast chats are ignored. - Status/broadcast chats are ignored.
- Direct chats use E.164; groups use group JID. - Direct chats use E.164; groups use group JID.
- **DM policy**: `whatsapp.dmPolicy` controls direct chat access (default: `pairing`). - **DM policy**: `channels.whatsapp.dmPolicy` controls direct chat access (default: `pairing`).
- Pairing: unknown senders get a pairing code (approve via `clawdbot pairing approve whatsapp <code>`; codes expire after 1 hour). - Pairing: unknown senders get a pairing code (approve via `clawdbot pairing approve whatsapp <code>`; codes expire after 1 hour).
- Open: requires `whatsapp.allowFrom` to include `"*"`. - Open: requires `channels.whatsapp.allowFrom` to include `"*"`.
- Self messages are always allowed; “self-chat mode” still requires `whatsapp.allowFrom` to include your own number. - Self messages are always allowed; “self-chat mode” still requires `channels.whatsapp.allowFrom` to include your own number.
### Personal-number mode (fallback) ### Personal-number mode (fallback)
If you run Clawdbot on your **personal WhatsApp number**, enable `whatsapp.selfChatMode` (see sample above). If you run Clawdbot on your **personal WhatsApp number**, enable `channels.whatsapp.selfChatMode` (see sample above).
Behavior: Behavior:
- Outbound DMs never trigger pairing replies (prevents spamming contacts). - Outbound DMs never trigger pairing replies (prevents spamming contacts).
- Inbound unknown senders still follow `whatsapp.dmPolicy`. - Inbound unknown senders still follow `channels.whatsapp.dmPolicy`.
- Self-chat mode (allowFrom includes your number) avoids auto read receipts and ignores mention JIDs. - Self-chat mode (allowFrom includes your number) avoids auto read receipts and ignores mention JIDs.
- Read receipts sent for non-self-chat DMs. - Read receipts sent for non-self-chat DMs.
@ -133,13 +137,13 @@ No. Default DM policy is **pairing**, so unknown senders only get a pairing code
Pairing is a DM gate for unknown senders: Pairing is a DM gate for unknown senders:
- First DM from a new sender returns a short code (message is not processed). - First DM from a new sender returns a short code (message is not processed).
- Approve with: `clawdbot pairing approve whatsapp <code>` (list with `clawdbot pairing list whatsapp`). - Approve with: `clawdbot pairing approve whatsapp <code>` (list with `clawdbot pairing list whatsapp`).
- Codes expire after 1 hour; pending requests are capped at 3 per provider. - Codes expire after 1 hour; pending requests are capped at 3 per channel.
**Can multiple people use different Clawdbots on one WhatsApp number?** **Can multiple people use different Clawdbots on one WhatsApp number?**
Yes, by routing each sender to a different agent via `bindings` (peer `kind: "dm"`, sender E.164 like `+15551234567`). Replies still come from the **same WhatsApp account**, and direct chats collapse to each agents main session, so use **one agent per person**. DM access control (`dmPolicy`/`allowFrom`) is global per WhatsApp account. See [Multi-Agent Routing](/concepts/multi-agent). Yes, by routing each sender to a different agent via `bindings` (peer `kind: "dm"`, sender E.164 like `+15551234567`). Replies still come from the **same WhatsApp account**, and direct chats collapse to each agents main session, so use **one agent per person**. DM access control (`dmPolicy`/`allowFrom`) is global per WhatsApp account. See [Multi-Agent Routing](/concepts/multi-agent).
**Why do you ask for my phone number in the wizard?** **Why do you ask for my phone number in the wizard?**
The wizard uses it to set your **allowlist/owner** so your own DMs are permitted. Its not used for auto-sending. If you run on your personal WhatsApp number, use that same number and enable `whatsapp.selfChatMode`. The wizard uses it to set your **allowlist/owner** so your own DMs are permitted. Its not used for auto-sending. If you run on your personal WhatsApp number, use that same number and enable `channels.whatsapp.selfChatMode`.
## Message normalization (what the model sees) ## Message normalization (what the model sees)
- `Body` is the current message body with envelope. - `Body` is the current message body with envelope.
@ -158,12 +162,12 @@ The wizard uses it to set your **allowlist/owner** so your own DMs are permitted
## Groups ## Groups
- Groups map to `agent:<agentId>:whatsapp:group:<jid>` sessions. - Groups map to `agent:<agentId>:whatsapp:group:<jid>` sessions.
- Group policy: `whatsapp.groupPolicy = open|disabled|allowlist` (default `allowlist`). - Group policy: `channels.whatsapp.groupPolicy = open|disabled|allowlist` (default `allowlist`).
- Activation modes: - Activation modes:
- `mention` (default): requires @mention or regex match. - `mention` (default): requires @mention or regex match.
- `always`: always triggers. - `always`: always triggers.
- `/activation mention|always` is owner-only and must be sent as a standalone message. - `/activation mention|always` is owner-only and must be sent as a standalone message.
- Owner = `whatsapp.allowFrom` (or self E.164 if unset). - Owner = `channels.whatsapp.allowFrom` (or self E.164 if unset).
- **History injection**: - **History injection**:
- Recent messages (default 50) inserted under: - Recent messages (default 50) inserted under:
`[Chat messages since your last reply - for context]` `[Chat messages since your last reply - for context]`
@ -174,7 +178,7 @@ The wizard uses it to set your **allowlist/owner** so your own DMs are permitted
## Reply delivery (threading) ## Reply delivery (threading)
- WhatsApp Web sends standard messages (no quoted reply threading in the current gateway). - WhatsApp Web sends standard messages (no quoted reply threading in the current gateway).
- Reply tags are ignored on this provider. - Reply tags are ignored on this channel.
## Acknowledgment reactions (auto-react on receipt) ## Acknowledgment reactions (auto-react on receipt)
@ -223,22 +227,22 @@ WhatsApp can automatically send emoji reactions to incoming messages immediately
- In groups with `requireMention: false` (activation: always), `group: "mentions"` will react to all messages (not just @mentions). - In groups with `requireMention: false` (activation: always), `group: "mentions"` will react to all messages (not just @mentions).
- Fire-and-forget: reaction failures are logged but don't prevent the bot from replying. - Fire-and-forget: reaction failures are logged but don't prevent the bot from replying.
- Participant JID is automatically included for group reactions. - Participant JID is automatically included for group reactions.
- WhatsApp ignores `messages.ackReaction`; use `whatsapp.ackReaction` instead. - WhatsApp ignores `messages.ackReaction`; use `channels.whatsapp.ackReaction` instead.
## Agent tool (reactions) ## Agent tool (reactions)
- Tool: `whatsapp` with `react` action (`chatJid`, `messageId`, `emoji`, optional `remove`). - Tool: `whatsapp` with `react` action (`chatJid`, `messageId`, `emoji`, optional `remove`).
- Optional: `participant` (group sender), `fromMe` (reacting to your own message), `accountId` (multi-account). - Optional: `participant` (group sender), `fromMe` (reacting to your own message), `accountId` (multi-account).
- Reaction removal semantics: see [/tools/reactions](/tools/reactions). - Reaction removal semantics: see [/tools/reactions](/tools/reactions).
- Tool gating: `whatsapp.actions.reactions` (default: enabled). - Tool gating: `channels.whatsapp.actions.reactions` (default: enabled).
## Limits ## Limits
- Outbound text is chunked to `whatsapp.textChunkLimit` (default 4000). - Outbound text is chunked to `channels.whatsapp.textChunkLimit` (default 4000).
- Inbound media saves are capped by `whatsapp.mediaMaxMb` (default 50 MB). - Inbound media saves are capped by `channels.whatsapp.mediaMaxMb` (default 50 MB).
- Outbound media items are capped by `agents.defaults.mediaMaxMb` (default 5 MB). - Outbound media items are capped by `agents.defaults.mediaMaxMb` (default 5 MB).
## Outbound send (text + media) ## Outbound send (text + media)
- Uses active web listener; error if gateway not running. - Uses active web listener; error if gateway not running.
- Text chunking: 4k max per message (configurable via `whatsapp.textChunkLimit`). - Text chunking: 4k max per message (configurable via `channels.whatsapp.textChunkLimit`).
- Media: - Media:
- Image/video/audio/document supported. - Image/video/audio/document supported.
- Audio sent as PTT; `audio/ogg` => `audio/ogg; codecs=opus`. - Audio sent as PTT; `audio/ogg` => `audio/ogg; codecs=opus`.
@ -258,7 +262,7 @@ WhatsApp can automatically send emoji reactions to incoming messages immediately
- **Gateway heartbeat** logs connection health (`web.heartbeatSeconds`, default 60s). - **Gateway heartbeat** logs connection health (`web.heartbeatSeconds`, default 60s).
- **Agent heartbeat** is global (`agents.defaults.heartbeat.*`) and runs in the main session. - **Agent heartbeat** is global (`agents.defaults.heartbeat.*`) and runs in the main session.
- Uses the configured heartbeat prompt (default: `Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.`) + `HEARTBEAT_OK` skip behavior. - Uses the configured heartbeat prompt (default: `Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.`) + `HEARTBEAT_OK` skip behavior.
- Delivery defaults to the last used provider (or configured target). - Delivery defaults to the last used channel (or configured target).
## Reconnect behavior ## Reconnect behavior
- Backoff policy: `web.reconnect`: - Backoff policy: `web.reconnect`:
@ -267,22 +271,22 @@ WhatsApp can automatically send emoji reactions to incoming messages immediately
- Logged-out => stop and require re-link. - Logged-out => stop and require re-link.
## Config quick map ## Config quick map
- `whatsapp.dmPolicy` (DM policy: pairing/allowlist/open/disabled). - `channels.whatsapp.dmPolicy` (DM policy: pairing/allowlist/open/disabled).
- `whatsapp.selfChatMode` (same-phone setup; bot uses your personal WhatsApp number). - `channels.whatsapp.selfChatMode` (same-phone setup; bot uses your personal WhatsApp number).
- `whatsapp.allowFrom` (DM allowlist). - `channels.whatsapp.allowFrom` (DM allowlist).
- `whatsapp.mediaMaxMb` (inbound media save cap). - `channels.whatsapp.mediaMaxMb` (inbound media save cap).
- `whatsapp.ackReaction` (auto-reaction on message receipt: `{emoji, direct, group}`). - `channels.whatsapp.ackReaction` (auto-reaction on message receipt: `{emoji, direct, group}`).
- `whatsapp.accounts.<accountId>.*` (per-account settings + optional `authDir`). - `channels.whatsapp.accounts.<accountId>.*` (per-account settings + optional `authDir`).
- `whatsapp.accounts.<accountId>.mediaMaxMb` (per-account inbound media cap). - `channels.whatsapp.accounts.<accountId>.mediaMaxMb` (per-account inbound media cap).
- `whatsapp.accounts.<accountId>.ackReaction` (per-account ack reaction override). - `channels.whatsapp.accounts.<accountId>.ackReaction` (per-account ack reaction override).
- `whatsapp.groupAllowFrom` (group sender allowlist). - `channels.whatsapp.groupAllowFrom` (group sender allowlist).
- `whatsapp.groupPolicy` (group policy). - `channels.whatsapp.groupPolicy` (group policy).
- `whatsapp.historyLimit` / `whatsapp.accounts.<accountId>.historyLimit` (group history context; `0` disables). - `channels.whatsapp.historyLimit` / `channels.whatsapp.accounts.<accountId>.historyLimit` (group history context; `0` disables).
- `whatsapp.groups` (group allowlist + mention gating defaults; use `"*"` to allow all) - `channels.whatsapp.groups` (group allowlist + mention gating defaults; use `"*"` to allow all)
- `whatsapp.actions.reactions` (gate WhatsApp tool reactions). - `channels.whatsapp.actions.reactions` (gate WhatsApp tool reactions).
- `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) - `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`)
- `messages.groupChat.historyLimit` - `messages.groupChat.historyLimit`
- `whatsapp.messagePrefix` (inbound prefix; per-account: `whatsapp.accounts.<accountId>.messagePrefix`; deprecated: `messages.messagePrefix`) - `channels.whatsapp.messagePrefix` (inbound prefix; per-account: `channels.whatsapp.accounts.<accountId>.messagePrefix`; deprecated: `messages.messagePrefix`)
- `messages.responsePrefix` (outbound prefix) - `messages.responsePrefix` (outbound prefix)
- `agents.defaults.mediaMaxMb` - `agents.defaults.mediaMaxMb`
- `agents.defaults.heartbeat.every` - `agents.defaults.heartbeat.every`
@ -290,7 +294,7 @@ WhatsApp can automatically send emoji reactions to incoming messages immediately
- `agents.defaults.heartbeat.target` - `agents.defaults.heartbeat.target`
- `agents.defaults.heartbeat.to` - `agents.defaults.heartbeat.to`
- `session.*` (scope, idle, store, mainKey) - `session.*` (scope, idle, store, mainKey)
- `web.enabled` (disable provider startup when false) - `web.enabled` (disable channel startup when false)
- `web.heartbeatSeconds` - `web.heartbeatSeconds`
- `web.reconnect.*` - `web.reconnect.*`
@ -302,12 +306,12 @@ WhatsApp can automatically send emoji reactions to incoming messages immediately
## Troubleshooting (quick) ## Troubleshooting (quick)
**Not linked / QR login required** **Not linked / QR login required**
- Symptom: `providers status` shows `linked: false` or warns “Not linked”. - Symptom: `channels status` shows `linked: false` or warns “Not linked”.
- Fix: run `clawdbot providers login` on the gateway host and scan the QR (WhatsApp → Settings → Linked Devices). - Fix: run `clawdbot channels login` on the gateway host and scan the QR (WhatsApp → Settings → Linked Devices).
**Linked but disconnected / reconnect loop** **Linked but disconnected / reconnect loop**
- Symptom: `providers status` shows `running, disconnected` or warns “Linked but disconnected”. - Symptom: `channels status` shows `running, disconnected` or warns “Linked but disconnected”.
- Fix: `clawdbot doctor` (or restart the gateway). If it persists, relink via `providers login` and inspect `clawdbot logs --follow`. - Fix: `clawdbot doctor` (or restart the gateway). If it persists, relink via `channels login` and inspect `clawdbot logs --follow`.
**Bun runtime** **Bun runtime**
- Bun is **not recommended**. WhatsApp (Baileys) and Telegram are unreliable on Bun. - Bun is **not recommended**. WhatsApp (Baileys) and Telegram are unreliable on Bun.

View File

@ -51,7 +51,7 @@ clawdbot [--dev] [--profile <name>] <command>
reset reset
uninstall uninstall
update update
providers channels
list list
status status
logs logs
@ -257,8 +257,8 @@ Options:
- `--tailscale-reset-on-exit` - `--tailscale-reset-on-exit`
- `--install-daemon` - `--install-daemon`
- `--no-install-daemon` (alias: `--skip-daemon`) - `--no-install-daemon` (alias: `--skip-daemon`)
- `--daemon-runtime <node|bun>` (bun not recommended for WhatsApp/Telegram) - `--daemon-runtime <node|bun>`
- `--skip-providers` - `--skip-channels`
- `--skip-skills` - `--skip-skills`
- `--skip-health` - `--skip-health`
- `--skip-ui` - `--skip-ui`
@ -266,7 +266,7 @@ Options:
- `--json` - `--json`
### `configure` / `config` ### `configure` / `config`
Interactive configuration wizard (models, providers, skills, gateway). Interactive configuration wizard (models, channels, skills, gateway).
### `doctor` ### `doctor`
Health checks + quick fixes (config + gateway + legacy services). Health checks + quick fixes (config + gateway + legacy services).
@ -277,41 +277,41 @@ Options:
- `--non-interactive`: skip prompts; apply safe migrations only. - `--non-interactive`: skip prompts; apply safe migrations only.
- `--deep`: scan system services for extra gateway installs. - `--deep`: scan system services for extra gateway installs.
## Provider helpers ## Channel helpers
### `providers` ### `channels`
Manage chat provider accounts (WhatsApp/Telegram/Discord/Slack/Signal/iMessage/MS Teams). Manage chat channel accounts (WhatsApp/Telegram/Discord/Slack/Signal/iMessage/MS Teams).
Subcommands: Subcommands:
- `providers list`: show configured chat providers and auth profiles (Claude Code + Codex CLI OAuth sync included). - `channels list`: show configured channels and auth profiles (Claude Code + Codex CLI OAuth sync included).
- `providers status`: check gateway reachability and provider health (`--probe` runs extra checks; use `clawdbot health` or `clawdbot status --deep` for gateway health probes). - `channels status`: check gateway reachability and channel health (`--probe` runs extra checks; use `clawdbot health` or `clawdbot status --deep` for gateway health probes).
- Tip: `providers status` prints warnings with suggested fixes when it can detect common misconfigurations (then points you to `clawdbot doctor`). - Tip: `channels status` prints warnings with suggested fixes when it can detect common misconfigurations (then points you to `clawdbot doctor`).
- `providers logs`: show recent provider logs from the gateway log file. - `channels logs`: show recent channel logs from the gateway log file.
- `providers add`: wizard-style setup when no flags are passed; flags switch to non-interactive mode. - `channels add`: wizard-style setup when no flags are passed; flags switch to non-interactive mode.
- `providers remove`: disable by default; pass `--delete` to remove config entries without prompts. - `channels remove`: disable by default; pass `--delete` to remove config entries without prompts.
- `providers login`: interactive provider login (WhatsApp Web only). - `channels login`: interactive channel login (WhatsApp Web only).
- `providers logout`: log out of a provider session (if supported). - `channels logout`: log out of a channel session (if supported).
Common options: Common options:
- `--provider <name>`: `whatsapp|telegram|discord|slack|signal|imessage|msteams` - `--channel <name>`: `whatsapp|telegram|discord|slack|signal|imessage|msteams`
- `--account <id>`: provider account id (default `default`) - `--account <id>`: channel account id (default `default`)
- `--name <label>`: display name for the account - `--name <label>`: display name for the account
`providers login` options: `channels login` options:
- `--provider <provider>` (default `whatsapp`; supports `whatsapp`/`web`) - `--channel <channel>` (default `whatsapp`; supports `whatsapp`/`web`)
- `--account <id>` - `--account <id>`
- `--verbose` - `--verbose`
`providers logout` options: `channels logout` options:
- `--provider <provider>` (default `whatsapp`) - `--channel <channel>` (default `whatsapp`)
- `--account <id>` - `--account <id>`
`providers list` options: `channels list` options:
- `--no-usage`: skip provider usage/quota snapshots (OAuth/API-backed only). - `--no-usage`: skip model provider usage/quota snapshots (OAuth/API-backed only).
- `--json`: output JSON (includes usage unless `--no-usage` is set). - `--json`: output JSON (includes usage unless `--no-usage` is set).
`providers logs` options: `channels logs` options:
- `--provider <name|all>` (default `all`) - `--channel <name|all>` (default `all`)
- `--lines <n>` (default `200`) - `--lines <n>` (default `200`)
- `--json` - `--json`
@ -325,10 +325,10 @@ More detail: [/concepts/oauth](/concepts/oauth)
Examples: Examples:
```bash ```bash
clawdbot providers add --provider telegram --account alerts --name "Alerts Bot" --token $TELEGRAM_BOT_TOKEN clawdbot channels add --channel telegram --account alerts --name "Alerts Bot" --token $TELEGRAM_BOT_TOKEN
clawdbot providers add --provider discord --account work --name "Work Bot" --token $DISCORD_BOT_TOKEN clawdbot channels add --channel discord --account work --name "Work Bot" --token $DISCORD_BOT_TOKEN
clawdbot providers remove --provider discord --account work --delete clawdbot channels remove --channel discord --account work --delete
clawdbot providers status --probe clawdbot channels status --probe
clawdbot status --deep clawdbot status --deep
``` ```
@ -348,11 +348,11 @@ Options:
Tip: use `npx clawdhub` to search, install, and sync skills. Tip: use `npx clawdhub` to search, install, and sync skills.
### `pairing` ### `pairing`
Approve DM pairing requests across providers. Approve DM pairing requests across channels.
Subcommands: Subcommands:
- `pairing list <provider> [--json]` - `pairing list <channel> [--json]`
- `pairing approve <provider> <code> [--notify]` - `pairing approve <channel> <code> [--notify]`
### `hooks gmail` ### `hooks gmail`
Gmail Pub/Sub hook setup + runner. See [/automation/gmail-pubsub](/automation/gmail-pubsub). Gmail Pub/Sub hook setup + runner. See [/automation/gmail-pubsub](/automation/gmail-pubsub).
@ -370,7 +370,7 @@ Options:
## Messaging + agent ## Messaging + agent
### `message` ### `message`
Unified outbound messaging + provider actions. Unified outbound messaging + channel actions.
See: [/cli/message](/cli/message) See: [/cli/message](/cli/message)
@ -423,11 +423,11 @@ Options:
- `--workspace <dir>` - `--workspace <dir>`
- `--model <id>` - `--model <id>`
- `--agent-dir <dir>` - `--agent-dir <dir>`
- `--bind <provider[:accountId]>` (repeatable) - `--bind <channel[:accountId]>` (repeatable)
- `--non-interactive` - `--non-interactive`
- `--json` - `--json`
Binding specs use `provider[:accountId]`. When `accountId` is omitted for WhatsApp, the default account id is used. Binding specs use `channel[:accountId]`. When `accountId` is omitted for WhatsApp, the default account id is used.
#### `agents delete <id>` #### `agents delete <id>`
Delete an agent and prune its workspace + state. Delete an agent and prune its workspace + state.
@ -442,7 +442,7 @@ Show linked session health and recent recipients.
Options: Options:
- `--json` - `--json`
- `--all` (full diagnosis; read-only, pasteable) - `--all` (full diagnosis; read-only, pasteable)
- `--deep` (probe providers) - `--deep` (probe channels)
- `--usage` (show provider usage/quota) - `--usage` (show provider usage/quota)
- `--timeout <ms>` - `--timeout <ms>`
- `--verbose` - `--verbose`

View File

@ -43,82 +43,82 @@ Target formats (`--to`):
### Core ### Core
- `send` - `send`
- Providers: WhatsApp/Telegram/Discord/Slack/Signal/iMessage/MS Teams - Channels: WhatsApp/Telegram/Discord/Slack/Signal/iMessage/MS Teams
- Required: `--to`, `--message` - Required: `--to`, `--message`
- Optional: `--media`, `--reply-to`, `--thread-id`, `--gif-playback` - Optional: `--media`, `--reply-to`, `--thread-id`, `--gif-playback`
- Telegram only: `--buttons` (requires `"inlineButtons"` in `telegram.capabilities` or `telegram.accounts.<id>.capabilities`) - Telegram only: `--buttons` (requires `"inlineButtons"` in `channels.telegram.capabilities` or `channels.telegram.accounts.<id>.capabilities`)
- Telegram only: `--thread-id` (forum topic id) - Telegram only: `--thread-id` (forum topic id)
- Slack only: `--thread-id` (thread timestamp; `--reply-to` uses the same field) - Slack only: `--thread-id` (thread timestamp; `--reply-to` uses the same field)
- WhatsApp only: `--gif-playback` - WhatsApp only: `--gif-playback`
- `poll` - `poll`
- Providers: WhatsApp/Discord/MS Teams - Channels: WhatsApp/Discord/MS Teams
- Required: `--to`, `--poll-question`, `--poll-option` (repeat) - Required: `--to`, `--poll-question`, `--poll-option` (repeat)
- Optional: `--poll-multi` - Optional: `--poll-multi`
- Discord only: `--poll-duration-hours`, `--message` - Discord only: `--poll-duration-hours`, `--message`
- `react` - `react`
- Providers: Discord/Slack/Telegram/WhatsApp - Channels: Discord/Slack/Telegram/WhatsApp
- Required: `--message-id`, `--to` or `--channel-id` - Required: `--message-id`, `--to` or `--channel-id`
- Optional: `--emoji`, `--remove`, `--participant`, `--from-me`, `--channel-id` - Optional: `--emoji`, `--remove`, `--participant`, `--from-me`, `--channel-id`
- Note: `--remove` requires `--emoji` (omit `--emoji` to clear own reactions where supported; see /tools/reactions) - Note: `--remove` requires `--emoji` (omit `--emoji` to clear own reactions where supported; see /tools/reactions)
- WhatsApp only: `--participant`, `--from-me` - WhatsApp only: `--participant`, `--from-me`
- `reactions` - `reactions`
- Providers: Discord/Slack - Channels: Discord/Slack
- Required: `--message-id`, `--to` or `--channel-id` - Required: `--message-id`, `--to` or `--channel-id`
- Optional: `--limit`, `--channel-id` - Optional: `--limit`, `--channel-id`
- `read` - `read`
- Providers: Discord/Slack - Channels: Discord/Slack
- Required: `--to` or `--channel-id` - Required: `--to` or `--channel-id`
- Optional: `--limit`, `--before`, `--after`, `--channel-id` - Optional: `--limit`, `--before`, `--after`, `--channel-id`
- Discord only: `--around` - Discord only: `--around`
- `edit` - `edit`
- Providers: Discord/Slack - Channels: Discord/Slack
- Required: `--message-id`, `--message`, `--to` or `--channel-id` - Required: `--message-id`, `--message`, `--to` or `--channel-id`
- Optional: `--channel-id` - Optional: `--channel-id`
- `delete` - `delete`
- Providers: Discord/Slack - Channels: Discord/Slack
- Required: `--message-id`, `--to` or `--channel-id` - Required: `--message-id`, `--to` or `--channel-id`
- Optional: `--channel-id` - Optional: `--channel-id`
- `pin` / `unpin` - `pin` / `unpin`
- Providers: Discord/Slack - Channels: Discord/Slack
- Required: `--message-id`, `--to` or `--channel-id` - Required: `--message-id`, `--to` or `--channel-id`
- Optional: `--channel-id` - Optional: `--channel-id`
- `pins` (list) - `pins` (list)
- Providers: Discord/Slack - Channels: Discord/Slack
- Required: `--to` or `--channel-id` - Required: `--to` or `--channel-id`
- Optional: `--channel-id` - Optional: `--channel-id`
- `permissions` - `permissions`
- Providers: Discord - Channels: Discord
- Required: `--to` or `--channel-id` - Required: `--to` or `--channel-id`
- Optional: `--channel-id` - Optional: `--channel-id`
- `search` - `search`
- Providers: Discord - Channels: Discord
- Required: `--guild-id`, `--query` - Required: `--guild-id`, `--query`
- Optional: `--channel-id`, `--channel-ids` (repeat), `--author-id`, `--author-ids` (repeat), `--limit` - Optional: `--channel-id`, `--channel-ids` (repeat), `--author-id`, `--author-ids` (repeat), `--limit`
### Threads ### Threads
- `thread create` - `thread create`
- Providers: Discord - Channels: Discord
- Required: `--thread-name`, `--to` (channel id) or `--channel-id` - Required: `--thread-name`, `--to` (channel id) or `--channel-id`
- Optional: `--message-id`, `--auto-archive-min` - Optional: `--message-id`, `--auto-archive-min`
- `thread list` - `thread list`
- Providers: Discord - Channels: Discord
- Required: `--guild-id` - Required: `--guild-id`
- Optional: `--channel-id`, `--include-archived`, `--before`, `--limit` - Optional: `--channel-id`, `--include-archived`, `--before`, `--limit`
- `thread reply` - `thread reply`
- Providers: Discord - Channels: Discord
- Required: `--to` (thread id), `--message` - Required: `--to` (thread id), `--message`
- Optional: `--media`, `--reply-to` - Optional: `--media`, `--reply-to`
@ -129,19 +129,19 @@ Target formats (`--to`):
- Slack: no extra flags - Slack: no extra flags
- `emoji upload` - `emoji upload`
- Providers: Discord - Channels: Discord
- Required: `--guild-id`, `--emoji-name`, `--media` - Required: `--guild-id`, `--emoji-name`, `--media`
- Optional: `--role-ids` (repeat) - Optional: `--role-ids` (repeat)
### Stickers ### Stickers
- `sticker send` - `sticker send`
- Providers: Discord - Channels: Discord
- Required: `--to`, `--sticker-id` (repeat) - Required: `--to`, `--sticker-id` (repeat)
- Optional: `--message` - Optional: `--message`
- `sticker upload` - `sticker upload`
- Providers: Discord - Channels: Discord
- Required: `--guild-id`, `--sticker-name`, `--sticker-desc`, `--sticker-tags`, `--media` - Required: `--guild-id`, `--sticker-name`, `--sticker-desc`, `--sticker-tags`, `--media`
### Roles / Channels / Members / Voice ### Roles / Channels / Members / Voice

View File

@ -219,6 +219,6 @@ Suggested `.gitignore` starter:
## Advanced notes ## Advanced notes
- Multi-agent routing can use different workspaces per agent. See - Multi-agent routing can use different workspaces per agent. See
[Provider routing](/concepts/provider-routing) for routing configuration. [Channel routing](/concepts/channel-routing) for routing configuration.
- If `agents.defaults.sandbox` is enabled, non-main sessions can use per-session sandbox - If `agents.defaults.sandbox` is enabled, non-main sessions can use per-session sandbox
workspaces under `agents.defaults.sandbox.workspaceRoot`. workspaces under `agents.defaults.sandbox.workspaceRoot`.

View File

@ -102,7 +102,7 @@ More details: [Streaming + chunking](/concepts/streaming).
At minimum, set: At minimum, set:
- `agents.defaults.workspace` - `agents.defaults.workspace`
- `whatsapp.allowFrom` (strongly recommended) - `channels.whatsapp.allowFrom` (strongly recommended)
--- ---

View File

@ -1,19 +1,19 @@
--- ---
summary: "Routing rules per provider (WhatsApp, Telegram, Discord, Slack) and shared context" summary: "Routing rules per channel (WhatsApp, Telegram, Discord, Slack) and shared context"
read_when: read_when:
- Changing provider routing or inbox behavior - Changing channel routing or inbox behavior
--- ---
# Providers & routing # Channels & routing
Clawdbot routes replies **back to the provider where a message came from**. The Clawdbot routes replies **back to the channel where a message came from**. The
model does not choose a provider; routing is deterministic and controlled by the model does not choose a channel; routing is deterministic and controlled by the
host configuration. host configuration.
## Key terms ## Key terms
- **Provider**: `whatsapp`, `telegram`, `discord`, `slack`, `signal`, `imessage`, `webchat`. - **Channel**: `whatsapp`, `telegram`, `discord`, `slack`, `signal`, `imessage`, `webchat`.
- **AccountId**: perprovider account instance (when supported). - **AccountId**: perchannel account instance (when supported).
- **AgentId**: an isolated workspace + session store (“brain”). - **AgentId**: an isolated workspace + session store (“brain”).
- **SessionKey**: the bucket key used to store context and control concurrency. - **SessionKey**: the bucket key used to store context and control concurrency.
@ -23,10 +23,10 @@ Direct messages collapse to the agents **main** session:
- `agent:<agentId>:<mainKey>` (default: `agent:main:main`) - `agent:<agentId>:<mainKey>` (default: `agent:main:main`)
Groups and channels remain isolated per provider: Groups and channels remain isolated per channel:
- Groups: `agent:<agentId>:<provider>:group:<id>` - Groups: `agent:<agentId>:<channel>:group:<id>`
- Channels/rooms: `agent:<agentId>:<provider>:channel:<id>` - Channels/rooms: `agent:<agentId>:<channel>:channel:<id>`
Threads: Threads:
@ -45,8 +45,8 @@ Routing picks **one agent** for each inbound message:
1. **Exact peer match** (`bindings` with `peer.kind` + `peer.id`). 1. **Exact peer match** (`bindings` with `peer.kind` + `peer.id`).
2. **Guild match** (Discord) via `guildId`. 2. **Guild match** (Discord) via `guildId`.
3. **Team match** (Slack) via `teamId`. 3. **Team match** (Slack) via `teamId`.
4. **Account match** (`accountId` on the provider). 4. **Account match** (`accountId` on the channel).
5. **Provider match** (any account on that provider). 5. **Channel match** (any account on that channel).
6. **Default agent** (`agents.list[].default`, else first list entry, fallback to `main`). 6. **Default agent** (`agents.list[].default`, else first list entry, fallback to `main`).
The matched agent determines which workspace and session store are used. The matched agent determines which workspace and session store are used.
@ -72,7 +72,7 @@ See: [Broadcast Groups](/broadcast-groups).
## Config overview ## Config overview
- `agents.list`: named agent definitions (workspace, model, etc.). - `agents.list`: named agent definitions (workspace, model, etc.).
- `bindings`: map inbound providers/accounts/peers to agents. - `bindings`: map inbound channels/accounts/peers to agents.
Example: Example:
@ -84,8 +84,8 @@ Example:
] ]
}, },
bindings: [ bindings: [
{ match: { provider: "slack", teamId: "T123" }, agentId: "support" }, { match: { channel: "slack", teamId: "T123" }, agentId: "support" },
{ match: { provider: "telegram", peer: { kind: "group", id: "-100123" } }, agentId: "support" } { match: { channel: "telegram", peer: { kind: "group", id: "-100123" } }, agentId: "support" }
] ]
} }
``` ```
@ -102,7 +102,7 @@ You can override the store path via `session.store` and `{agentId}` templating.
## WebChat behavior ## WebChat behavior
WebChat attaches to the **selected agent** and defaults to the agents main WebChat attaches to the **selected agent** and defaults to the agents main
session. Because of this, WebChat lets you see crossprovider context for that session. Because of this, WebChat lets you see crosschannel context for that
agent in one place. agent in one place.
## Reply context ## Reply context
@ -111,4 +111,4 @@ Inbound replies include:
- `ReplyToId`, `ReplyToBody`, and `ReplyToSender` when available. - `ReplyToId`, `ReplyToBody`, and `ReplyToSender` when available.
- Quoted context is appended to `Body` as a `[Replying to ...]` block. - Quoted context is appended to `Body` as a `[Replying to ...]` block.
This is consistent across providers. This is consistent across channels.

View File

@ -10,8 +10,8 @@ Goal: let Clawd sit in WhatsApp groups, wake up only when pinged, and keep that
Note: `agents.list[].groupChat.mentionPatterns` is now used by Telegram/Discord/Slack/iMessage as well; this doc focuses on WhatsApp-specific behavior. For multi-agent setups, set `agents.list[].groupChat.mentionPatterns` per agent (or use `messages.groupChat.mentionPatterns` as a global fallback). Note: `agents.list[].groupChat.mentionPatterns` is now used by Telegram/Discord/Slack/iMessage as well; this doc focuses on WhatsApp-specific behavior. For multi-agent setups, set `agents.list[].groupChat.mentionPatterns` per agent (or use `messages.groupChat.mentionPatterns` as a global fallback).
## Whats implemented (2025-12-03) ## Whats implemented (2025-12-03)
- Activation modes: `mention` (default) or `always`. `mention` requires a ping (real WhatsApp @-mentions via `mentionedJids`, regex patterns, or the bots E.164 anywhere in the text). `always` wakes the agent on every message but it should reply only when it can add meaningful value; otherwise it returns the silent token `NO_REPLY`. Defaults can be set in config (`whatsapp.groups`) and overridden per group via `/activation`. When `whatsapp.groups` is set, it also acts as a group allowlist (include `"*"` to allow all). - Activation modes: `mention` (default) or `always`. `mention` requires a ping (real WhatsApp @-mentions via `mentionedJids`, regex patterns, or the bots E.164 anywhere in the text). `always` wakes the agent on every message but it should reply only when it can add meaningful value; otherwise it returns the silent token `NO_REPLY`. Defaults can be set in config (`channels.whatsapp.groups`) and overridden per group via `/activation`. When `channels.whatsapp.groups` is set, it also acts as a group allowlist (include `"*"` to allow all).
- Group policy: `whatsapp.groupPolicy` controls whether group messages are accepted (`open|disabled|allowlist`). `allowlist` uses `whatsapp.groupAllowFrom` (fallback: explicit `whatsapp.allowFrom`). Default is `allowlist` (blocked until you add senders). - Group policy: `channels.whatsapp.groupPolicy` controls whether group messages are accepted (`open|disabled|allowlist`). `allowlist` uses `channels.whatsapp.groupAllowFrom` (fallback: explicit `channels.whatsapp.allowFrom`). Default is `allowlist` (blocked until you add senders).
- Per-group sessions: session keys look like `agent:<agentId>:whatsapp:group:<jid>` so commands such as `/verbose on` or `/think high` (sent as standalone messages) are scoped to that group; personal DM state is untouched. Heartbeats are skipped for group threads. - Per-group sessions: session keys look like `agent:<agentId>:whatsapp:group:<jid>` so commands such as `/verbose on` or `/think high` (sent as standalone messages) are scoped to that group; personal DM state is untouched. Heartbeats are skipped for group threads.
- Context injection: last N (default 50) group messages are prefixed under `[Chat messages since your last reply - for context]`, with the triggering line under `[Current message - respond to this]`. - Context injection: last N (default 50) group messages are prefixed under `[Chat messages since your last reply - for context]`, with the triggering line under `[Current message - respond to this]`.
- Sender surfacing: every group batch now ends with `[from: Sender Name (+E164)]` so Pi knows who is speaking. - Sender surfacing: every group batch now ends with `[from: Sender Name (+E164)]` so Pi knows who is speaking.
@ -57,7 +57,7 @@ Use the group chat command:
- `/activation mention` - `/activation mention`
- `/activation always` - `/activation always`
Only the owner number (from `whatsapp.allowFrom`, or the bots own E.164 when unset) can change this. Send `/status` as a standalone message in the group to see the current activation mode. Only the owner number (from `channels.whatsapp.allowFrom`, or the bots own E.164 when unset) can change this. Send `/status` as a standalone message in the group to see the current activation mode.
## How to use ## How to use
1) Add Clawd UK (`+447700900123`) to the group. 1) Add Clawd UK (`+447700900123`) to the group.

View File

@ -55,35 +55,37 @@ Control how group/room messages are handled per provider:
```json5 ```json5
{ {
whatsapp: { channels: {
groupPolicy: "disabled", // "open" | "disabled" | "allowlist" whatsapp: {
groupAllowFrom: ["+15551234567"] groupPolicy: "disabled", // "open" | "disabled" | "allowlist"
}, groupAllowFrom: ["+15551234567"]
telegram: { },
groupPolicy: "disabled", telegram: {
groupAllowFrom: ["123456789", "@username"] groupPolicy: "disabled",
}, groupAllowFrom: ["123456789", "@username"]
signal: { },
groupPolicy: "disabled", signal: {
groupAllowFrom: ["+15551234567"] groupPolicy: "disabled",
}, groupAllowFrom: ["+15551234567"]
imessage: { },
groupPolicy: "disabled", imessage: {
groupAllowFrom: ["chat_id:123"] groupPolicy: "disabled",
}, groupAllowFrom: ["chat_id:123"]
msteams: { },
groupPolicy: "disabled", msteams: {
groupAllowFrom: ["user@org.com"] groupPolicy: "disabled",
}, groupAllowFrom: ["user@org.com"]
discord: { },
groupPolicy: "allowlist", discord: {
guilds: { groupPolicy: "allowlist",
"GUILD_ID": { channels: { help: { allow: true } } } guilds: {
"GUILD_ID": { channels: { help: { allow: true } } }
}
},
slack: {
groupPolicy: "allowlist",
channels: { "#general": { allow: true } }
} }
},
slack: {
groupPolicy: "allowlist",
channels: { "#general": { allow: true } }
} }
} }
``` ```
@ -97,9 +99,9 @@ Control how group/room messages are handled per provider:
Notes: Notes:
- `groupPolicy` is separate from mention-gating (which requires @mentions). - `groupPolicy` is separate from mention-gating (which requires @mentions).
- WhatsApp/Telegram/Signal/iMessage/Microsoft Teams: use `groupAllowFrom` (fallback: explicit `allowFrom`). - WhatsApp/Telegram/Signal/iMessage/Microsoft Teams: use `groupAllowFrom` (fallback: explicit `allowFrom`).
- Discord: allowlist uses `discord.guilds.<id>.channels`. - Discord: allowlist uses `channels.discord.guilds.<id>.channels`.
- Slack: allowlist uses `slack.channels`. - Slack: allowlist uses `channels.slack.channels`.
- Group DMs are controlled separately (`discord.dm.*`, `slack.dm.*`). - Group DMs are controlled separately (`channels.discord.dm.*`, `channels.slack.dm.*`).
- Telegram allowlist can match user IDs (`"123456789"`, `"telegram:123456789"`, `"tg:123456789"`) or usernames (`"@alice"` or `"alice"`); prefixes are case-insensitive. - Telegram allowlist can match user IDs (`"123456789"`, `"telegram:123456789"`, `"tg:123456789"`) or usernames (`"@alice"` or `"alice"`); prefixes are case-insensitive.
- Default is `groupPolicy: "allowlist"`; if your group allowlist is empty, group messages are blocked. - Default is `groupPolicy: "allowlist"`; if your group allowlist is empty, group messages are blocked.
@ -113,22 +115,24 @@ Group messages require a mention unless overridden per group. Defaults live per
```json5 ```json5
{ {
whatsapp: { channels: {
groups: { whatsapp: {
"*": { requireMention: true }, groups: {
"123@g.us": { requireMention: false } "*": { requireMention: true },
} "123@g.us": { requireMention: false }
}, }
telegram: { },
groups: { telegram: {
"*": { requireMention: true }, groups: {
"123456789": { requireMention: false } "*": { requireMention: true },
} "123456789": { requireMention: false }
}, }
imessage: { },
groups: { imessage: {
"*": { requireMention: true }, groups: {
"123": { requireMention: false } "*": { requireMention: true },
"123": { requireMention: false }
}
} }
}, },
agents: { agents: {
@ -150,28 +154,30 @@ Notes:
- Surfaces that provide explicit mentions still pass; patterns are a fallback. - Surfaces that provide explicit mentions still pass; patterns are a fallback.
- Per-agent override: `agents.list[].groupChat.mentionPatterns` (useful when multiple agents share a group). - Per-agent override: `agents.list[].groupChat.mentionPatterns` (useful when multiple agents share a group).
- Mention gating is only enforced when mention detection is possible (native mentions or `mentionPatterns` are configured). - Mention gating is only enforced when mention detection is possible (native mentions or `mentionPatterns` are configured).
- Discord defaults live in `discord.guilds."*"` (overridable per guild/channel). - Discord defaults live in `channels.discord.guilds."*"` (overridable per guild/channel).
- Group history context is wrapped uniformly across providers; use `messages.groupChat.historyLimit` for the global default and `<provider>.historyLimit` (or `<provider>.accounts.*.historyLimit`) for overrides. Set `0` to disable. - Group history context is wrapped uniformly across providers; use `messages.groupChat.historyLimit` for the global default and `<provider>.historyLimit` (or `<provider>.accounts.*.historyLimit`) for overrides. Set `0` to disable.
## Group allowlists ## Group allowlists
When `whatsapp.groups`, `telegram.groups`, or `imessage.groups` is configured, the keys act as a group allowlist. Use `"*"` to allow all groups while still setting default mention behavior. When `channels.whatsapp.groups`, `channels.telegram.groups`, or `channels.imessage.groups` is configured, the keys act as a group allowlist. Use `"*"` to allow all groups while still setting default mention behavior.
Common intents (copy/paste): Common intents (copy/paste):
1) Disable all group replies 1) Disable all group replies
```json5 ```json5
{ {
whatsapp: { groupPolicy: "disabled" } channels: { whatsapp: { groupPolicy: "disabled" } }
} }
``` ```
2) Allow only specific groups (WhatsApp) 2) Allow only specific groups (WhatsApp)
```json5 ```json5
{ {
whatsapp: { channels: {
groups: { whatsapp: {
"123@g.us": { requireMention: true }, groups: {
"456@g.us": { requireMention: false } "123@g.us": { requireMention: true },
"456@g.us": { requireMention: false }
}
} }
} }
} }
@ -180,8 +186,10 @@ Common intents (copy/paste):
3) Allow all groups but require mention (explicit) 3) Allow all groups but require mention (explicit)
```json5 ```json5
{ {
whatsapp: { channels: {
groups: { "*": { requireMention: true } } whatsapp: {
groups: { "*": { requireMention: true } }
}
} }
} }
``` ```
@ -189,10 +197,12 @@ Common intents (copy/paste):
4) Only the owner can trigger in groups (WhatsApp) 4) Only the owner can trigger in groups (WhatsApp)
```json5 ```json5
{ {
whatsapp: { channels: {
groupPolicy: "allowlist", whatsapp: {
groupAllowFrom: ["+15551234567"], groupPolicy: "allowlist",
groups: { "*": { requireMention: true } } groupAllowFrom: ["+15551234567"],
groups: { "*": { requireMention: true } }
}
} }
} }
``` ```
@ -202,7 +212,7 @@ Group owners can toggle per-group activation:
- `/activation mention` - `/activation mention`
- `/activation always` - `/activation always`
Owner is determined by `whatsapp.allowFrom` (or the bots self E.164 when unset). Send the command as a standalone message. Other surfaces currently ignore `/activation`. Owner is determined by `channels.whatsapp.allowFrom` (or the bots self E.164 when unset). Send the command as a standalone message. Other surfaces currently ignore `/activation`.
## Context fields ## Context fields
Group inbound payloads set: Group inbound payloads set:

View File

@ -23,7 +23,7 @@ Inbound message
Key knobs live in configuration: Key knobs live in configuration:
- `messages.*` for prefixes, queueing, and group behavior. - `messages.*` for prefixes, queueing, and group behavior.
- `agents.defaults.*` for block streaming and chunking defaults. - `agents.defaults.*` for block streaming and chunking defaults.
- Provider overrides (`whatsapp.*`, `telegram.*`, etc.) for caps and streaming toggles. - Provider overrides (`channels.whatsapp.*`, `channels.telegram.*`, etc.) for caps and streaming toggles.
See [Configuration](/gateway/configuration) for full schema. See [Configuration](/gateway/configuration) for full schema.
@ -63,8 +63,8 @@ Directive stripping only applies to the **current message** section so history
remains intact. Providers that wrap history should set `CommandBody` (or remains intact. Providers that wrap history should set `CommandBody` (or
`RawBody`) to the original message text and keep `Body` as the combined prompt. `RawBody`) to the original message text and keep `Body` as the combined prompt.
History buffers are configurable via `messages.groupChat.historyLimit` (global History buffers are configurable via `messages.groupChat.historyLimit` (global
default) and per-provider overrides like `slack.historyLimit` or default) and per-provider overrides like `channels.slack.historyLimit` or
`telegram.accounts.<id>.historyLimit` (set `0` to disable). `channels.telegram.accounts.<id>.historyLimit` (set `0` to disable).
## Queueing and followups ## Queueing and followups
@ -103,7 +103,7 @@ Details: [Thinking + reasoning directives](/tools/thinking) and [Token use](/tok
## Prefixes, threading, and replies ## Prefixes, threading, and replies
Outbound message formatting is centralized in `messages`: Outbound message formatting is centralized in `messages`:
- `messages.responsePrefix` (outbound prefix) and `whatsapp.messagePrefix` (WhatsApp inbound prefix) - `messages.responsePrefix` (outbound prefix) and `channels.whatsapp.messagePrefix` (WhatsApp inbound prefix)
- Reply threading via `replyToMode` and per-provider defaults - Reply threading via `replyToMode` and per-provider defaults
Details: [Configuration](/gateway/configuration#messages) and provider docs. Details: [Configuration](/gateway/configuration#messages) and provider docs.

View File

@ -90,9 +90,11 @@ Example:
{ agentId: "alex", match: { provider: "whatsapp", peer: { kind: "dm", id: "+15551230001" } } }, { agentId: "alex", match: { provider: "whatsapp", peer: { kind: "dm", id: "+15551230001" } } },
{ agentId: "mia", match: { provider: "whatsapp", peer: { kind: "dm", id: "+15551230002" } } } { agentId: "mia", match: { provider: "whatsapp", peer: { kind: "dm", id: "+15551230002" } } }
], ],
whatsapp: { channels: {
dmPolicy: "allowlist", whatsapp: {
allowFrom: ["+15551230001", "+15551230002"] dmPolicy: "allowlist",
allowFrom: ["+15551230001", "+15551230002"]
}
} }
} }
``` ```
@ -173,15 +175,17 @@ multiple phone numbers without mixing sessions.
}, },
}, },
whatsapp: { channels: {
accounts: { whatsapp: {
personal: { accounts: {
// Optional override. Default: ~/.clawdbot/credentials/whatsapp/personal personal: {
// authDir: "~/.clawdbot/credentials/whatsapp/personal", // Optional override. Default: ~/.clawdbot/credentials/whatsapp/personal
}, // authDir: "~/.clawdbot/credentials/whatsapp/personal",
biz: { },
// Optional override. Default: ~/.clawdbot/credentials/whatsapp/biz biz: {
// authDir: "~/.clawdbot/credentials/whatsapp/biz", // Optional override. Default: ~/.clawdbot/credentials/whatsapp/biz
// authDir: "~/.clawdbot/credentials/whatsapp/biz",
},
}, },
}, },
}, },

View File

@ -56,13 +56,13 @@ How to verify:
```bash ```bash
clawdbot models status clawdbot models status
clawdbot providers list clawdbot channels list
``` ```
Or JSON: Or JSON:
```bash ```bash
clawdbot providers list --json clawdbot channels list --json
``` ```
## OAuth exchange (how login works) ## OAuth exchange (how login works)
@ -148,7 +148,7 @@ Example (session override):
- `/model Opus@anthropic:work` - `/model Opus@anthropic:work`
How to see what profile IDs exist: How to see what profile IDs exist:
- `clawdbot providers list --json` (shows `auth[]`) - `clawdbot channels list --json` (shows `auth[]`)
Related docs: Related docs:
- [/concepts/model-failover](/concepts/model-failover) (rotation + cooldown rules) - [/concepts/model-failover](/concepts/model-failover) (rotation + cooldown rules)

View File

@ -34,20 +34,22 @@ Set retry policy per provider in `~/.clawdbot/clawdbot.json`:
```json5 ```json5
{ {
telegram: { channels: {
retry: { telegram: {
attempts: 3, retry: {
minDelayMs: 400, attempts: 3,
maxDelayMs: 30000, minDelayMs: 400,
jitter: 0.1 maxDelayMs: 30000,
} jitter: 0.1
}, }
discord: { },
retry: { discord: {
attempts: 3, retry: {
minDelayMs: 500, attempts: 3,
maxDelayMs: 30000, minDelayMs: 500,
jitter: 0.1 maxDelayMs: 30000,
jitter: 0.1
}
} }
} }
} }

View File

@ -37,8 +37,8 @@ Legend:
- `agents.defaults.blockStreamingBreak`: `"text_end"` or `"message_end"`. - `agents.defaults.blockStreamingBreak`: `"text_end"` or `"message_end"`.
- `agents.defaults.blockStreamingChunk`: `{ minChars, maxChars, breakPreference? }`. - `agents.defaults.blockStreamingChunk`: `{ minChars, maxChars, breakPreference? }`.
- `agents.defaults.blockStreamingCoalesce`: `{ minChars?, maxChars?, idleMs? }` (merge streamed blocks before send). - `agents.defaults.blockStreamingCoalesce`: `{ minChars?, maxChars?, idleMs? }` (merge streamed blocks before send).
- Provider hard cap: `*.textChunkLimit` (e.g., `whatsapp.textChunkLimit`). - Provider hard cap: `*.textChunkLimit` (e.g., `channels.whatsapp.textChunkLimit`).
- Discord soft cap: `discord.maxLinesPerMessage` (default 17) splits tall replies to avoid UI clipping. - Discord soft cap: `channels.discord.maxLinesPerMessage` (default 17) splits tall replies to avoid UI clipping.
**Boundary semantics:** **Boundary semantics:**
- `text_end`: stream blocks as soon as chunker emits; flush on each `text_end`. - `text_end`: stream blocks as soon as chunker emits; flush on each `text_end`.
@ -90,7 +90,7 @@ This maps to:
**Provider note:** For non-Telegram providers, block streaming is **off unless** **Provider note:** For non-Telegram providers, block streaming is **off unless**
`*.blockStreaming` is explicitly set to `true`. Telegram can stream drafts `*.blockStreaming` is explicitly set to `true`. Telegram can stream drafts
(`telegram.streamMode`) without block replies. (`channels.telegram.streamMode`) without block replies.
Config location reminder: the `blockStreaming*` defaults live under Config location reminder: the `blockStreaming*` defaults live under
`agents.defaults`, not the root config. `agents.defaults`, not the root config.
@ -99,11 +99,11 @@ Config location reminder: the `blockStreaming*` defaults live under
Telegram is the only provider with draft streaming: Telegram is the only provider with draft streaming:
- Uses Bot API `sendMessageDraft` in **private chats with topics**. - Uses Bot API `sendMessageDraft` in **private chats with topics**.
- `telegram.streamMode: "partial" | "block" | "off"`. - `channels.telegram.streamMode: "partial" | "block" | "off"`.
- `partial`: draft updates with the latest stream text. - `partial`: draft updates with the latest stream text.
- `block`: draft updates in chunked blocks (same chunker rules). - `block`: draft updates in chunked blocks (same chunker rules).
- `off`: no draft streaming. - `off`: no draft streaming.
- Draft chunk config (only for `streamMode: "block"`): `telegram.draftChunk` (defaults: `minChars: 200`, `maxChars: 800`). - Draft chunk config (only for `streamMode: "block"`): `channels.telegram.draftChunk` (defaults: `minChars: 200`, `maxChars: 800`).
- Draft streaming is separate from block streaming; block replies are off by default and only enabled by `*.blockStreaming: true` on non-Telegram providers. - Draft streaming is separate from block streaming; block replies are off by default and only enabled by `*.blockStreaming: true` on non-Telegram providers.
- Final reply is still a normal message. - Final reply is still a normal message.
- `/reasoning stream` writes reasoning into the draft bubble (Telegram only). - `/reasoning stream` writes reasoning into the draft bubble (Telegram only).

View File

@ -21,7 +21,7 @@ The timestamp in the envelope is **always UTC**, with minutes precision.
## Tool payloads (raw provider data) ## Tool payloads (raw provider data)
Tool calls (`discord.readMessages`, `slack.readMessages`, etc.) return **raw provider timestamps**. Tool calls (`channels.discord.readMessages`, `channels.slack.readMessages`, etc.) return **raw provider timestamps**.
These are typically UTC ISO strings (Discord) or UTC epoch strings (Slack). We do not rewrite them. These are typically UTC ISO strings (Discord) or UTC epoch strings (Slack). We do not rewrite them.
## User timezone for the system prompt ## User timezone for the system prompt

View File

@ -14,7 +14,7 @@ read_when:
- `/status` in chats: emojirich status card with session tokens + estimated cost (API key only) and provider quota windows when available. - `/status` in chats: emojirich status card with session tokens + estimated cost (API key only) and provider quota windows when available.
- `/cost on|off` in chats: toggles perresponse usage lines (OAuth shows tokens only). - `/cost on|off` in chats: toggles perresponse usage lines (OAuth shows tokens only).
- CLI: `clawdbot status --usage` prints a full per-provider breakdown. - CLI: `clawdbot status --usage` prints a full per-provider breakdown.
- CLI: `clawdbot providers list` prints the same usage snapshot alongside provider config (use `--no-usage` to skip). - CLI: `clawdbot channels list` prints the same usage snapshot alongside provider config (use `--no-usage` to skip).
- macOS menu bar: “Usage” section under Context (only if available). - macOS menu bar: “Usage” section under Context (only if available).
## Providers + credentials ## Providers + credentials

View File

@ -117,6 +117,86 @@
"source": "/message/", "source": "/message/",
"destination": "/cli/message" "destination": "/cli/message"
}, },
{
"source": "/providers/discord",
"destination": "/channels/discord"
},
{
"source": "/providers/discord/",
"destination": "/channels/discord"
},
{
"source": "/providers/grammy",
"destination": "/channels/grammy"
},
{
"source": "/providers/grammy/",
"destination": "/channels/grammy"
},
{
"source": "/providers/imessage",
"destination": "/channels/imessage"
},
{
"source": "/providers/imessage/",
"destination": "/channels/imessage"
},
{
"source": "/providers/location",
"destination": "/channels/location"
},
{
"source": "/providers/location/",
"destination": "/channels/location"
},
{
"source": "/providers/msteams",
"destination": "/channels/msteams"
},
{
"source": "/providers/msteams/",
"destination": "/channels/msteams"
},
{
"source": "/providers/signal",
"destination": "/channels/signal"
},
{
"source": "/providers/signal/",
"destination": "/channels/signal"
},
{
"source": "/providers/slack",
"destination": "/channels/slack"
},
{
"source": "/providers/slack/",
"destination": "/channels/slack"
},
{
"source": "/providers/telegram",
"destination": "/channels/telegram"
},
{
"source": "/providers/telegram/",
"destination": "/channels/telegram"
},
{
"source": "/providers/troubleshooting",
"destination": "/channels/troubleshooting"
},
{
"source": "/providers/troubleshooting/",
"destination": "/channels/troubleshooting"
},
{
"source": "/providers/whatsapp",
"destination": "/channels/whatsapp"
},
{
"source": "/providers/whatsapp/",
"destination": "/channels/whatsapp"
},
{ {
"source": "/sandbox", "source": "/sandbox",
"destination": "/cli/sandbox" "destination": "/cli/sandbox"
@ -231,7 +311,7 @@
}, },
{ {
"source": "/discord", "source": "/discord",
"destination": "/providers/discord" "destination": "/channels/discord"
}, },
{ {
"source": "/discovery", "source": "/discovery",
@ -267,7 +347,7 @@
}, },
{ {
"source": "/grammy", "source": "/grammy",
"destination": "/providers/grammy" "destination": "/channels/grammy"
}, },
{ {
"source": "/group-messages", "source": "/group-messages",
@ -295,7 +375,7 @@
}, },
{ {
"source": "/imessage", "source": "/imessage",
"destination": "/providers/imessage" "destination": "/channels/imessage"
}, },
{ {
"source": "/ios", "source": "/ios",
@ -307,7 +387,7 @@
}, },
{ {
"source": "/location", "source": "/location",
"destination": "/providers/location" "destination": "/channels/location"
}, },
{ {
"source": "/location-command", "source": "/location-command",
@ -451,7 +531,15 @@
}, },
{ {
"source": "/provider-routing", "source": "/provider-routing",
"destination": "/concepts/provider-routing" "destination": "/concepts/channel-routing"
},
{
"source": "/concepts/provider-routing",
"destination": "/concepts/channel-routing"
},
{
"source": "/concepts/provider-routing/",
"destination": "/concepts/channel-routing"
}, },
{ {
"source": "/queue", "source": "/queue",
@ -499,7 +587,7 @@
}, },
{ {
"source": "/signal", "source": "/signal",
"destination": "/providers/signal" "destination": "/channels/signal"
}, },
{ {
"source": "/skills", "source": "/skills",
@ -511,7 +599,7 @@
}, },
{ {
"source": "/slack", "source": "/slack",
"destination": "/providers/slack" "destination": "/channels/slack"
}, },
{ {
"source": "/slash-commands", "source": "/slash-commands",
@ -531,7 +619,7 @@
}, },
{ {
"source": "/telegram", "source": "/telegram",
"destination": "/providers/telegram" "destination": "/channels/telegram"
}, },
{ {
"source": "/templates/AGENTS", "source": "/templates/AGENTS",
@ -603,7 +691,7 @@
}, },
{ {
"source": "/whatsapp", "source": "/whatsapp",
"destination": "/providers/whatsapp" "destination": "/channels/whatsapp"
}, },
{ {
"source": "/windows", "source": "/windows",
@ -680,7 +768,7 @@
"concepts/sessions", "concepts/sessions",
"concepts/session-tool", "concepts/session-tool",
"concepts/presence", "concepts/presence",
"concepts/provider-routing", "concepts/channel-routing",
"concepts/messages", "concepts/messages",
"concepts/streaming", "concepts/streaming",
"concepts/groups", "concepts/groups",
@ -736,9 +824,28 @@
"tui" "tui"
] ]
}, },
{
"group": "Channels",
"pages": [
"channels/index",
"channels/whatsapp",
"channels/telegram",
"channels/grammy",
"channels/discord",
"channels/slack",
"channels/signal",
"channels/imessage",
"channels/msteams",
"broadcast-groups",
"channels/troubleshooting",
"channels/location"
]
},
{ {
"group": "Providers", "group": "Providers",
"pages": [ "pages": [
"providers/index",
"providers/models",
"providers/openai", "providers/openai",
"providers/anthropic", "providers/anthropic",
"providers/moonshot", "providers/moonshot",
@ -746,18 +853,7 @@
"providers/openrouter", "providers/openrouter",
"providers/opencode", "providers/opencode",
"providers/glm", "providers/glm",
"providers/zai", "providers/zai"
"providers/telegram",
"providers/grammy",
"providers/discord",
"providers/slack",
"providers/signal",
"providers/imessage",
"providers/whatsapp",
"broadcast-groups",
"providers/msteams",
"providers/troubleshooting",
"providers/location"
] ]
}, },
{ {

View File

@ -35,4 +35,4 @@ false negatives when deciding whether to respond in DMs or groups.
## Related docs ## Related docs
- [Group Chats](/concepts/groups) - [Group Chats](/concepts/groups)
- [Telegram Provider](/providers/telegram) - [Telegram Provider](/channels/telegram)

View File

@ -15,7 +15,7 @@ Examples below are aligned with the current config schema. For the exhaustive re
```json5 ```json5
{ {
agent: { workspace: "~/clawd" }, agent: { workspace: "~/clawd" },
whatsapp: { allowFrom: ["+15555550123"] } channels: { whatsapp: { allowFrom: ["+15555550123"] } }
} }
``` ```
@ -33,9 +33,11 @@ Save to `~/.clawdbot/clawdbot.json` and you can DM the bot from that number.
workspace: "~/clawd", workspace: "~/clawd",
model: { primary: "anthropic/claude-sonnet-4-5" } model: { primary: "anthropic/claude-sonnet-4-5" }
}, },
whatsapp: { channels: {
allowFrom: ["+15555550123"], whatsapp: {
groups: { "*": { requireMention: true } } allowFrom: ["+15555550123"],
groups: { "*": { requireMention: true } }
}
} }
} }
``` ```
@ -147,52 +149,54 @@ Save to `~/.clawdbot/clawdbot.json` and you can DM the bot from that number.
}, },
// Providers // Providers
whatsapp: { channels: {
dmPolicy: "pairing", whatsapp: {
allowFrom: ["+15555550123"], dmPolicy: "pairing",
groupPolicy: "allowlist", allowFrom: ["+15555550123"],
groupAllowFrom: ["+15555550123"], groupPolicy: "allowlist",
groups: { "*": { requireMention: true } } groupAllowFrom: ["+15555550123"],
}, groups: { "*": { requireMention: true } }
},
telegram: { telegram: {
enabled: true, enabled: true,
botToken: "YOUR_TELEGRAM_BOT_TOKEN", botToken: "YOUR_TELEGRAM_BOT_TOKEN",
allowFrom: ["123456789"], allowFrom: ["123456789"],
groupPolicy: "allowlist", groupPolicy: "allowlist",
groupAllowFrom: ["123456789"], groupAllowFrom: ["123456789"],
groups: { "*": { requireMention: true } } groups: { "*": { requireMention: true } }
}, },
discord: { discord: {
enabled: true, enabled: true,
token: "YOUR_DISCORD_BOT_TOKEN", token: "YOUR_DISCORD_BOT_TOKEN",
dm: { enabled: true, allowFrom: ["steipete"] }, dm: { enabled: true, allowFrom: ["steipete"] },
guilds: { guilds: {
"123456789012345678": { "123456789012345678": {
slug: "friends-of-clawd", slug: "friends-of-clawd",
requireMention: false, requireMention: false,
channels: { channels: {
general: { allow: true }, general: { allow: true },
help: { allow: true, requireMention: true } help: { allow: true, requireMention: true }
}
} }
} }
}
},
slack: {
enabled: true,
botToken: "xoxb-REPLACE_ME",
appToken: "xapp-REPLACE_ME",
channels: {
"#general": { allow: true, requireMention: true }
}, },
dm: { enabled: true, allowFrom: ["U123"] },
slashCommand: { slack: {
enabled: true, enabled: true,
name: "clawd", botToken: "xoxb-REPLACE_ME",
sessionPrefix: "slack:slash", appToken: "xapp-REPLACE_ME",
ephemeral: true channels: {
"#general": { allow: true, requireMention: true }
},
dm: { enabled: true, allowFrom: ["U123"] },
slashCommand: {
enabled: true,
name: "clawd",
sessionPrefix: "slack:slash",
ephemeral: true
}
} }
}, },
@ -406,16 +410,18 @@ Save to `~/.clawdbot/clawdbot.json` and you can DM the bot from that number.
```json5 ```json5
{ {
agent: { workspace: "~/clawd" }, agent: { workspace: "~/clawd" },
whatsapp: { allowFrom: ["+15555550123"] }, channels: {
telegram: { whatsapp: { allowFrom: ["+15555550123"] },
enabled: true, telegram: {
botToken: "YOUR_TOKEN", enabled: true,
allowFrom: ["123456789"] botToken: "YOUR_TOKEN",
}, allowFrom: ["123456789"]
discord: { },
enabled: true, discord: {
token: "YOUR_TOKEN", enabled: true,
dm: { allowFrom: ["yourname"] } token: "YOUR_TOKEN",
dm: { allowFrom: ["yourname"] }
}
} }
} }
``` ```
@ -460,12 +466,14 @@ Save to `~/.clawdbot/clawdbot.json` and you can DM the bot from that number.
workspace: "~/work-clawd", workspace: "~/work-clawd",
elevated: { enabled: false } elevated: { enabled: false }
}, },
slack: { channels: {
enabled: true, slack: {
botToken: "xoxb-...", enabled: true,
channels: { botToken: "xoxb-...",
"#engineering": { allow: true, requireMention: true }, channels: {
"#general": { allow: true, requireMention: true } "#engineering": { allow: true, requireMention: true },
"#general": { allow: true, requireMention: true }
}
} }
} }
} }
@ -507,4 +515,4 @@ Save to `~/.clawdbot/clawdbot.json` and you can DM the bot from that number.
- If you set `dmPolicy: "open"`, the matching `allowFrom` list must include `"*"`. - If you set `dmPolicy: "open"`, the matching `allowFrom` list must include `"*"`.
- Provider IDs differ (phone numbers, user IDs, channel IDs). Use the provider docs to confirm the format. - Provider IDs differ (phone numbers, user IDs, channel IDs). Use the provider docs to confirm the format.
- Optional sections to add later: `web`, `browser`, `ui`, `bridge`, `discovery`, `canvasHost`, `talk`, `signal`, `imessage`. - Optional sections to add later: `web`, `browser`, `ui`, `bridge`, `discovery`, `canvasHost`, `talk`, `signal`, `imessage`.
- See [Providers](/providers/whatsapp) and [Troubleshooting](/gateway/troubleshooting) for deeper setup notes. - See [Providers](/channels/whatsapp) and [Troubleshooting](/gateway/troubleshooting) for deeper setup notes.

View File

@ -8,8 +8,8 @@ read_when:
Clawdbot reads an optional **JSON5** config from `~/.clawdbot/clawdbot.json` (comments + trailing commas allowed). Clawdbot reads an optional **JSON5** config from `~/.clawdbot/clawdbot.json` (comments + trailing commas allowed).
If the file is missing, Clawdbot uses safe-ish defaults (embedded Pi agent + per-sender sessions + workspace `~/clawd`). You usually only need a config to: If the file is missing, Clawdbot uses safe-ish defaults (embedded Pi agent + per-sender sessions + workspace `~/clawd`). You usually only need a config to:
- restrict who can trigger the bot (`whatsapp.allowFrom`, `telegram.allowFrom`, etc.) - restrict who can trigger the bot (`channels.whatsapp.allowFrom`, `channels.telegram.allowFrom`, etc.)
- control group allowlists + mention behavior (`whatsapp.groups`, `telegram.groups`, `discord.guilds`, `agents.list[].groupChat`) - control group allowlists + mention behavior (`channels.whatsapp.groups`, `channels.telegram.groups`, `channels.discord.guilds`, `agents.list[].groupChat`)
- customize message prefixes (`messages`) - customize message prefixes (`messages`)
- set the agent's workspace (`agents.defaults.workspace` or `agents.list[].workspace`) - set the agent's workspace (`agents.defaults.workspace` or `agents.list[].workspace`)
- tune the embedded agent defaults (`agents.defaults`) and session behavior (`session`) - tune the embedded agent defaults (`agents.defaults`) and session behavior (`session`)
@ -50,7 +50,7 @@ clawdbot gateway call config.apply --params '{
```json5 ```json5
{ {
agents: { defaults: { workspace: "~/clawd" } }, agents: { defaults: { workspace: "~/clawd" } },
whatsapp: { allowFrom: ["+15555550123"] } channels: { whatsapp: { allowFrom: ["+15555550123"] } }
} }
``` ```
@ -74,10 +74,12 @@ To prevent the bot from responding to WhatsApp @-mentions in groups (only respon
} }
] ]
}, },
whatsapp: { channels: {
// Allowlist is DMs only; including your own number enables self-chat mode. whatsapp: {
allowFrom: ["+15555550123"], // Allowlist is DMs only; including your own number enables self-chat mode.
groups: { "*": { requireMention: true } } allowFrom: ["+15555550123"],
groups: { "*": { requireMention: true } }
}
} }
} }
``` ```
@ -189,7 +191,7 @@ Included files can themselves contain `$include` directives (up to 10 levels dee
"./clients/schmidt/broadcast.json5" "./clients/schmidt/broadcast.json5"
]}, ]},
whatsapp: { groupPolicy: "allowlist" } channels: { whatsapp: { groupPolicy: "allowlist" } }
} }
``` ```
@ -366,12 +368,12 @@ Metadata written by CLI wizards (`onboard`, `configure`, `doctor`).
} }
``` ```
### `whatsapp.dmPolicy` ### `channels.whatsapp.dmPolicy`
Controls how WhatsApp direct chats (DMs) are handled: Controls how WhatsApp direct chats (DMs) are handled:
- `"pairing"` (default): unknown senders get a pairing code; owner must approve - `"pairing"` (default): unknown senders get a pairing code; owner must approve
- `"allowlist"`: only allow senders in `whatsapp.allowFrom` (or paired allow store) - `"allowlist"`: only allow senders in `channels.whatsapp.allowFrom` (or paired allow store)
- `"open"`: allow all inbound DMs (**requires** `whatsapp.allowFrom` to include `"*"`) - `"open"`: allow all inbound DMs (**requires** `channels.whatsapp.allowFrom` to include `"*"`)
- `"disabled"`: ignore all inbound DMs - `"disabled"`: ignore all inbound DMs
Pairing codes expire after 1 hour; the bot only sends a pairing code when a new request is created. Pending DM pairing requests are capped at **3 per provider** by default. Pairing codes expire after 1 hour; the bot only sends a pairing code when a new request is created. Pending DM pairing requests are capped at **3 per provider** by default.
@ -380,36 +382,40 @@ Pairing approvals:
- `clawdbot pairing list whatsapp` - `clawdbot pairing list whatsapp`
- `clawdbot pairing approve whatsapp <code>` - `clawdbot pairing approve whatsapp <code>`
### `whatsapp.allowFrom` ### `channels.whatsapp.allowFrom`
Allowlist of E.164 phone numbers that may trigger WhatsApp auto-replies (**DMs only**). Allowlist of E.164 phone numbers that may trigger WhatsApp auto-replies (**DMs only**).
If empty and `whatsapp.dmPolicy="pairing"`, unknown senders will receive a pairing code. If empty and `channels.whatsapp.dmPolicy="pairing"`, unknown senders will receive a pairing code.
For groups, use `whatsapp.groupPolicy` + `whatsapp.groupAllowFrom`. For groups, use `channels.whatsapp.groupPolicy` + `channels.whatsapp.groupAllowFrom`.
```json5 ```json5
{ {
whatsapp: { channels: {
dmPolicy: "pairing", // pairing | allowlist | open | disabled whatsapp: {
allowFrom: ["+15555550123", "+447700900123"], dmPolicy: "pairing", // pairing | allowlist | open | disabled
textChunkLimit: 4000, // optional outbound chunk size (chars) allowFrom: ["+15555550123", "+447700900123"],
mediaMaxMb: 50 // optional inbound media cap (MB) textChunkLimit: 4000, // optional outbound chunk size (chars)
mediaMaxMb: 50 // optional inbound media cap (MB)
}
} }
} }
``` ```
### `whatsapp.accounts` (multi-account) ### `channels.whatsapp.accounts` (multi-account)
Run multiple WhatsApp accounts in one gateway: Run multiple WhatsApp accounts in one gateway:
```json5 ```json5
{ {
whatsapp: { channels: {
accounts: { whatsapp: {
default: {}, // optional; keeps the default id stable accounts: {
personal: {}, default: {}, // optional; keeps the default id stable
biz: { personal: {},
// Optional override. Default: ~/.clawdbot/credentials/whatsapp/biz biz: {
// authDir: "~/.clawdbot/credentials/whatsapp/biz", // Optional override. Default: ~/.clawdbot/credentials/whatsapp/biz
// authDir: "~/.clawdbot/credentials/whatsapp/biz",
}
} }
} }
} }
@ -420,21 +426,23 @@ Notes:
- Outbound commands default to account `default` if present; otherwise the first configured account id (sorted). - Outbound commands default to account `default` if present; otherwise the first configured account id (sorted).
- The legacy single-account Baileys auth dir is migrated by `clawdbot doctor` into `whatsapp/default`. - The legacy single-account Baileys auth dir is migrated by `clawdbot doctor` into `whatsapp/default`.
### `telegram.accounts` / `discord.accounts` / `slack.accounts` / `signal.accounts` / `imessage.accounts` ### `channels.telegram.accounts` / `channels.discord.accounts` / `channels.slack.accounts` / `channels.signal.accounts` / `channels.imessage.accounts`
Run multiple accounts per provider (each account has its own `accountId` and optional `name`): Run multiple accounts per provider (each account has its own `accountId` and optional `name`):
```json5 ```json5
{ {
telegram: { channels: {
accounts: { telegram: {
default: { accounts: {
name: "Primary bot", default: {
botToken: "123456:ABC..." name: "Primary bot",
}, botToken: "123456:ABC..."
alerts: { },
name: "Alerts bot", alerts: {
botToken: "987654:XYZ..." name: "Alerts bot",
botToken: "987654:XYZ..."
}
} }
} }
} }
@ -452,7 +460,7 @@ Notes:
Group messages default to **require mention** (either metadata mention or regex patterns). Applies to WhatsApp, Telegram, Discord, and iMessage group chats. Group messages default to **require mention** (either metadata mention or regex patterns). Applies to WhatsApp, Telegram, Discord, and iMessage group chats.
**Mention types:** **Mention types:**
- **Metadata mentions**: Native platform @-mentions (e.g., WhatsApp tap-to-mention). Ignored in WhatsApp self-chat mode (see `whatsapp.allowFrom`). - **Metadata mentions**: Native platform @-mentions (e.g., WhatsApp tap-to-mention). Ignored in WhatsApp self-chat mode (see `channels.whatsapp.allowFrom`).
- **Text patterns**: Regex patterns defined in `agents.list[].groupChat.mentionPatterns`. Always checked regardless of self-chat mode. - **Text patterns**: Regex patterns defined in `agents.list[].groupChat.mentionPatterns`. Always checked regardless of self-chat mode.
- Mention gating is enforced only when mention detection is possible (native mentions or at least one `mentionPattern`). - Mention gating is enforced only when mention detection is possible (native mentions or at least one `mentionPattern`).
@ -469,7 +477,7 @@ Group messages default to **require mention** (either metadata mention or regex
} }
``` ```
`messages.groupChat.historyLimit` sets the global default for group history context. Providers can override with `<provider>.historyLimit` (or `<provider>.accounts.*.historyLimit` for multi-account). Set `0` to disable history wrapping. `messages.groupChat.historyLimit` sets the global default for group history context. Providers can override with `channels.<provider>.historyLimit` (or `channels.<provider>.accounts.*.historyLimit` for multi-account). Set `0` to disable history wrapping.
Per-agent override (takes precedence when set, even `[]`): Per-agent override (takes precedence when set, even `[]`):
```json5 ```json5
@ -483,15 +491,17 @@ Per-agent override (takes precedence when set, even `[]`):
} }
``` ```
Mention gating defaults live per provider (`whatsapp.groups`, `telegram.groups`, `imessage.groups`, `discord.guilds`). When `*.groups` is set, it also acts as a group allowlist; include `"*"` to allow all groups. Mention gating defaults live per provider (`channels.whatsapp.groups`, `channels.telegram.groups`, `channels.imessage.groups`, `channels.discord.guilds`). When `*.groups` is set, it also acts as a group allowlist; include `"*"` to allow all groups.
To respond **only** to specific text triggers (ignoring native @-mentions): To respond **only** to specific text triggers (ignoring native @-mentions):
```json5 ```json5
{ {
whatsapp: { channels: {
// Include your own number to enable self-chat mode (ignore native @-mentions). whatsapp: {
allowFrom: ["+15555550123"], // Include your own number to enable self-chat mode (ignore native @-mentions).
groups: { "*": { requireMention: true } } allowFrom: ["+15555550123"],
groups: { "*": { requireMention: true } }
}
}, },
agents: { agents: {
list: [ list: [
@ -509,41 +519,43 @@ To respond **only** to specific text triggers (ignoring native @-mentions):
### Group policy (per provider) ### Group policy (per provider)
Use `*.groupPolicy` to control whether group/room messages are accepted at all: Use `channels.*.groupPolicy` to control whether group/room messages are accepted at all:
```json5 ```json5
{ {
whatsapp: { channels: {
groupPolicy: "allowlist", whatsapp: {
groupAllowFrom: ["+15551234567"] groupPolicy: "allowlist",
}, groupAllowFrom: ["+15551234567"]
telegram: { },
groupPolicy: "allowlist", telegram: {
groupAllowFrom: ["tg:123456789", "@alice"] groupPolicy: "allowlist",
}, groupAllowFrom: ["tg:123456789", "@alice"]
signal: { },
groupPolicy: "allowlist", signal: {
groupAllowFrom: ["+15551234567"] groupPolicy: "allowlist",
}, groupAllowFrom: ["+15551234567"]
imessage: { },
groupPolicy: "allowlist", imessage: {
groupAllowFrom: ["chat_id:123"] groupPolicy: "allowlist",
}, groupAllowFrom: ["chat_id:123"]
msteams: { },
groupPolicy: "allowlist", msteams: {
groupAllowFrom: ["user@org.com"] groupPolicy: "allowlist",
}, groupAllowFrom: ["user@org.com"]
discord: { },
groupPolicy: "allowlist", discord: {
guilds: { groupPolicy: "allowlist",
"GUILD_ID": { guilds: {
channels: { help: { allow: true } } "GUILD_ID": {
channels: { help: { allow: true } }
}
} }
},
slack: {
groupPolicy: "allowlist",
channels: { "#general": { allow: true } }
} }
},
slack: {
groupPolicy: "allowlist",
channels: { "#general": { allow: true } }
} }
} }
``` ```
@ -553,7 +565,7 @@ Notes:
- `"disabled"`: block all group/room messages. - `"disabled"`: block all group/room messages.
- `"allowlist"`: only allow groups/rooms that match the configured allowlist. - `"allowlist"`: only allow groups/rooms that match the configured allowlist.
- WhatsApp/Telegram/Signal/iMessage/Microsoft Teams use `groupAllowFrom` (fallback: explicit `allowFrom`). - WhatsApp/Telegram/Signal/iMessage/Microsoft Teams use `groupAllowFrom` (fallback: explicit `allowFrom`).
- Discord/Slack use channel allowlists (`discord.guilds.*.channels`, `slack.channels`). - Discord/Slack use channel allowlists (`channels.discord.guilds.*.channels`, `channels.slack.channels`).
- Group DMs (Discord/Slack) are still controlled by `dm.groupEnabled` + `dm.groupChannels`. - Group DMs (Discord/Slack) are still controlled by `dm.groupEnabled` + `dm.groupChannels`.
- Default is `groupPolicy: "allowlist"`; if no allowlist is configured, group messages are blocked. - Default is `groupPolicy: "allowlist"`; if no allowlist is configured, group messages are blocked.
@ -691,10 +703,12 @@ Example: two WhatsApp accounts → two agents:
{ agentId: "home", match: { provider: "whatsapp", accountId: "personal" } }, { agentId: "home", match: { provider: "whatsapp", accountId: "personal" } },
{ agentId: "work", match: { provider: "whatsapp", accountId: "biz" } } { agentId: "work", match: { provider: "whatsapp", accountId: "biz" } }
], ],
whatsapp: { channels: {
accounts: { whatsapp: {
personal: {}, accounts: {
biz: {}, personal: {},
biz: {},
}
} }
} }
} }
@ -761,9 +775,9 @@ Controls how chat commands are enabled across connectors.
Notes: Notes:
- Text commands must be sent as a **standalone** message and use the leading `/` (no plain-text aliases). - Text commands must be sent as a **standalone** message and use the leading `/` (no plain-text aliases).
- `commands.text: false` disables parsing chat messages for commands. - `commands.text: false` disables parsing chat messages for commands.
- `commands.native: "auto"` (default) turns on native commands for Discord/Telegram and leaves Slack off; unsupported providers stay text-only. - `commands.native: "auto"` (default) turns on native commands for Discord/Telegram and leaves Slack off; unsupported channels stay text-only.
- Set `commands.native: true|false` to force all, or override per provider with `discord.commands.native`, `telegram.commands.native`, `slack.commands.native` (bool or `"auto"`). `false` clears previously registered commands on Discord/Telegram at startup; Slack commands are managed in the Slack app. - Set `commands.native: true|false` to force all, or override per channel with `channels.discord.commands.native`, `channels.telegram.commands.native`, `channels.slack.commands.native` (bool or `"auto"`). `false` clears previously registered commands on Discord/Telegram at startup; Slack commands are managed in the Slack app.
- `commands.bash: true` enables `! <cmd>` to run host shell commands (`/bash <cmd>` also works as an alias). Requires `tools.elevated.enabled` and allowlisting the sender in `tools.elevated.allowFrom.<provider>`. - `commands.bash: true` enables `! <cmd>` to run host shell commands (`/bash <cmd>` also works as an alias). Requires `tools.elevated.enabled` and allowlisting the sender in `tools.elevated.allowFrom.<channel>`.
- `commands.bashForegroundMs` controls how long bash waits before backgrounding. While a bash job is running, new `! <cmd>` requests are rejected (one at a time). - `commands.bashForegroundMs` controls how long bash waits before backgrounding. While a bash job is running, new `! <cmd>` requests are rejected (one at a time).
- `commands.config: true` enables `/config` (reads/writes `clawdbot.json`). - `commands.config: true` enables `/config` (reads/writes `clawdbot.json`).
- `commands.debug: true` enables `/debug` (runtime-only overrides). - `commands.debug: true` enables `/debug` (runtime-only overrides).
@ -791,53 +805,55 @@ Set `web.enabled: false` to keep it off by default.
} }
``` ```
### `telegram` (bot transport) ### `channels.telegram` (bot transport)
Clawdbot starts Telegram only when a `telegram` config section exists. The bot token is resolved from `TELEGRAM_BOT_TOKEN` or `telegram.botToken`. Clawdbot starts Telegram only when a `channels.telegram` config section exists. The bot token is resolved from `TELEGRAM_BOT_TOKEN` or `channels.telegram.botToken`.
Set `telegram.enabled: false` to disable automatic startup. Set `channels.telegram.enabled: false` to disable automatic startup.
Multi-account support lives under `telegram.accounts` (see the multi-account section above). Env tokens only apply to the default account. Multi-account support lives under `channels.telegram.accounts` (see the multi-account section above). Env tokens only apply to the default account.
```json5 ```json5
{ {
telegram: { channels: {
enabled: true, telegram: {
botToken: "your-bot-token", enabled: true,
dmPolicy: "pairing", // pairing | allowlist | open | disabled botToken: "your-bot-token",
allowFrom: ["tg:123456789"], // optional; "open" requires ["*"] dmPolicy: "pairing", // pairing | allowlist | open | disabled
groups: { allowFrom: ["tg:123456789"], // optional; "open" requires ["*"]
"*": { requireMention: true }, groups: {
"-1001234567890": { "*": { requireMention: true },
allowFrom: ["@admin"], "-1001234567890": {
systemPrompt: "Keep answers brief.", allowFrom: ["@admin"],
topics: { systemPrompt: "Keep answers brief.",
"99": { topics: {
requireMention: false, "99": {
skills: ["search"], requireMention: false,
systemPrompt: "Stay on topic." skills: ["search"],
systemPrompt: "Stay on topic."
}
} }
} }
} },
}, historyLimit: 50, // include last N group messages as context (0 disables)
historyLimit: 50, // include last N group messages as context (0 disables) replyToMode: "first", // off | first | all
replyToMode: "first", // off | first | all streamMode: "partial", // off | partial | block (draft streaming; separate from block streaming)
streamMode: "partial", // off | partial | block (draft streaming; separate from block streaming) draftChunk: { // optional; only for streamMode=block
draftChunk: { // optional; only for streamMode=block minChars: 200,
minChars: 200, maxChars: 800,
maxChars: 800, breakPreference: "paragraph" // paragraph | newline | sentence
breakPreference: "paragraph" // paragraph | newline | sentence },
}, actions: { reactions: true, sendMessage: true }, // tool action gates (false disables)
actions: { reactions: true, sendMessage: true }, // tool action gates (false disables) mediaMaxMb: 5,
mediaMaxMb: 5, retry: { // outbound retry policy
retry: { // outbound retry policy attempts: 3,
attempts: 3, minDelayMs: 400,
minDelayMs: 400, maxDelayMs: 30000,
maxDelayMs: 30000, jitter: 0.1
jitter: 0.1 },
}, proxy: "socks5://localhost:9050",
proxy: "socks5://localhost:9050", webhookUrl: "https://example.com/telegram-webhook",
webhookUrl: "https://example.com/telegram-webhook", webhookSecret: "secret",
webhookSecret: "secret", webhookPath: "/telegram-webhook"
webhookPath: "/telegram-webhook" }
} }
} }
``` ```
@ -848,148 +864,152 @@ Draft streaming notes:
- `/reasoning stream` streams reasoning into the draft, then sends the final answer. - `/reasoning stream` streams reasoning into the draft, then sends the final answer.
Retry policy defaults and behavior are documented in [Retry policy](/concepts/retry). Retry policy defaults and behavior are documented in [Retry policy](/concepts/retry).
### `discord` (bot transport) ### `channels.discord` (bot transport)
Configure the Discord bot by setting the bot token and optional gating: Configure the Discord bot by setting the bot token and optional gating:
Multi-account support lives under `discord.accounts` (see the multi-account section above). Env tokens only apply to the default account. Multi-account support lives under `channels.discord.accounts` (see the multi-account section above). Env tokens only apply to the default account.
```json5 ```json5
{ {
discord: { channels: {
enabled: true, discord: {
token: "your-bot-token", enabled: true,
mediaMaxMb: 8, // clamp inbound media size token: "your-bot-token",
allowBots: false, // allow bot-authored messages mediaMaxMb: 8, // clamp inbound media size
actions: { // tool action gates (false disables) allowBots: false, // allow bot-authored messages
reactions: true, actions: { // tool action gates (false disables)
stickers: true, reactions: true,
polls: true, stickers: true,
permissions: true, polls: true,
messages: true, permissions: true,
threads: true, messages: true,
pins: true, threads: true,
search: true, pins: true,
memberInfo: true, search: true,
roleInfo: true, memberInfo: true,
roles: false, roleInfo: true,
channelInfo: true, roles: false,
voiceStatus: true, channelInfo: true,
events: true, voiceStatus: true,
moderation: false events: true,
}, moderation: false
replyToMode: "off", // off | first | all },
dm: { replyToMode: "off", // off | first | all
enabled: true, // disable all DMs when false dm: {
policy: "pairing", // pairing | allowlist | open | disabled enabled: true, // disable all DMs when false
allowFrom: ["1234567890", "steipete"], // optional DM allowlist ("open" requires ["*"]) policy: "pairing", // pairing | allowlist | open | disabled
groupEnabled: false, // enable group DMs allowFrom: ["1234567890", "steipete"], // optional DM allowlist ("open" requires ["*"])
groupChannels: ["clawd-dm"] // optional group DM allowlist groupEnabled: false, // enable group DMs
}, groupChannels: ["clawd-dm"] // optional group DM allowlist
guilds: { },
"123456789012345678": { // guild id (preferred) or slug guilds: {
slug: "friends-of-clawd", "123456789012345678": { // guild id (preferred) or slug
requireMention: false, // per-guild default slug: "friends-of-clawd",
reactionNotifications: "own", // off | own | all | allowlist requireMention: false, // per-guild default
users: ["987654321098765432"], // optional per-guild user allowlist reactionNotifications: "own", // off | own | all | allowlist
channels: { users: ["987654321098765432"], // optional per-guild user allowlist
general: { allow: true }, channels: {
help: { general: { allow: true },
allow: true, help: {
requireMention: true, allow: true,
users: ["987654321098765432"], requireMention: true,
skills: ["docs"], users: ["987654321098765432"],
systemPrompt: "Short answers only." skills: ["docs"],
systemPrompt: "Short answers only."
}
} }
} }
},
historyLimit: 20, // include last N guild messages as context
textChunkLimit: 2000, // optional outbound text chunk size (chars)
maxLinesPerMessage: 17, // soft max lines per message (Discord UI clipping)
retry: { // outbound retry policy
attempts: 3,
minDelayMs: 500,
maxDelayMs: 30000,
jitter: 0.1
} }
},
historyLimit: 20, // include last N guild messages as context
textChunkLimit: 2000, // optional outbound text chunk size (chars)
maxLinesPerMessage: 17, // soft max lines per message (Discord UI clipping)
retry: { // outbound retry policy
attempts: 3,
minDelayMs: 500,
maxDelayMs: 30000,
jitter: 0.1
} }
} }
} }
``` ```
Clawdbot starts Discord only when a `discord` config section exists. The token is resolved from `DISCORD_BOT_TOKEN` or `discord.token` (unless `discord.enabled` is `false`). Use `user:<id>` (DM) or `channel:<id>` (guild channel) when specifying delivery targets for cron/CLI commands; bare numeric IDs are ambiguous and rejected. Clawdbot starts Discord only when a `channels.discord` config section exists. The token is resolved from `DISCORD_BOT_TOKEN` or `channels.discord.token` (unless `channels.discord.enabled` is `false`). Use `user:<id>` (DM) or `channel:<id>` (guild channel) when specifying delivery targets for cron/CLI commands; bare numeric IDs are ambiguous and rejected.
Guild slugs are lowercase with spaces replaced by `-`; channel keys use the slugged channel name (no leading `#`). Prefer guild ids as keys to avoid rename ambiguity. Guild slugs are lowercase with spaces replaced by `-`; channel keys use the slugged channel name (no leading `#`). Prefer guild ids as keys to avoid rename ambiguity.
Bot-authored messages are ignored by default. Enable with `discord.allowBots` (own messages are still filtered to prevent self-reply loops). Bot-authored messages are ignored by default. Enable with `channels.discord.allowBots` (own messages are still filtered to prevent self-reply loops).
Reaction notification modes: Reaction notification modes:
- `off`: no reaction events. - `off`: no reaction events.
- `own`: reactions on the bot's own messages (default). - `own`: reactions on the bot's own messages (default).
- `all`: all reactions on all messages. - `all`: all reactions on all messages.
- `allowlist`: reactions from `guilds.<id>.users` on all messages (empty list disables). - `allowlist`: reactions from `guilds.<id>.users` on all messages (empty list disables).
Outbound text is chunked by `discord.textChunkLimit` (default 2000). Discord clients can clip very tall messages, so `discord.maxLinesPerMessage` (default 17) splits long multi-line replies even when under 2000 chars. Outbound text is chunked by `channels.discord.textChunkLimit` (default 2000). Discord clients can clip very tall messages, so `channels.discord.maxLinesPerMessage` (default 17) splits long multi-line replies even when under 2000 chars.
Retry policy defaults and behavior are documented in [Retry policy](/concepts/retry). Retry policy defaults and behavior are documented in [Retry policy](/concepts/retry).
### `slack` (socket mode) ### `channels.slack` (socket mode)
Slack runs in Socket Mode and requires both a bot token and app token: Slack runs in Socket Mode and requires both a bot token and app token:
```json5 ```json5
{ {
slack: { channels: {
enabled: true, slack: {
botToken: "xoxb-...",
appToken: "xapp-...",
dm: {
enabled: true, enabled: true,
policy: "pairing", // pairing | allowlist | open | disabled botToken: "xoxb-...",
allowFrom: ["U123", "U456", "*"], // optional; "open" requires ["*"] appToken: "xapp-...",
groupEnabled: false, dm: {
groupChannels: ["G123"] enabled: true,
}, policy: "pairing", // pairing | allowlist | open | disabled
channels: { allowFrom: ["U123", "U456", "*"], // optional; "open" requires ["*"]
C123: { allow: true, requireMention: true, allowBots: false }, groupEnabled: false,
"#general": { groupChannels: ["G123"]
allow: true, },
requireMention: true, channels: {
allowBots: false, C123: { allow: true, requireMention: true, allowBots: false },
users: ["U123"], "#general": {
skills: ["docs"], allow: true,
systemPrompt: "Short answers only." requireMention: true,
} allowBots: false,
}, users: ["U123"],
historyLimit: 50, // include last N channel/group messages as context (0 disables) skills: ["docs"],
allowBots: false, systemPrompt: "Short answers only."
reactionNotifications: "own", // off | own | all | allowlist }
reactionAllowlist: ["U123"], },
replyToMode: "off", // off | first | all historyLimit: 50, // include last N channel/group messages as context (0 disables)
actions: { allowBots: false,
reactions: true, reactionNotifications: "own", // off | own | all | allowlist
messages: true, reactionAllowlist: ["U123"],
pins: true, replyToMode: "off", // off | first | all
memberInfo: true, actions: {
emojiList: true reactions: true,
}, messages: true,
slashCommand: { pins: true,
enabled: true, memberInfo: true,
name: "clawd", emojiList: true
sessionPrefix: "slack:slash", },
ephemeral: true slashCommand: {
}, enabled: true,
textChunkLimit: 4000, name: "clawd",
mediaMaxMb: 20 sessionPrefix: "slack:slash",
ephemeral: true
},
textChunkLimit: 4000,
mediaMaxMb: 20
}
} }
} }
``` ```
Multi-account support lives under `slack.accounts` (see the multi-account section above). Env tokens only apply to the default account. Multi-account support lives under `channels.slack.accounts` (see the multi-account section above). Env tokens only apply to the default account.
Clawdbot starts Slack when the provider is enabled and both tokens are set (via config or `SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN`). Use `user:<id>` (DM) or `channel:<id>` when specifying delivery targets for cron/CLI commands. Clawdbot starts Slack when the provider is enabled and both tokens are set (via config or `SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN`). Use `user:<id>` (DM) or `channel:<id>` when specifying delivery targets for cron/CLI commands.
Bot-authored messages are ignored by default. Enable with `slack.allowBots` or `slack.channels.<id>.allowBots`. Bot-authored messages are ignored by default. Enable with `channels.slack.allowBots` or `channels.slack.channels.<id>.allowBots`.
Reaction notification modes: Reaction notification modes:
- `off`: no reaction events. - `off`: no reaction events.
- `own`: reactions on the bot's own messages (default). - `own`: reactions on the bot's own messages (default).
- `all`: all reactions on all messages. - `all`: all reactions on all messages.
- `allowlist`: reactions from `slack.reactionAllowlist` on all messages (empty list disables). - `allowlist`: reactions from `channels.slack.reactionAllowlist` on all messages (empty list disables).
Slack action groups (gate `slack` tool actions): Slack action groups (gate `slack` tool actions):
| Action group | Default | Notes | | Action group | Default | Notes |
@ -1000,16 +1020,18 @@ Slack action groups (gate `slack` tool actions):
| memberInfo | enabled | Member info | | memberInfo | enabled | Member info |
| emojiList | enabled | Custom emoji list | | emojiList | enabled | Custom emoji list |
### `signal` (signal-cli) ### `channels.signal` (signal-cli)
Signal reactions can emit system events (shared reaction tooling): Signal reactions can emit system events (shared reaction tooling):
```json5 ```json5
{ {
signal: { channels: {
reactionNotifications: "own", // off | own | all | allowlist signal: {
reactionAllowlist: ["+15551234567", "uuid:123e4567-e89b-12d3-a456-426614174000"], reactionNotifications: "own", // off | own | all | allowlist
historyLimit: 50 // include last N group messages as context (0 disables) reactionAllowlist: ["+15551234567", "uuid:123e4567-e89b-12d3-a456-426614174000"],
historyLimit: 50 // include last N group messages as context (0 disables)
}
} }
} }
``` ```
@ -1018,36 +1040,38 @@ Reaction notification modes:
- `off`: no reaction events. - `off`: no reaction events.
- `own`: reactions on the bot's own messages (default). - `own`: reactions on the bot's own messages (default).
- `all`: all reactions on all messages. - `all`: all reactions on all messages.
- `allowlist`: reactions from `signal.reactionAllowlist` on all messages (empty list disables). - `allowlist`: reactions from `channels.signal.reactionAllowlist` on all messages (empty list disables).
### `imessage` (imsg CLI) ### `channels.imessage` (imsg CLI)
Clawdbot spawns `imsg rpc` (JSON-RPC over stdio). No daemon or port required. Clawdbot spawns `imsg rpc` (JSON-RPC over stdio). No daemon or port required.
```json5 ```json5
{ {
imessage: { channels: {
enabled: true, imessage: {
cliPath: "imsg", enabled: true,
dbPath: "~/Library/Messages/chat.db", cliPath: "imsg",
dmPolicy: "pairing", // pairing | allowlist | open | disabled dbPath: "~/Library/Messages/chat.db",
allowFrom: ["+15555550123", "user@example.com", "chat_id:123"], dmPolicy: "pairing", // pairing | allowlist | open | disabled
historyLimit: 50, // include last N group messages as context (0 disables) allowFrom: ["+15555550123", "user@example.com", "chat_id:123"],
includeAttachments: false, historyLimit: 50, // include last N group messages as context (0 disables)
mediaMaxMb: 16, includeAttachments: false,
service: "auto", mediaMaxMb: 16,
region: "US" service: "auto",
region: "US"
}
} }
} }
``` ```
Multi-account support lives under `imessage.accounts` (see the multi-account section above). Multi-account support lives under `channels.imessage.accounts` (see the multi-account section above).
Notes: Notes:
- Requires Full Disk Access to the Messages DB. - Requires Full Disk Access to the Messages DB.
- The first send will prompt for Messages automation permission. - The first send will prompt for Messages automation permission.
- Prefer `chat_id:<id>` targets. Use `imsg chats --limit 20` to list chats. - Prefer `chat_id:<id>` targets. Use `imsg chats --limit 20` to list chats.
- `imessage.cliPath` can point to a wrapper script (e.g. `ssh` to another Mac that runs `imsg rpc`); use SSH keys to avoid password prompts. - `channels.imessage.cliPath` can point to a wrapper script (e.g. `ssh` to another Mac that runs `imsg rpc`); use SSH keys to avoid password prompts.
Example wrapper: Example wrapper:
```bash ```bash
@ -1129,9 +1153,9 @@ streaming, final replies) across providers unless already present.
If `messages.responsePrefix` is unset, no prefix is applied by default. If `messages.responsePrefix` is unset, no prefix is applied by default.
Set it to `"auto"` to derive `[{identity.name}]` for the routed agent (when set). Set it to `"auto"` to derive `[{identity.name}]` for the routed agent (when set).
WhatsApp inbound prefix is configured via `whatsapp.messagePrefix` (deprecated: WhatsApp inbound prefix is configured via `channels.whatsapp.messagePrefix` (deprecated:
`messages.messagePrefix`). Default stays **unchanged**: `"[clawdbot]"` when `messages.messagePrefix`). Default stays **unchanged**: `"[clawdbot]"` when
`whatsapp.allowFrom` is empty, otherwise `""` (no prefix). When using `channels.whatsapp.allowFrom` is empty, otherwise `""` (no prefix). When using
`"[clawdbot]"`, Clawdbot will instead use `[{identity.name}]` when the routed `"[clawdbot]"`, Clawdbot will instead use `[{identity.name}]` when the routed
agent has `identity.name` set. agent has `identity.name` set.
@ -1474,9 +1498,9 @@ Block streaming:
Defaults to `{ idleMs: 1000 }` and inherits `minChars` from `blockStreamingChunk` Defaults to `{ idleMs: 1000 }` and inherits `minChars` from `blockStreamingChunk`
with `maxChars` capped to the provider text limit. Signal/Slack/Discord default with `maxChars` capped to the provider text limit. Signal/Slack/Discord default
to `minChars: 1500` unless overridden. to `minChars: 1500` unless overridden.
Provider overrides: `whatsapp.blockStreamingCoalesce`, `telegram.blockStreamingCoalesce`, Provider overrides: `channels.whatsapp.blockStreamingCoalesce`, `channels.telegram.blockStreamingCoalesce`,
`discord.blockStreamingCoalesce`, `slack.blockStreamingCoalesce`, `signal.blockStreamingCoalesce`, `channels.discord.blockStreamingCoalesce`, `channels.slack.blockStreamingCoalesce`, `channels.signal.blockStreamingCoalesce`,
`imessage.blockStreamingCoalesce`, `msteams.blockStreamingCoalesce` (and per-account variants). `channels.imessage.blockStreamingCoalesce`, `channels.msteams.blockStreamingCoalesce` (and per-account variants).
- `agents.defaults.humanDelay`: randomized pause between **block replies** after the first. - `agents.defaults.humanDelay`: randomized pause between **block replies** after the first.
Modes: `off` (default), `natural` (8002500ms), `custom` (use `minMs`/`maxMs`). Modes: `off` (default), `natural` (8002500ms), `custom` (use `minMs`/`maxMs`).
Per-agent override: `agents.list[].humanDelay`. Per-agent override: `agents.list[].humanDelay`.
@ -1585,7 +1609,7 @@ Tool groups (shorthands) work in **global** and **per-agent** tool policies:
- `allowFrom`: per-provider allowlists (empty = disabled) - `allowFrom`: per-provider allowlists (empty = disabled)
- `whatsapp`: E.164 numbers - `whatsapp`: E.164 numbers
- `telegram`: chat ids or usernames - `telegram`: chat ids or usernames
- `discord`: user ids or usernames (falls back to `discord.dm.allowFrom` if omitted) - `discord`: user ids or usernames (falls back to `channels.discord.dm.allowFrom` if omitted)
- `signal`: E.164 numbers - `signal`: E.164 numbers
- `imessage`: handles/chat ids - `imessage`: handles/chat ids
- `webchat`: session ids or usernames - `webchat`: session ids or usernames

View File

@ -16,7 +16,7 @@ The design goal is to keep all network discovery/advertising in the **Node Gatew
## Terms ## Terms
- **Gateway**: the single, long-running gateway process that owns state (sessions, pairing, node registry) and runs providers. - **Gateway**: the single, long-running gateway process that owns state (sessions, pairing, node registry) and runs channels.
- **Gateway WS (loopback)**: the existing gateway WebSocket control endpoint on `127.0.0.1:18789`. - **Gateway WS (loopback)**: the existing gateway WebSocket control endpoint on `127.0.0.1:18789`.
- **Bridge (direct transport)**: a LAN/tailnet-facing endpoint owned by the gateway that allows authenticated clients/nodes to call a scoped subset of gateway methods. The bridge exists so the gateway can remain loopback-only. - **Bridge (direct transport)**: a LAN/tailnet-facing endpoint owned by the gateway that allows authenticated clients/nodes to call a scoped subset of gateway methods. The bridge exists so the gateway can remain loopback-only.
- **SSH transport (fallback)**: remote control by forwarding `127.0.0.1:18789` over SSH. - **SSH transport (fallback)**: remote control by forwarding `127.0.0.1:18789` over SSH.

View File

@ -103,8 +103,8 @@ The Gateway also auto-runs doctor migrations on startup when it detects a
legacy config format, so stale configs are repaired without manual intervention. legacy config format, so stale configs are repaired without manual intervention.
Current migrations: Current migrations:
- `routing.allowFrom``whatsapp.allowFrom` - `routing.allowFrom``channels.whatsapp.allowFrom`
- `routing.groupChat.requireMention``whatsapp/telegram/imessage.groups."*".requireMention` - `routing.groupChat.requireMention``channels.whatsapp/telegram/imessage.groups."*".requireMention`
- `routing.groupChat.historyLimit``messages.groupChat.historyLimit` - `routing.groupChat.historyLimit``messages.groupChat.historyLimit`
- `routing.groupChat.mentionPatterns``messages.groupChat.mentionPatterns` - `routing.groupChat.mentionPatterns``messages.groupChat.mentionPatterns`
- `routing.queue``messages.queue` - `routing.queue``messages.queue`

View File

@ -18,12 +18,12 @@ Short guide to verify provider connectivity without guessing.
## Deep diagnostics ## Deep diagnostics
- Creds on disk: `ls -l ~/.clawdbot/credentials/whatsapp/<accountId>/creds.json` (mtime should be recent). - Creds on disk: `ls -l ~/.clawdbot/credentials/whatsapp/<accountId>/creds.json` (mtime should be recent).
- Session store: `ls -l ~/.clawdbot/agents/<agentId>/sessions/sessions.json` (path can be overridden in config). Count and recent recipients are surfaced via `status`. - Session store: `ls -l ~/.clawdbot/agents/<agentId>/sessions/sessions.json` (path can be overridden in config). Count and recent recipients are surfaced via `status`.
- Relink flow: `clawdbot providers logout && clawdbot providers login --verbose` when status codes 409515 or `loggedOut` appear in logs. (Note: the QR login flow auto-restarts once for status 515 after pairing.) - Relink flow: `clawdbot channels logout && clawdbot channels login --verbose` when status codes 409515 or `loggedOut` appear in logs. (Note: the QR login flow auto-restarts once for status 515 after pairing.)
## When something fails ## When something fails
- `logged out` or status 409515 → relink with `clawdbot providers logout` then `clawdbot providers login`. - `logged out` or status 409515 → relink with `clawdbot channels logout` then `clawdbot channels login`.
- Gateway unreachable → start it: `clawdbot gateway --port 18789` (use `--force` if the port is busy). - Gateway unreachable → start it: `clawdbot gateway --port 18789` (use `--force` if the port is busy).
- No inbound messages → confirm linked phone is online and the sender is allowed (`whatsapp.allowFrom`); for group chats, ensure allowlist + mention rules match (`whatsapp.groups`, `agents.list[].groupChat.mentionPatterns`). - No inbound messages → confirm linked phone is online and the sender is allowed (`channels.whatsapp.allowFrom`); for group chats, ensure allowlist + mention rules match (`channels.whatsapp.groups`, `agents.list[].groupChat.mentionPatterns`).
## Dedicated "health" command ## Dedicated "health" command
`clawdbot health --json` asks the running Gateway for its health snapshot (no direct provider sockets from the CLI). It reports linked creds/auth age when available, per-provider probe summaries, session-store summary, and a probe duration. It exits non-zero if the Gateway is unreachable or the probe fails/timeouts. Use `--timeout <ms>` to override the 10s default. `clawdbot health --json` asks the running Gateway for its health snapshot (no direct provider sockets from the CLI). It reports linked creds/auth age when available, per-provider probe summaries, session-store summary, and a probe duration. It exits non-zero if the Gateway is unreachable or the probe fails/timeouts. Use `--timeout <ms>` to override the 10s default.

View File

@ -81,6 +81,6 @@ Side-effecting methods require **idempotency keys** (see schema).
## Scope ## Scope
This protocol exposes the **full gateway API** (status, providers, models, This protocol exposes the **full gateway API** (status, channels, models,
chat, agent, sessions, nodes, etc.). The exact surface is defined by the chat, agent, sessions, nodes, etc.). The exact surface is defined by the
TypeBox schemas in `src/gateway/protocol/schema.ts`. TypeBox schemas in `src/gateway/protocol/schema.ts`.

View File

@ -17,7 +17,7 @@ This repo supports “remote over SSH” by keeping a single Gateway (the master
## Command flow (what runs where) ## Command flow (what runs where)
One gateway daemon owns state + providers. Nodes are peripherals. One gateway daemon owns state + channels. Nodes are peripherals.
Flow example (Telegram → node): Flow example (Telegram → node):
- Telegram message arrives at the **Gateway**. - Telegram message arrives at the **Gateway**.

View File

@ -65,13 +65,13 @@ Details + files on disk: [Pairing](/start/pairing)
Clawdbot has two separate “who can trigger me?” layers: Clawdbot has two separate “who can trigger me?” layers:
- **DM allowlist** (`allowFrom` / `discord.dm.allowFrom` / `slack.dm.allowFrom`): who is allowed to talk to the bot in direct messages. - **DM allowlist** (`allowFrom` / `channels.discord.dm.allowFrom` / `channels.slack.dm.allowFrom`): who is allowed to talk to the bot in direct messages.
- When `dmPolicy="pairing"`, approvals are written to `~/.clawdbot/credentials/<provider>-allowFrom.json` (merged with config allowlists). - When `dmPolicy="pairing"`, approvals are written to `~/.clawdbot/credentials/<provider>-allowFrom.json` (merged with config allowlists).
- **Group allowlist** (provider-specific): which groups/channels/guilds the bot will accept messages from at all. - **Group allowlist** (provider-specific): which groups/channels/guilds the bot will accept messages from at all.
- Common patterns: - Common patterns:
- `whatsapp.groups`, `telegram.groups`, `imessage.groups`: per-group defaults like `requireMention`; when set, it also acts as a group allowlist (include `"*"` to keep allow-all behavior). - `channels.whatsapp.groups`, `channels.telegram.groups`, `channels.imessage.groups`: per-group defaults like `requireMention`; when set, it also acts as a group allowlist (include `"*"` to keep allow-all behavior).
- `groupPolicy="allowlist"` + `groupAllowFrom`: restrict who can trigger the bot *inside* a group session (WhatsApp/Telegram/Signal/iMessage/Microsoft Teams). - `groupPolicy="allowlist"` + `groupAllowFrom`: restrict who can trigger the bot *inside* a group session (WhatsApp/Telegram/Signal/iMessage/Microsoft Teams).
- `discord.guilds` / `slack.channels`: per-surface allowlists + mention defaults. - `channels.discord.guilds` / `channels.slack.channels`: per-surface allowlists + mention defaults.
- **Security note:** treat `dmPolicy="open"` and `groupPolicy="open"` as last-resort settings. They should be barely used; prefer pairing + allowlists unless you fully trust every member of the room. - **Security note:** treat `dmPolicy="open"` and `groupPolicy="open"` as last-resort settings. They should be barely used; prefer pairing + allowlists unless you fully trust every member of the room.
Details: [Configuration](/gateway/configuration) and [Groups](/concepts/groups) Details: [Configuration](/gateway/configuration) and [Groups](/concepts/groups)
@ -163,7 +163,7 @@ See [Tailscale](/gateway/tailscale) and [Web overview](/web).
```json5 ```json5
{ {
whatsapp: { dmPolicy: "pairing" } channels: { whatsapp: { dmPolicy: "pairing" } }
} }
``` ```
@ -171,9 +171,11 @@ See [Tailscale](/gateway/tailscale) and [Web overview](/web).
```json ```json
{ {
"whatsapp": { "channels": {
"groups": { "whatsapp": {
"*": { "requireMention": true } "groups": {
"*": { "requireMention": true }
}
} }
}, },
"agents": { "agents": {

View File

@ -9,7 +9,7 @@ When Clawdbot misbehaves, here's how to fix it.
Start with the FAQs [First 60 seconds](/start/faq#first-60-seconds-if-somethings-broken) if you just want a quick triage recipe. This page goes deeper on runtime failures and diagnostics. Start with the FAQs [First 60 seconds](/start/faq#first-60-seconds-if-somethings-broken) if you just want a quick triage recipe. This page goes deeper on runtime failures and diagnostics.
Provider-specific shortcuts: [/providers/troubleshooting](/providers/troubleshooting) Provider-specific shortcuts: [/channels/troubleshooting](/channels/troubleshooting)
## Status & Diagnostics ## Status & Diagnostics
@ -21,7 +21,7 @@ Quick triage commands (in order):
| `clawdbot status --all` | Full local diagnosis (read-only, pasteable, safe-ish) incl. log tail | When you need to share a debug report | | `clawdbot status --all` | Full local diagnosis (read-only, pasteable, safe-ish) incl. log tail | When you need to share a debug report |
| `clawdbot status --deep` | Runs gateway health checks (incl. provider probes; requires reachable gateway) | When “configured” doesnt mean “working” | | `clawdbot status --deep` | Runs gateway health checks (incl. provider probes; requires reachable gateway) | When “configured” doesnt mean “working” |
| `clawdbot gateway status` | Gateway discovery + reachability (local + remote targets) | When you suspect youre probing the wrong gateway | | `clawdbot gateway status` | Gateway discovery + reachability (local + remote targets) | When you suspect youre probing the wrong gateway |
| `clawdbot providers status --probe` | Asks the running gateway for provider status (and optionally probes) | When gateway is reachable but providers misbehave | | `clawdbot channels status --probe` | Asks the running gateway for channel status (and optionally probes) | When gateway is reachable but channels misbehave |
| `clawdbot daemon status` | Supervisor state (launchd/systemd/schtasks), runtime PID/exit, last gateway error | When the daemon “looks loaded” but nothing runs | | `clawdbot daemon status` | Supervisor state (launchd/systemd/schtasks), runtime PID/exit, last gateway error | When the daemon “looks loaded” but nothing runs |
| `clawdbot logs --follow` | Live logs (best signal for runtime issues) | When you need the actual failure reason | | `clawdbot logs --follow` | Live logs (best signal for runtime issues) | When you need the actual failure reason |
@ -176,7 +176,7 @@ Look for `AllowFrom: ...` in the output.
```bash ```bash
# The message must match mentionPatterns or explicit mentions; defaults live in provider groups/guilds. # The message must match mentionPatterns or explicit mentions; defaults live in provider groups/guilds.
# Multi-agent: `agents.list[].groupChat.mentionPatterns` overrides global patterns. # Multi-agent: `agents.list[].groupChat.mentionPatterns` overrides global patterns.
grep -n "agents\\|groupChat\\|mentionPatterns\\|whatsapp\\.groups\\|telegram\\.groups\\|imessage\\.groups\\|discord\\.guilds" \ grep -n "agents\\|groupChat\\|mentionPatterns\\|channels\\.whatsapp\\.groups\\|channels\\.telegram\\.groups\\|channels\\.imessage\\.groups\\|channels\\.discord\\.guilds" \
"${CLAWDBOT_CONFIG_PATH:-$HOME/.clawdbot/clawdbot.json}" "${CLAWDBOT_CONFIG_PATH:-$HOME/.clawdbot/clawdbot.json}"
``` ```
@ -266,9 +266,9 @@ clawdbot gateway --verbose
If youre logged out / unlinked: If youre logged out / unlinked:
```bash ```bash
clawdbot providers logout clawdbot channels logout
trash "${CLAWDBOT_STATE_DIR:-$HOME/.clawdbot}/credentials" # if logout can't cleanly remove everything trash "${CLAWDBOT_STATE_DIR:-$HOME/.clawdbot}/credentials" # if logout can't cleanly remove everything
clawdbot providers login --verbose # re-scan QR clawdbot channels login --verbose # re-scan QR
``` ```
### Media Send Failing ### Media Send Failing
@ -356,7 +356,7 @@ Get verbose logging:
# #
# Then run verbose commands to mirror debug output to stdout: # Then run verbose commands to mirror debug output to stdout:
clawdbot gateway --verbose clawdbot gateway --verbose
clawdbot providers login --verbose clawdbot channels login --verbose
``` ```
## Log Locations ## Log Locations
@ -401,7 +401,7 @@ clawdbot daemon stop
# clawdbot daemon uninstall # clawdbot daemon uninstall
trash "${CLAWDBOT_STATE_DIR:-$HOME/.clawdbot}" trash "${CLAWDBOT_STATE_DIR:-$HOME/.clawdbot}"
clawdbot providers login # re-pair WhatsApp clawdbot channels login # re-pair WhatsApp
clawdbot daemon restart # or: clawdbot gateway clawdbot daemon restart # or: clawdbot gateway
``` ```

View File

@ -23,7 +23,7 @@ read_when:
<a href="/start/clawd">Clawdbot assistant setup</a> <a href="/start/clawd">Clawdbot assistant setup</a>
</p> </p>
Clawdbot bridges WhatsApp (via WhatsApp Web / Baileys), Telegram (Bot API / grammY), Discord (Bot API / discord.js), and iMessage (imsg CLI) to coding agents like [Pi](https://github.com/badlogic/pi-mono). Clawdbot bridges WhatsApp (via WhatsApp Web / Baileys), Telegram (Bot API / grammY), Discord (Bot API / channels.discord.js), and iMessage (imsg CLI) to coding agents like [Pi](https://github.com/badlogic/pi-mono).
Clawdbot also powers [Clawd](https://clawd.me), the spacelobster assistant. Clawdbot also powers [Clawd](https://clawd.me), the spacelobster assistant.
## Start here ## Start here
@ -78,7 +78,7 @@ Most operations flow through the **Gateway** (`clawdbot gateway`), a single long
- 📱 **WhatsApp Integration** — Uses Baileys for WhatsApp Web protocol - 📱 **WhatsApp Integration** — Uses Baileys for WhatsApp Web protocol
- ✈️ **Telegram Bot** — DMs + groups via grammY - ✈️ **Telegram Bot** — DMs + groups via grammY
- 🎮 **Discord Bot** — DMs + guild channels via discord.js - 🎮 **Discord Bot** — DMs + guild channels via channels.discord.js
- 💬 **iMessage** — Local imsg CLI integration (macOS) - 💬 **iMessage** — Local imsg CLI integration (macOS)
- 🤖 **Agent bridge** — Pi (RPC mode) with tool streaming - 🤖 **Agent bridge** — Pi (RPC mode) with tool streaming
- ⏱️ **Streaming + chunking** — Block streaming + Telegram draft streaming details ([/concepts/streaming](/concepts/streaming)) - ⏱️ **Streaming + chunking** — Block streaming + Telegram draft streaming details ([/concepts/streaming](/concepts/streaming))
@ -107,7 +107,7 @@ npm install -g clawdbot@latest
clawdbot onboard --install-daemon clawdbot onboard --install-daemon
# Pair WhatsApp Web (shows QR) # Pair WhatsApp Web (shows QR)
clawdbot providers login clawdbot channels login
# Gateway runs via daemon after onboarding; manual run is still possible: # Gateway runs via daemon after onboarding; manual run is still possible:
clawdbot gateway --port 18789 clawdbot gateway --port 18789
@ -145,17 +145,19 @@ clawdbot message send --to +15555550123 --message "Hello from Clawdbot"
Config lives at `~/.clawdbot/clawdbot.json`. Config lives at `~/.clawdbot/clawdbot.json`.
- If you **do nothing**, Clawdbot uses the bundled Pi binary in RPC mode with per-sender sessions. - If you **do nothing**, Clawdbot uses the bundled Pi binary in RPC mode with per-sender sessions.
- If you want to lock it down, start with `whatsapp.allowFrom` and (for groups) mention rules. - If you want to lock it down, start with `channels.whatsapp.allowFrom` and (for groups) mention rules.
Example: Example:
```json5 ```json5
{ {
whatsapp: { channels: {
allowFrom: ["+15555550123"], whatsapp: {
groups: { "*": { requireMention: true } } allowFrom: ["+15555550123"],
groups: { "*": { requireMention: true } }
}
}, },
routing: { groupChat: { mentionPatterns: ["@clawd"] } } messages: { groupChat: { mentionPatterns: ["@clawd"] } }
} }
``` ```
@ -184,9 +186,9 @@ Example:
- Providers and UX: - Providers and UX:
- [WebChat](/web/webchat) - [WebChat](/web/webchat)
- [Control UI (browser)](/web/control-ui) - [Control UI (browser)](/web/control-ui)
- [Telegram](/providers/telegram) - [Telegram](/channels/telegram)
- [Discord](/providers/discord) - [Discord](/channels/discord)
- [iMessage](/providers/imessage) - [iMessage](/channels/imessage)
- [Groups](/concepts/groups) - [Groups](/concepts/groups)
- [WhatsApp group messages](/concepts/group-messages) - [WhatsApp group messages](/concepts/group-messages)
- [Media: images](/nodes/images) - [Media: images](/nodes/images)

View File

@ -80,7 +80,7 @@ sudo systemctl restart clawdbot
# Provider login (run as clawdbot user) # Provider login (run as clawdbot user)
sudo -i -u clawdbot sudo -i -u clawdbot
clawdbot providers login clawdbot channels login
``` ```
## Security Architecture ## Security Architecture
@ -187,7 +187,7 @@ Make sure you're running as the `clawdbot` user:
```bash ```bash
sudo -i -u clawdbot sudo -i -u clawdbot
clawdbot providers login clawdbot channels login
``` ```
## Advanced Configuration ## Advanced Configuration

View File

@ -186,7 +186,7 @@ Discord (bot token):
docker compose run --rm clawdbot-cli providers add --provider discord --token "<token>" docker compose run --rm clawdbot-cli providers add --provider discord --token "<token>"
``` ```
Docs: [WhatsApp](/providers/whatsapp), [Telegram](/providers/telegram), [Discord](/providers/discord) Docs: [WhatsApp](/channels/whatsapp), [Telegram](/channels/telegram), [Discord](/channels/discord)
### Health check ### Health check

View File

@ -198,4 +198,4 @@ git pull
- Run `clawdbot doctor` again and read the output carefully (it often tells you the fix). - Run `clawdbot doctor` again and read the output carefully (it often tells you the fix).
- Check: [Troubleshooting](/gateway/troubleshooting) - Check: [Troubleshooting](/gateway/troubleshooting)
- Ask in Discord: https://discord.gg/clawd - Ask in Discord: https://channels.discord.gg/clawd

View File

@ -73,7 +73,7 @@ See [/web/control-ui](/web/control-ui) for how to open it.
To filter provider activity (WhatsApp/Telegram/etc), use: To filter provider activity (WhatsApp/Telegram/etc), use:
```bash ```bash
clawdbot providers logs --provider whatsapp clawdbot channels logs --channel whatsapp
``` ```
## Log formats ## Log formats
@ -87,7 +87,7 @@ entries to render structured output (time, level, subsystem, message).
Console logs are **TTY-aware** and formatted for readability: Console logs are **TTY-aware** and formatted for readability:
- Subsystem prefixes (e.g. `gateway/providers/whatsapp`) - Subsystem prefixes (e.g. `gateway/channels/whatsapp`)
- Level coloring (info/warn/error) - Level coloring (info/warn/error)
- Optional compact or JSON mode - Optional compact or JSON mode

View File

@ -37,7 +37,7 @@ This flow lets the macOS app act as a full remote control for a Clawdbot gateway
- Nodes advertise their permission state via `node.list` / `node.describe` so agents know whats available. - Nodes advertise their permission state via `node.list` / `node.describe` so agents know whats available.
## WhatsApp login flow (remote) ## WhatsApp login flow (remote)
- Run `clawdbot providers login --verbose` **on the remote host**. Scan the QR with WhatsApp on your phone. - Run `clawdbot channels login --verbose` **on the remote host**. Scan the QR with WhatsApp on your phone.
- Re-run login on that host if auth expires. Health check will surface link problems. - Re-run login on that host if auth expires. Health check will surface link problems.
## Troubleshooting ## Troubleshooting

View File

@ -1,30 +1,35 @@
--- ---
summary: "Messaging platforms Clawdbot can connect to" summary: "Model providers (LLMs) supported by Clawdbot"
read_when: read_when:
- You want to choose a chat provider for Clawdbot - You want to choose a model provider
- You need a quick overview of supported messaging platforms - You need a quick overview of supported LLM backends
--- ---
# Chat Providers # Model Providers
Clawdbot can talk to you on any chat app you already use. Each provider connects via the Gateway. Clawdbot can use many LLM providers. Pick a provider, authenticate, then set the
Text is supported everywhere; media and reactions vary by provider. default model as `provider/model`.
## Supported providers ## Quick start
- [WhatsApp](/providers/whatsapp) — Most popular; uses Baileys and requires QR pairing. 1) Authenticate with the provider (usually via `clawdbot onboard`).
- [Telegram](/providers/telegram) — Bot API via grammY; supports groups. 2) Set the default model:
- [Discord](/providers/discord) — Discord Bot API + Gateway; supports servers, channels, and DMs.
- [Slack](/providers/slack) — Bolt SDK; workspace apps.
- [Signal](/providers/signal) — signal-cli; privacy-focused.
- [iMessage](/providers/imessage) — macOS only; native integration.
- [Microsoft Teams](/providers/msteams) — Bot Framework; enterprise support.
- [WebChat](/web/webchat) — Gateway WebChat UI over WebSocket.
## Notes ```json5
{
agents: { defaults: { model: { primary: "anthropic/claude-opus-4-5" } } }
}
```
- Providers can run simultaneously; configure multiple and Clawdbot will route per chat. ## Provider docs
- Group behavior varies by provider; see [Groups](/concepts/groups).
- DM pairing and allowlists are enforced for safety; see [Security](/gateway/security). - [OpenAI (API + Codex)](/providers/openai)
- Telegram internals: [grammY notes](/providers/grammy). - [Anthropic (API + Claude CLI)](/providers/anthropic)
- Troubleshooting: [Provider troubleshooting](/providers/troubleshooting). - [OpenRouter](/providers/openrouter)
- Model providers are documented separately; see [Model Providers](/providers/models). - [Moonshot AI (Kimi)](/providers/moonshot)
- [OpenCode Zen](/providers/opencode)
- [Z.AI](/providers/zai)
- [GLM models](/providers/glm)
- [MiniMax](/providers/minimax)
For the full provider catalog (xAI, Groq, Mistral, etc.) and advanced configuration,
see [Model providers](/concepts/model-providers).

View File

@ -1,119 +0,0 @@
---
summary: "Signal support via signal-cli (JSON-RPC + SSE), setup, and number model"
read_when:
- Setting up Signal support
- Debugging Signal send/receive
---
# Signal (signal-cli)
Status: external CLI integration. Gateway talks to `signal-cli` over HTTP JSON-RPC + SSE.
## Quick setup (beginner)
1) Use a **separate Signal number** for the bot (recommended).
2) Install `signal-cli` (Java required).
3) Link the bot device and start the daemon:
- `signal-cli link -n "Clawdbot"`
4) Configure Clawdbot and start the gateway.
Minimal config:
```json5
{
signal: {
enabled: true,
account: "+15551234567",
cliPath: "signal-cli",
dmPolicy: "pairing",
allowFrom: ["+15557654321"]
}
}
```
## What it is
- Signal provider via `signal-cli` (not embedded libsignal).
- Deterministic routing: replies always go back to Signal.
- DMs share the agent's main session; groups are isolated (`agent:<agentId>:signal:group:<groupId>`).
## The number model (important)
- The gateway connects to a **Signal device** (the `signal-cli` account).
- If you run the bot on **your personal Signal account**, it will ignore your own messages (loop protection).
- For "I text the bot and it replies," use a **separate bot number**.
## Setup (fast path)
1) Install `signal-cli` (Java required).
2) Link a bot account:
- `signal-cli link -n "Clawdbot"` then scan the QR in Signal.
3) Configure Signal and start the gateway.
Example:
```json5
{
signal: {
enabled: true,
account: "+15551234567",
cliPath: "signal-cli",
dmPolicy: "pairing",
allowFrom: ["+15557654321"]
}
}
```
Multi-account support: use `signal.accounts` with per-account config and optional `name`. See [`gateway/configuration`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern.
## Access control (DMs + groups)
DMs:
- Default: `signal.dmPolicy = "pairing"`.
- Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour).
- Approve via:
- `clawdbot pairing list signal`
- `clawdbot pairing approve signal <CODE>`
- Pairing is the default token exchange for Signal DMs. Details: [Pairing](/start/pairing)
- UUID-only senders (from `sourceUuid`) are stored as `uuid:<id>` in `signal.allowFrom`.
Groups:
- `signal.groupPolicy = open | allowlist | disabled`.
- `signal.groupAllowFrom` controls who can trigger in groups when `allowlist` is set.
## How it works (behavior)
- `signal-cli` runs as a daemon; the gateway reads events via SSE.
- Inbound messages are normalized into the shared provider envelope.
- Replies always route back to the same number or group.
## Media + limits
- Outbound text is chunked to `signal.textChunkLimit` (default 4000).
- Attachments supported (base64 fetched from `signal-cli`).
- Default media cap: `signal.mediaMaxMb` (default 8).
- Use `signal.ignoreAttachments` to skip downloading media.
- Group history context uses `signal.historyLimit` (or `signal.accounts.*.historyLimit`), falling back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50).
## Delivery targets (CLI/cron)
- DMs: `signal:+15551234567` (or plain E.164).
- Groups: `signal:group:<groupId>`.
- Usernames: `username:<name>` (if supported by your Signal account).
## Configuration reference (Signal)
Full configuration: [Configuration](/gateway/configuration)
Provider options:
- `signal.enabled`: enable/disable provider startup.
- `signal.account`: E.164 for the bot account.
- `signal.cliPath`: path to `signal-cli`.
- `signal.httpUrl`: full daemon URL (overrides host/port).
- `signal.httpHost`, `signal.httpPort`: daemon bind (default 127.0.0.1:8080).
- `signal.autoStart`: auto-spawn daemon (default true if `httpUrl` unset).
- `signal.receiveMode`: `on-start | manual`.
- `signal.ignoreAttachments`: skip attachment downloads.
- `signal.ignoreStories`: ignore stories from the daemon.
- `signal.sendReadReceipts`: forward read receipts.
- `signal.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing).
- `signal.allowFrom`: DM allowlist (E.164 or `uuid:<id>`). `open` requires `"*"`.
- `signal.groupPolicy`: `open | allowlist | disabled` (default: allowlist).
- `signal.groupAllowFrom`: group sender allowlist.
- `signal.historyLimit`: max group messages to include as context (0 disables).
- `signal.textChunkLimit`: outbound chunk size (chars).
- `signal.mediaMaxMb`: inbound/outbound media cap (MB).
Related global options:
- `agents.list[].groupChat.mentionPatterns` (Signal does not support native mentions).
- `messages.groupChat.mentionPatterns` (global fallback).
- `messages.responsePrefix`.

View File

@ -1,22 +0,0 @@
---
summary: "Provider-specific troubleshooting shortcuts (Discord/Telegram/WhatsApp)"
read_when:
- A provider connects but messages dont flow
- Investigating provider misconfiguration (intents, permissions, privacy mode)
---
# Provider troubleshooting
Start with:
```bash
clawdbot doctor
clawdbot providers status --probe
```
`providers status --probe` prints warnings when it can detect common provider misconfigurations, and includes small live checks (credentials, some permissions/membership).
## Providers
- Discord: [/providers/discord#troubleshooting](/providers/discord#troubleshooting)
- Telegram: [/providers/telegram#troubleshooting](/providers/telegram#troubleshooting)
- WhatsApp: [/providers/whatsapp#troubleshooting-quick](/providers/whatsapp#troubleshooting-quick)

View File

@ -31,7 +31,7 @@ Each `ProviderPlugin` bundles:
- `gateway`: startAccount/stopAccount with runtime context (`getStatus`/`setStatus`), plus optional `loginWithQrStart/loginWithQrWait` for gateway-owned QR login flows. - `gateway`: startAccount/stopAccount with runtime context (`getStatus`/`setStatus`), plus optional `loginWithQrStart/loginWithQrWait` for gateway-owned QR login flows.
- `security`: dmPolicy + allowFrom hints used by `doctor security`. - `security`: dmPolicy + allowFrom hints used by `doctor security`.
- `heartbeat`: optional readiness checks + heartbeat recipient resolution when providers own targeting. - `heartbeat`: optional readiness checks + heartbeat recipient resolution when providers own targeting.
- `auth`: optional login hook used by `clawdbot providers login`. - `auth`: optional login hook used by `clawdbot channels login`.
- `reload`: `configPrefixes` that map to hot restarts. - `reload`: `configPrefixes` that map to hot restarts.
- `onboarding`: optional CLI onboarding adapter (wizard UI hooks per provider). - `onboarding`: optional CLI onboarding adapter (wizard UI hooks per provider).
- `agentTools`: optional provider-owned agent tools (ex: QR login). - `agentTools`: optional provider-owned agent tools (ex: QR login).
@ -88,14 +88,14 @@ Each `ProviderPlugin` bundles:
- Session announce targets can opt into `meta.preferSessionLookupForAnnounceTarget` when session keys are insufficient (e.g., WhatsApp). - Session announce targets can opt into `meta.preferSessionLookupForAnnounceTarget` when session keys are insufficient (e.g., WhatsApp).
- Onboarding provider setup is delegated to adapter modules under `src/providers/plugins/onboarding/*`, keeping `setupProviders` provider-agnostic. - Onboarding provider setup is delegated to adapter modules under `src/providers/plugins/onboarding/*`, keeping `setupProviders` provider-agnostic.
- Onboarding registry now reads `plugin.onboarding` from each provider (no standalone onboarding map). - Onboarding registry now reads `plugin.onboarding` from each provider (no standalone onboarding map).
- Provider login flows (`clawdbot providers login`) route through `plugin.auth.login` when available. - Channel login flows (`clawdbot channels login`) route through `plugin.auth.login` when available.
- `clawdbot status` reports `linkProvider` (derived from `status.buildProviderSummary().linked`) instead of a hardcoded `web` provider field. - `clawdbot status` reports `linkProvider` (derived from `status.buildProviderSummary().linked`) instead of a hardcoded `web` provider field.
- Gateway `web.login.*` methods use `plugin.gatewayMethods` ownership to pick the provider (no hardcoded `normalizeProviderId("web")` in the handler). - Gateway `web.login.*` methods use `plugin.gatewayMethods` ownership to pick the provider (no hardcoded `normalizeProviderId("web")` in the handler).
## CLI Commands (inline references) ## CLI Commands (inline references)
- Add/remove providers: `clawdbot providers add <provider>` / `clawdbot providers remove <provider>`. - Add/remove channels: `clawdbot channels add <channel>` / `clawdbot channels remove <channel>`.
- Inspect provider state: `clawdbot providers list`, `clawdbot providers status`. - Inspect channel state: `clawdbot channels list`, `clawdbot channels status`.
- Link/unlink providers: `clawdbot providers login --provider <provider>` / `clawdbot providers logout --provider <provider>`. - Link/unlink channels: `clawdbot channels login --channel <channel>` / `clawdbot channels logout --channel <channel>`.
- Pairing approvals: `clawdbot pairing list <provider>`, `clawdbot pairing approve <provider> <code>`. - Pairing approvals: `clawdbot pairing list <provider>`, `clawdbot pairing approve <provider> <code>`.
## Adding a Provider (checklist) ## Adding a Provider (checklist)

View File

@ -12,9 +12,9 @@ Clawdbot integrates external CLIs via JSON-RPC. Two patterns are used today.
- `signal-cli` runs as a daemon with JSON-RPC over HTTP. - `signal-cli` runs as a daemon with JSON-RPC over HTTP.
- Event stream is SSE (`/api/v1/events`). - Event stream is SSE (`/api/v1/events`).
- Health probe: `/api/v1/check`. - Health probe: `/api/v1/check`.
- Clawdbot owns lifecycle when `signal.autoStart=true`. - Clawdbot owns lifecycle when `channels.signal.autoStart=true`.
See [Signal](/providers/signal) for setup and endpoints. See [Signal](/channels/signal) for setup and endpoints.
## Pattern B: stdio child process (imsg) ## Pattern B: stdio child process (imsg)
- Clawdbot spawns `imsg rpc` as a child process. - Clawdbot spawns `imsg rpc` as a child process.
@ -27,7 +27,7 @@ Core methods used:
- `send` - `send`
- `chats.list` (probe/diagnostics) - `chats.list` (probe/diagnostics)
See [iMessage](/providers/imessage) for setup and addressing (`chat_id` preferred). See [iMessage](/channels/imessage) for setup and addressing (`chat_id` preferred).
## Adapter guidelines ## Adapter guidelines
- Gateway owns the process (start/stop tied to provider lifecycle). - Gateway owns the process (start/stop tied to provider lifecycle).

View File

@ -16,7 +16,7 @@ Youre putting an agent in a position to:
- send messages back out via WhatsApp/Telegram/Discord - send messages back out via WhatsApp/Telegram/Discord
Start conservative: Start conservative:
- Always set `whatsapp.allowFrom` (never run open-to-the-world on your personal Mac). - Always set `channels.whatsapp.allowFrom` (never run open-to-the-world on your personal Mac).
- Use a dedicated WhatsApp number for the assistant. - Use a dedicated WhatsApp number for the assistant.
- Heartbeats now default to every 30 minutes. Disable until you trust the setup by setting `agents.defaults.heartbeat.every: "0m"`. - Heartbeats now default to every 30 minutes. Disable until you trust the setup by setting `agents.defaults.heartbeat.every: "0m"`.
@ -68,7 +68,7 @@ If you link your personal WhatsApp to Clawdbot, every message to you becomes “
1) Pair WhatsApp Web (shows QR; scan with the assistant phone): 1) Pair WhatsApp Web (shows QR; scan with the assistant phone):
```bash ```bash
clawdbot providers login clawdbot channels login
``` ```
2) Start the Gateway (leave it running): 2) Start the Gateway (leave it running):
@ -81,9 +81,7 @@ clawdbot gateway --port 18789
```json5 ```json5
{ {
whatsapp: { channels: { whatsapp: { allowFrom: ["+15555550123"] } }
allowFrom: ["+15555550123"]
}
} }
``` ```
@ -146,10 +144,12 @@ Example:
// Start with 0; enable later. // Start with 0; enable later.
heartbeat: { every: "0m" } heartbeat: { every: "0m" }
}, },
whatsapp: { channels: {
allowFrom: ["+15555550123"], whatsapp: {
groups: { allowFrom: ["+15555550123"],
"*": { requireMention: true } groups: {
"*": { requireMention: true }
}
} }
}, },
routing: { routing: {

View File

@ -271,11 +271,11 @@ without WhatsApp/Telegram.
### Telegram: what goes in `allowFrom`? ### Telegram: what goes in `allowFrom`?
`telegram.allowFrom` is **the human senders Telegram user ID** (numeric, recommended) or `@username`. It is not the bot username. To find your ID, DM `@userinfobot` or read the `from.id` in the gateway log for a DM. See [/providers/telegram](/providers/telegram#access-control-dms--groups). `channels.telegram.allowFrom` is **the human senders Telegram user ID** (numeric, recommended) or `@username`. It is not the bot username. To find your ID, DM `@userinfobot` or read the `from.id` in the gateway log for a DM. See [/channels/telegram](/channels/telegram#access-control-dms--groups).
### Can multiple people use one WhatsApp number with different Clawdbots? ### Can multiple people use one WhatsApp number with different Clawdbots?
Yes, via **multiagent routing**. Bind each senders WhatsApp **DM** (peer `kind: "dm"`, sender E.164 like `+15551234567`) to a different `agentId`, so each person gets their own workspace and session store. Replies still come from the **same WhatsApp account**, and DM access control (`whatsapp.dmPolicy` / `whatsapp.allowFrom`) is global per WhatsApp account. See [Multi-Agent Routing](/concepts/multi-agent) and [WhatsApp](/providers/whatsapp). Yes, via **multiagent routing**. Bind each senders WhatsApp **DM** (peer `kind: "dm"`, sender E.164 like `+15551234567`) to a different `agentId`, so each person gets their own workspace and session store. Replies still come from the **same WhatsApp account**, and DM access control (`channels.whatsapp.dmPolicy` / `channels.whatsapp.allowFrom`) is global per WhatsApp account. See [Multi-Agent Routing](/concepts/multi-agent) and [WhatsApp](/channels/whatsapp).
### Can I run a "fast chat" agent and an "Opus for coding" agent? ### Can I run a "fast chat" agent and an "Opus for coding" agent?
@ -548,7 +548,7 @@ The Gateway watches the config and supports hotreload:
The common pattern is **one Gateway** (e.g. Raspberry Pi) plus **nodes** and **agents**: The common pattern is **one Gateway** (e.g. Raspberry Pi) plus **nodes** and **agents**:
- **Gateway (central):** owns providers (Signal/WhatsApp), routing, and sessions. - **Gateway (central):** owns channels (Signal/WhatsApp), routing, and sessions.
- **Nodes (devices):** Macs/iOS/Android connect as peripherals and expose local tools (`system.run`, `canvas`, `camera`). - **Nodes (devices):** Macs/iOS/Android connect as peripherals and expose local tools (`system.run`, `canvas`, `camera`).
- **Agents (workers):** separate brains/workspaces for special roles (e.g. “Hetzner ops”, “Personal data”). - **Agents (workers):** separate brains/workspaces for special roles (e.g. “Hetzner ops”, “Personal data”).
- **Subagents:** spawn background work from a main agent when you want parallelism. - **Subagents:** spawn background work from a main agent when you want parallelism.
@ -605,7 +605,7 @@ Yes. `config.apply` validates + writes the full config and restarts the Gateway
```json5 ```json5
{ {
agents: { defaults: { workspace: "~/clawd" } }, agents: { defaults: { workspace: "~/clawd" } },
whatsapp: { allowFrom: ["+15555550123"] } channels: { whatsapp: { allowFrom: ["+15555550123"] } }
} }
``` ```
@ -788,9 +788,11 @@ If you want only **you** to be able to trigger group replies:
```json5 ```json5
{ {
whatsapp: { channels: {
groupPolicy: "allowlist", whatsapp: {
groupAllowFrom: ["+15551234567"] groupPolicy: "allowlist",
groupAllowFrom: ["+15551234567"]
}
} }
} }
``` ```
@ -799,7 +801,7 @@ If you want only **you** to be able to trigger group replies:
Two common causes: Two common causes:
- Mention gating is on (default). You must @mention the bot (or match `mentionPatterns`). - Mention gating is on (default). You must @mention the bot (or match `mentionPatterns`).
- You configured `whatsapp.groups` without `"*"` and the group isnt allowlisted. - You configured `channels.whatsapp.groups` without `"*"` and the group isnt allowlisted.
See [Groups](/concepts/groups) and [Group messages](/concepts/group-messages). See [Groups](/concepts/groups) and [Group messages](/concepts/group-messages).
@ -1276,7 +1278,7 @@ Note: images are resized/recompressed (max side 2048px) to hit size limits. See
Treat inbound DMs as untrusted input. Defaults are designed to reduce risk: Treat inbound DMs as untrusted input. Defaults are designed to reduce risk:
- Default behavior on DMcapable providers is **pairing**: - Default behavior on DMcapable channels is **pairing**:
- Unknown senders receive a pairing code; the bot does not process their message. - Unknown senders receive a pairing code; the bot does not process their message.
- Approve with: `clawdbot pairing approve <provider> <code>` - Approve with: `clawdbot pairing approve <provider> <code>`
- Pending requests are capped at **3 per provider**; check `clawdbot pairing list <provider>` if a code didnt arrive. - Pending requests are capped at **3 per provider**; check `clawdbot pairing list <provider>` if a code didnt arrive.
@ -1300,7 +1302,7 @@ List pending requests:
clawdbot pairing list whatsapp clawdbot pairing list whatsapp
``` ```
Wizard phone number prompt: its used to set your **allowlist/owner** so your own DMs are permitted. Its not used for auto-sending. If you run on your personal WhatsApp number, use that number and enable `whatsapp.selfChatMode`. Wizard phone number prompt: its used to set your **allowlist/owner** so your own DMs are permitted. Its not used for auto-sending. If you run on your personal WhatsApp number, use that number and enable `channels.whatsapp.selfChatMode`.
## Chat commands, aborting tasks, and “it wont stop” ## Chat commands, aborting tasks, and “it wont stop”
@ -1355,22 +1357,24 @@ Enable self-chat mode and allowlist your own number:
```json5 ```json5
{ {
whatsapp: { channels: {
selfChatMode: true, whatsapp: {
dmPolicy: "allowlist", selfChatMode: true,
allowFrom: ["+15555550123"] dmPolicy: "allowlist",
allowFrom: ["+15555550123"]
}
} }
} }
``` ```
See [WhatsApp setup](/providers/whatsapp). See [WhatsApp setup](/channels/whatsapp).
### WhatsApp logged me out. How do I reauth? ### WhatsApp logged me out. How do I reauth?
Run the login command again and scan the QR code: Run the login command again and scan the QR code:
```bash ```bash
clawdbot providers login clawdbot channels login
``` ```
### Build errors on `main` — whats the standard fix path? ### Build errors on `main` — whats the standard fix path?

View File

@ -1,5 +1,5 @@
--- ---
summary: "Beginner guide: from zero to first message (wizard, auth, providers, pairing)" summary: "Beginner guide: from zero to first message (wizard, auth, channels, pairing)"
read_when: read_when:
- First time setup from zero - First time setup from zero
- You want the fastest path from install → onboarding → first message - You want the fastest path from install → onboarding → first message
@ -12,7 +12,7 @@ Goal: go from **zero** → **first working chat** (with sane defaults) as quickl
Recommended path: use the **CLI onboarding wizard** (`clawdbot onboard`). It sets up: Recommended path: use the **CLI onboarding wizard** (`clawdbot onboard`). It sets up:
- model/auth (OAuth recommended) - model/auth (OAuth recommended)
- gateway settings - gateway settings
- providers (WhatsApp/Telegram/Discord/…) - channels (WhatsApp/Telegram/Discord/…)
- pairing defaults (secure DMs) - pairing defaults (secure DMs)
- workspace bootstrap + skills - workspace bootstrap + skills
- optional background daemon - optional background daemon
@ -118,18 +118,18 @@ providers. If you use WhatsApp or Telegram, run the Gateway with **Node**.
### WhatsApp (QR login) ### WhatsApp (QR login)
```bash ```bash
clawdbot providers login clawdbot channels login
``` ```
Scan via WhatsApp → Settings → Linked Devices. Scan via WhatsApp → Settings → Linked Devices.
WhatsApp doc: [WhatsApp](/providers/whatsapp) WhatsApp doc: [WhatsApp](/channels/whatsapp)
### Telegram / Discord / others ### Telegram / Discord / others
The wizard can write tokens/config for you. If you prefer manual config, start with: The wizard can write tokens/config for you. If you prefer manual config, start with:
- Telegram: [Telegram](/providers/telegram) - Telegram: [Telegram](/channels/telegram)
- Discord: [Discord](/providers/discord) - Discord: [Discord](/channels/discord)
**Telegram DM tip:** your first DM returns a pairing code. Approve it (see next step) or the bot wont respond. **Telegram DM tip:** your first DM returns a pairing code. Approve it (see next step) or the bot wont respond.

View File

@ -51,7 +51,7 @@ Use these hubs to discover every page, including deep dives and reference docs t
- [Presence](/concepts/presence) - [Presence](/concepts/presence)
- [Discovery + transports](/gateway/discovery) - [Discovery + transports](/gateway/discovery)
- [Bonjour](/gateway/bonjour) - [Bonjour](/gateway/bonjour)
- [Provider routing](/concepts/provider-routing) - [Channel routing](/concepts/channel-routing)
- [Groups](/concepts/groups) - [Groups](/concepts/groups)
- [Group messages](/concepts/group-messages) - [Group messages](/concepts/group-messages)
- [Model failover](/concepts/model-failover) - [Model failover](/concepts/model-failover)
@ -59,16 +59,16 @@ Use these hubs to discover every page, including deep dives and reference docs t
## Providers + ingress ## Providers + ingress
- [Chat providers hub](/providers) - [Chat channels hub](/channels)
- [Model providers hub](/providers/models) - [Model providers hub](/providers/models)
- [WhatsApp](/providers/whatsapp) - [WhatsApp](/channels/whatsapp)
- [Telegram](/providers/telegram) - [Telegram](/channels/telegram)
- [Telegram (grammY notes)](/providers/grammy) - [Telegram (grammY notes)](/channels/grammy)
- [Slack](/providers/slack) - [Slack](/channels/slack)
- [Discord](/providers/discord) - [Discord](/channels/discord)
- [Signal](/providers/signal) - [Signal](/channels/signal)
- [iMessage](/providers/imessage) - [iMessage](/channels/imessage)
- [Location parsing](/providers/location) - [Location parsing](/channels/location)
- [WebChat](/web/webchat) - [WebChat](/web/webchat)
- [Webhooks](/automation/webhook) - [Webhooks](/automation/webhook)
- [Gmail Pub/Sub](/automation/gmail-pubsub) - [Gmail Pub/Sub](/automation/gmail-pubsub)

View File

@ -34,7 +34,7 @@ clawdbot pairing list telegram
clawdbot pairing approve telegram <CODE> clawdbot pairing approve telegram <CODE>
``` ```
Supported providers: `telegram`, `whatsapp`, `signal`, `imessage`, `discord`, `slack`. Supported channels: `telegram`, `whatsapp`, `signal`, `imessage`, `discord`, `slack`.
### Where the state lives ### Where the state lives
@ -73,9 +73,9 @@ Full protocol + design notes: [Gateway pairing](/gateway/pairing)
- Security model + prompt injection: [Security](/gateway/security) - Security model + prompt injection: [Security](/gateway/security)
- Updating safely (run doctor): [Updating](/install/updating) - Updating safely (run doctor): [Updating](/install/updating)
- Provider configs: - Provider configs:
- Telegram: [Telegram](/providers/telegram) - Telegram: [Telegram](/channels/telegram)
- WhatsApp: [WhatsApp](/providers/whatsapp) - WhatsApp: [WhatsApp](/channels/whatsapp)
- Signal: [Signal](/providers/signal) - Signal: [Signal](/channels/signal)
- iMessage: [iMessage](/providers/imessage) - iMessage: [iMessage](/channels/imessage)
- Discord: [Discord](/providers/discord) - Discord: [Discord](/channels/discord)
- Slack: [Slack](/providers/slack) - Slack: [Slack](/channels/slack)

View File

@ -46,7 +46,7 @@ pnpm clawdbot setup
4) Link surfaces (example: WhatsApp): 4) Link surfaces (example: WhatsApp):
```bash ```bash
clawdbot providers login clawdbot channels login
``` ```
5) Sanity check: 5) Sanity check:
@ -56,7 +56,7 @@ clawdbot health
``` ```
If onboarding is not available in your build: If onboarding is not available in your build:
- Run `clawdbot setup`, then `clawdbot providers login`, then start the Gateway manually (`clawdbot gateway`). - Run `clawdbot setup`, then `clawdbot channels login`, then start the Gateway manually (`clawdbot gateway`).
## Bleeding edge workflow (Gateway in a terminal) ## Bleeding edge workflow (Gateway in a terminal)
@ -124,6 +124,6 @@ user service (no lingering needed). See [Gateway runbook](/gateway) for the syst
- [Gateway runbook](/gateway) (flags, supervision, ports) - [Gateway runbook](/gateway) (flags, supervision, ports)
- [Gateway configuration](/gateway/configuration) (config schema + examples) - [Gateway configuration](/gateway/configuration) (config schema + examples)
- [Discord](/providers/discord) and [Telegram](/providers/telegram) (reply tags + replyToMode settings) - [Discord](/channels/discord) and [Telegram](/channels/telegram) (reply tags + replyToMode settings)
- [Clawdbot assistant setup](/start/clawd) - [Clawdbot assistant setup](/start/clawd)
- [macOS app](/platforms/macos) (gateway lifecycle) - [macOS app](/platforms/macos) (gateway lifecycle)

View File

@ -1,5 +1,5 @@
--- ---
summary: "CLI onboarding wizard: guided setup for gateway, workspace, providers, and skills" summary: "CLI onboarding wizard: guided setup for gateway, workspace, channels, and skills"
read_when: read_when:
- Running or configuring the onboarding wizard - Running or configuring the onboarding wizard
- Setting up a new machine - Setting up a new machine
@ -9,7 +9,7 @@ read_when:
The onboarding wizard is the **recommended** way to set up Clawdbot on macOS, The onboarding wizard is the **recommended** way to set up Clawdbot on macOS,
Linux, or Windows (via WSL2; strongly recommended). Linux, or Windows (via WSL2; strongly recommended).
It configures a local Gateway or a remote Gateway connection, plus providers, skills, It configures a local Gateway or a remote Gateway connection, plus channels, skills,
and workspace defaults in one guided flow. and workspace defaults in one guided flow.
Primary entrypoint: Primary entrypoint:
@ -36,7 +36,7 @@ The wizard starts with **QuickStart** (defaults) vs **Advanced** (full control).
- Tailscale exposure **Off** - Tailscale exposure **Off**
- Telegram + WhatsApp DMs default to **allowlist** (youll be prompted for your phone number) - Telegram + WhatsApp DMs default to **allowlist** (youll be prompted for your phone number)
**Advanced** exposes every step (mode, workspace, gateway, providers, daemon, skills). **Advanced** exposes every step (mode, workspace, gateway, channels, daemon, skills).
## What the wizard does ## What the wizard does
@ -259,7 +259,7 @@ Clients (macOS app, Control UI) can render steps without reimplementing onboa
The wizard can install `signal-cli` from GitHub releases: The wizard can install `signal-cli` from GitHub releases:
- Downloads the appropriate release asset. - Downloads the appropriate release asset.
- Stores it under `~/.clawdbot/tools/signal-cli/<version>/`. - Stores it under `~/.clawdbot/tools/signal-cli/<version>/`.
- Writes `signal.cliPath` to your config. - Writes `channels.signal.cliPath` to your config.
Notes: Notes:
- JVM builds require **Java 21**. - JVM builds require **Java 21**.
@ -272,7 +272,7 @@ Typical fields in `~/.clawdbot/clawdbot.json`:
- `agents.defaults.workspace` - `agents.defaults.workspace`
- `agents.defaults.model` / `models.providers` (if Minimax chosen) - `agents.defaults.model` / `models.providers` (if Minimax chosen)
- `gateway.*` (mode, bind, auth, tailscale) - `gateway.*` (mode, bind, auth, tailscale)
- `telegram.botToken`, `discord.token`, `signal.*`, `imessage.*` - `channels.telegram.botToken`, `channels.discord.token`, `channels.signal.*`, `channels.imessage.*`
- `skills.install.nodeManager` - `skills.install.nodeManager`
- `wizard.lastRunAt` - `wizard.lastRunAt`
- `wizard.lastRunVersion` - `wizard.lastRunVersion`
@ -289,5 +289,5 @@ Sessions are stored under `~/.clawdbot/agents/<agentId>/sessions/`.
- macOS app onboarding: [Onboarding](/start/onboarding) - macOS app onboarding: [Onboarding](/start/onboarding)
- Config reference: [Gateway configuration](/gateway/configuration) - Config reference: [Gateway configuration](/gateway/configuration)
- Providers: [WhatsApp](/providers/whatsapp), [Telegram](/providers/telegram), [Discord](/providers/discord), [Signal](/providers/signal), [iMessage](/providers/imessage) - Providers: [WhatsApp](/channels/whatsapp), [Telegram](/channels/telegram), [Discord](/channels/discord), [Signal](/channels/signal), [iMessage](/channels/imessage)
- Skills: [Skills](/tools/skills), [Skills config](/tools/skills-config) - Skills: [Skills](/tools/skills), [Skills config](/tools/skills-config)

View File

@ -47,7 +47,7 @@ Use these in chat:
Other surfaces: Other surfaces:
- **TUI/Web TUI:** `/status` + `/cost` are supported. - **TUI/Web TUI:** `/status` + `/cost` are supported.
- **CLI:** `clawdbot status --usage` and `clawdbot providers list` show - **CLI:** `clawdbot status --usage` and `clawdbot channels list` show
provider quota windows (not per-response costs). provider quota windows (not per-response costs).
## Cost estimation (when shown) ## Cost estimation (when shown)

View File

@ -45,7 +45,7 @@ Note:
- Sender allowlist: `tools.elevated.allowFrom` with per-provider allowlists (e.g. `discord`, `whatsapp`). - Sender allowlist: `tools.elevated.allowFrom` with per-provider allowlists (e.g. `discord`, `whatsapp`).
- Per-agent gate: `agents.list[].tools.elevated.enabled` (optional; can only further restrict). - Per-agent gate: `agents.list[].tools.elevated.enabled` (optional; can only further restrict).
- Per-agent allowlist: `agents.list[].tools.elevated.allowFrom` (optional; when set, the sender must match **both** global + per-agent allowlists). - Per-agent allowlist: `agents.list[].tools.elevated.allowFrom` (optional; when set, the sender must match **both** global + per-agent allowlists).
- Discord fallback: if `tools.elevated.allowFrom.discord` is omitted, the `discord.dm.allowFrom` list is used as a fallback. Set `tools.elevated.allowFrom.discord` (even `[]`) to override. Per-agent allowlists do **not** use the fallback. - Discord fallback: if `tools.elevated.allowFrom.discord` is omitted, the `channels.discord.dm.allowFrom` list is used as a fallback. Set `tools.elevated.allowFrom.discord` (even `[]`) to override. Per-agent allowlists do **not** use the fallback.
- All gates must pass; otherwise elevated is treated as unavailable. - All gates must pass; otherwise elevated is treated as unavailable.
## Logging + status ## Logging + status

View File

@ -16,4 +16,4 @@ Provider notes:
- **Discord/Slack**: empty `emoji` removes all of the bot's reactions on the message; `remove: true` removes just that emoji. - **Discord/Slack**: empty `emoji` removes all of the bot's reactions on the message; `remove: true` removes just that emoji.
- **Telegram**: empty `emoji` removes the bot's reactions; `remove: true` also removes reactions but still requires a non-empty `emoji` for tool validation. - **Telegram**: empty `emoji` removes the bot's reactions; `remove: true` also removes reactions but still requires a non-empty `emoji` for tool validation.
- **WhatsApp**: empty `emoji` removes the bot reaction; `remove: true` maps to empty emoji (still requires `emoji`). - **WhatsApp**: empty `emoji` removes the bot reaction; `remove: true` maps to empty emoji (still requires `emoji`).
- **Signal**: inbound reaction notifications emit system events when `signal.reactionNotifications` is enabled. - **Signal**: inbound reaction notifications emit system events when `channels.signal.reactionNotifications` is enabled.

View File

@ -41,7 +41,7 @@ They run immediately, are stripped before the model sees the message, and the re
- On surfaces without native commands (WhatsApp/WebChat/Signal/iMessage/MS Teams), text commands still work even if you set this to `false`. - On surfaces without native commands (WhatsApp/WebChat/Signal/iMessage/MS Teams), text commands still work even if you set this to `false`.
- `commands.native` (default `"auto"`) registers native commands. - `commands.native` (default `"auto"`) registers native commands.
- Auto: on for Discord/Telegram; off for Slack (until you add slash commands); ignored for providers without native support. - Auto: on for Discord/Telegram; off for Slack (until you add slash commands); ignored for providers without native support.
- Set `discord.commands.native`, `telegram.commands.native`, or `slack.commands.native` to override per provider (bool or `"auto"`). - Set `channels.discord.commands.native`, `channels.telegram.commands.native`, or `channels.slack.commands.native` to override per provider (bool or `"auto"`).
- `false` clears previously registered commands on Discord/Telegram at startup. Slack commands are managed in the Slack app and are not removed automatically. - `false` clears previously registered commands on Discord/Telegram at startup. Slack commands are managed in the Slack app and are not removed automatically.
- `commands.bash` (default `false`) enables `! <cmd>` to run host shell commands (`/bash <cmd>` is an alias; requires `tools.elevated` allowlists). - `commands.bash` (default `false`) enables `! <cmd>` to run host shell commands (`/bash <cmd>` is an alias; requires `tools.elevated` allowlists).
- `commands.bashForegroundMs` (default `2000`) controls how long bash waits before switching to background mode (`0` backgrounds immediately). - `commands.bashForegroundMs` (default `2000`) controls how long bash waits before switching to background mode (`0` backgrounds immediately).
@ -125,7 +125,7 @@ Examples:
``` ```
/debug show /debug show
/debug set messages.responsePrefix="[clawdbot]" /debug set messages.responsePrefix="[clawdbot]"
/debug set whatsapp.allowFrom=["+1555","+4477"] /debug set channels.whatsapp.allowFrom=["+1555","+4477"]
/debug unset messages.responsePrefix /debug unset messages.responsePrefix
/debug reset /debug reset
``` ```
@ -157,7 +157,7 @@ Notes:
- **Text commands** run in the normal chat session (DMs share `main`, groups have their own session). - **Text commands** run in the normal chat session (DMs share `main`, groups have their own session).
- **Native commands** use isolated sessions: - **Native commands** use isolated sessions:
- Discord: `agent:<agentId>:discord:slash:<userId>` - Discord: `agent:<agentId>:discord:slash:<userId>`
- Slack: `agent:<agentId>:slack:slash:<userId>` (prefix configurable via `slack.slashCommand.sessionPrefix`) - Slack: `agent:<agentId>:slack:slash:<userId>` (prefix configurable via `channels.slack.slashCommand.sessionPrefix`)
- Telegram: `telegram:slash:<userId>` (targets the chat session via `CommandTargetSessionKey`) - Telegram: `telegram:slash:<userId>` (targets the chat session via `CommandTargetSessionKey`)
- **`/stop`** targets the active chat session so it can abort the current run. - **`/stop`** targets the active chat session so it can abort the current run.
- **Slack:** `slack.slashCommand` is still supported for a single `/clawd`-style command. If you enable `commands.native`, you must create one Slack slash command per built-in command (same names as `/help`). - **Slack:** `channels.slack.slashCommand` is still supported for a single `/clawd`-style command. If you enable `commands.native`, you must create one Slack slash command per built-in command (same names as `/help`).

View File

@ -0,0 +1,17 @@
import { listChannelPlugins } from "../channels/plugins/index.js";
import type { ChannelAgentTool } from "../channels/plugins/types.js";
import type { ClawdbotConfig } from "../config/config.js";
export function listChannelAgentTools(params: {
cfg?: ClawdbotConfig;
}): ChannelAgentTool[] {
// Channel docking: aggregate channel-owned tools (login, etc.).
const tools: ChannelAgentTool[] = [];
for (const plugin of listChannelPlugins()) {
const entry = plugin.agentTools;
if (!entry) continue;
const resolved = typeof entry === "function" ? entry(params) : entry;
if (Array.isArray(resolved)) tools.push(...resolved);
}
return tools;
}

View File

@ -73,14 +73,14 @@ describe("sessions tools", () => {
kind: "direct", kind: "direct",
sessionId: "s-main", sessionId: "s-main",
updatedAt: 10, updatedAt: 10,
lastProvider: "whatsapp", lastChannel: "whatsapp",
}, },
{ {
key: "discord:group:dev", key: "discord:group:dev",
kind: "group", kind: "group",
sessionId: "s-group", sessionId: "s-group",
updatedAt: 11, updatedAt: 11,
provider: "discord", channel: "discord",
displayName: "discord:g-dev", displayName: "discord:g-dev",
}, },
{ {
@ -120,7 +120,7 @@ describe("sessions tools", () => {
}; };
expect(details.sessions).toHaveLength(3); expect(details.sessions).toHaveLength(3);
const main = details.sessions?.find((s) => s.key === "main"); const main = details.sessions?.find((s) => s.key === "main");
expect(main?.provider).toBe("whatsapp"); expect(main?.channel).toBe("whatsapp");
expect(main?.messages?.length).toBe(1); expect(main?.messages?.length).toBe(1);
expect(main?.messages?.[0]?.role).toBe("assistant"); expect(main?.messages?.[0]?.role).toBe("assistant");
@ -233,7 +233,7 @@ describe("sessions tools", () => {
const tool = createClawdbotTools({ const tool = createClawdbotTools({
agentSessionKey: requesterKey, agentSessionKey: requesterKey,
agentProvider: "discord", agentChannel: "discord",
}).find((candidate) => candidate.name === "sessions_send"); }).find((candidate) => candidate.name === "sessions_send");
expect(tool).toBeDefined(); expect(tool).toBeDefined();
if (!tool) throw new Error("missing sessions_send tool"); if (!tool) throw new Error("missing sessions_send tool");
@ -275,7 +275,7 @@ describe("sessions tools", () => {
for (const call of agentCalls) { for (const call of agentCalls) {
expect(call.params).toMatchObject({ expect(call.params).toMatchObject({
lane: "nested", lane: "nested",
provider: "webchat", channel: "webchat",
}); });
} }
expect( expect(
@ -321,7 +321,7 @@ describe("sessions tools", () => {
const replyByRunId = new Map<string, string>(); const replyByRunId = new Map<string, string>();
const requesterKey = "discord:group:req"; const requesterKey = "discord:group:req";
const targetKey = "discord:group:target"; const targetKey = "discord:group:target";
let sendParams: { to?: string; provider?: string; message?: string } = {}; let sendParams: { to?: string; channel?: string; message?: string } = {};
callGatewayMock.mockImplementation(async (opts: unknown) => { callGatewayMock.mockImplementation(async (opts: unknown) => {
const request = opts as { method?: string; params?: unknown }; const request = opts as { method?: string; params?: unknown };
calls.push(request); calls.push(request);
@ -371,11 +371,11 @@ describe("sessions tools", () => {
} }
if (request.method === "send") { if (request.method === "send") {
const params = request.params as const params = request.params as
| { to?: string; provider?: string; message?: string } | { to?: string; channel?: string; message?: string }
| undefined; | undefined;
sendParams = { sendParams = {
to: params?.to, to: params?.to,
provider: params?.provider, channel: params?.channel,
message: params?.message, message: params?.message,
}; };
return { messageId: "m-announce" }; return { messageId: "m-announce" };
@ -385,7 +385,7 @@ describe("sessions tools", () => {
const tool = createClawdbotTools({ const tool = createClawdbotTools({
agentSessionKey: requesterKey, agentSessionKey: requesterKey,
agentProvider: "discord", agentChannel: "discord",
}).find((candidate) => candidate.name === "sessions_send"); }).find((candidate) => candidate.name === "sessions_send");
expect(tool).toBeDefined(); expect(tool).toBeDefined();
if (!tool) throw new Error("missing sessions_send tool"); if (!tool) throw new Error("missing sessions_send tool");
@ -407,7 +407,7 @@ describe("sessions tools", () => {
for (const call of agentCalls) { for (const call of agentCalls) {
expect(call.params).toMatchObject({ expect(call.params).toMatchObject({
lane: "nested", lane: "nested",
provider: "webchat", channel: "webchat",
}); });
} }
@ -423,7 +423,7 @@ describe("sessions tools", () => {
expect(replySteps).toHaveLength(2); expect(replySteps).toHaveLength(2);
expect(sendParams).toMatchObject({ expect(sendParams).toMatchObject({
to: "channel:target", to: "channel:target",
provider: "discord", channel: "discord",
message: "announce now", message: "announce now",
}); });
}); });

View File

@ -37,12 +37,12 @@ describe("subagents", () => {
}; };
}); });
it("sessions_spawn announces back to the requester group provider", async () => { it("sessions_spawn announces back to the requester group channel", async () => {
resetSubagentRegistryForTests(); resetSubagentRegistryForTests();
callGatewayMock.mockReset(); callGatewayMock.mockReset();
const calls: Array<{ method?: string; params?: unknown }> = []; const calls: Array<{ method?: string; params?: unknown }> = [];
let agentCallCount = 0; let agentCallCount = 0;
let sendParams: { to?: string; provider?: string; message?: string } = {}; let sendParams: { to?: string; channel?: string; message?: string } = {};
let deletedKey: string | undefined; let deletedKey: string | undefined;
let childRunId: string | undefined; let childRunId: string | undefined;
let childSessionKey: string | undefined; let childSessionKey: string | undefined;
@ -58,7 +58,7 @@ describe("subagents", () => {
const params = request.params as { const params = request.params as {
message?: string; message?: string;
sessionKey?: string; sessionKey?: string;
provider?: string; channel?: string;
timeout?: number; timeout?: number;
}; };
const message = params?.message ?? ""; const message = params?.message ?? "";
@ -69,7 +69,7 @@ describe("subagents", () => {
childRunId = runId; childRunId = runId;
childSessionKey = sessionKey; childSessionKey = sessionKey;
sessionLastAssistantText.set(sessionKey, "result"); sessionLastAssistantText.set(sessionKey, "result");
expect(params?.provider).toBe("discord"); expect(params?.channel).toBe("discord");
expect(params?.timeout).toBe(1); expect(params?.timeout).toBe(1);
} }
return { return {
@ -96,11 +96,11 @@ describe("subagents", () => {
} }
if (request.method === "send") { if (request.method === "send") {
const params = request.params as const params = request.params as
| { to?: string; provider?: string; message?: string } | { to?: string; channel?: string; message?: string }
| undefined; | undefined;
sendParams = { sendParams = {
to: params?.to, to: params?.to,
provider: params?.provider, channel: params?.channel,
message: params?.message, message: params?.message,
}; };
return { messageId: "m-announce" }; return { messageId: "m-announce" };
@ -115,7 +115,7 @@ describe("subagents", () => {
const tool = createClawdbotTools({ const tool = createClawdbotTools({
agentSessionKey: "discord:group:req", agentSessionKey: "discord:group:req",
agentProvider: "discord", agentChannel: "discord",
}).find((candidate) => candidate.name === "sessions_spawn"); }).find((candidate) => candidate.name === "sessions_spawn");
if (!tool) throw new Error("missing sessions_spawn tool"); if (!tool) throw new Error("missing sessions_spawn tool");
@ -153,22 +153,22 @@ describe("subagents", () => {
lane?: string; lane?: string;
deliver?: boolean; deliver?: boolean;
sessionKey?: string; sessionKey?: string;
provider?: string; channel?: string;
} }
| undefined; | undefined;
expect(first?.lane).toBe("subagent"); expect(first?.lane).toBe("subagent");
expect(first?.deliver).toBe(false); expect(first?.deliver).toBe(false);
expect(first?.provider).toBe("discord"); expect(first?.channel).toBe("discord");
expect(first?.sessionKey?.startsWith("agent:main:subagent:")).toBe(true); expect(first?.sessionKey?.startsWith("agent:main:subagent:")).toBe(true);
expect(childSessionKey?.startsWith("agent:main:subagent:")).toBe(true); expect(childSessionKey?.startsWith("agent:main:subagent:")).toBe(true);
const second = agentCalls[1]?.params as const second = agentCalls[1]?.params as
| { provider?: string; deliver?: boolean; lane?: string } | { channel?: string; deliver?: boolean; lane?: string }
| undefined; | undefined;
expect(second?.lane).toBe("nested"); expect(second?.lane).toBe("nested");
expect(second?.deliver).toBe(false); expect(second?.deliver).toBe(false);
expect(second?.provider).toBe("webchat"); expect(second?.channel).toBe("webchat");
expect(sendParams.provider).toBe("discord"); expect(sendParams.channel).toBe("discord");
expect(sendParams.to).toBe("channel:req"); expect(sendParams.to).toBe("channel:req");
expect(sendParams.message ?? "").toContain("announce now"); expect(sendParams.message ?? "").toContain("announce now");
expect(sendParams.message ?? "").toContain("Stats:"); expect(sendParams.message ?? "").toContain("Stats:");
@ -180,7 +180,7 @@ describe("subagents", () => {
callGatewayMock.mockReset(); callGatewayMock.mockReset();
const calls: Array<{ method?: string; params?: unknown }> = []; const calls: Array<{ method?: string; params?: unknown }> = [];
let agentCallCount = 0; let agentCallCount = 0;
let sendParams: { to?: string; provider?: string; message?: string } = {}; let sendParams: { to?: string; channel?: string; message?: string } = {};
let deletedKey: string | undefined; let deletedKey: string | undefined;
let childRunId: string | undefined; let childRunId: string | undefined;
let childSessionKey: string | undefined; let childSessionKey: string | undefined;
@ -196,7 +196,7 @@ describe("subagents", () => {
const params = request.params as { const params = request.params as {
message?: string; message?: string;
sessionKey?: string; sessionKey?: string;
provider?: string; channel?: string;
timeout?: number; timeout?: number;
}; };
const message = params?.message ?? ""; const message = params?.message ?? "";
@ -207,7 +207,7 @@ describe("subagents", () => {
childRunId = runId; childRunId = runId;
childSessionKey = sessionKey; childSessionKey = sessionKey;
sessionLastAssistantText.set(sessionKey, "result"); sessionLastAssistantText.set(sessionKey, "result");
expect(params?.provider).toBe("discord"); expect(params?.channel).toBe("discord");
expect(params?.timeout).toBe(1); expect(params?.timeout).toBe(1);
} }
return { return {
@ -238,11 +238,11 @@ describe("subagents", () => {
} }
if (request.method === "send") { if (request.method === "send") {
const params = request.params as const params = request.params as
| { to?: string; provider?: string; message?: string } | { to?: string; channel?: string; message?: string }
| undefined; | undefined;
sendParams = { sendParams = {
to: params?.to, to: params?.to,
provider: params?.provider, channel: params?.channel,
message: params?.message, message: params?.message,
}; };
return { messageId: "m-announce" }; return { messageId: "m-announce" };
@ -257,7 +257,7 @@ describe("subagents", () => {
const tool = createClawdbotTools({ const tool = createClawdbotTools({
agentSessionKey: "discord:group:req", agentSessionKey: "discord:group:req",
agentProvider: "discord", agentChannel: "discord",
}).find((candidate) => candidate.name === "sessions_spawn"); }).find((candidate) => candidate.name === "sessions_spawn");
if (!tool) throw new Error("missing sessions_spawn tool"); if (!tool) throw new Error("missing sessions_spawn tool");
@ -282,13 +282,13 @@ describe("subagents", () => {
const agentCalls = calls.filter((call) => call.method === "agent"); const agentCalls = calls.filter((call) => call.method === "agent");
expect(agentCalls).toHaveLength(2); expect(agentCalls).toHaveLength(2);
const second = agentCalls[1]?.params as const second = agentCalls[1]?.params as
| { provider?: string; deliver?: boolean; lane?: string } | { channel?: string; deliver?: boolean; lane?: string }
| undefined; | undefined;
expect(second?.lane).toBe("nested"); expect(second?.lane).toBe("nested");
expect(second?.deliver).toBe(false); expect(second?.deliver).toBe(false);
expect(second?.provider).toBe("webchat"); expect(second?.channel).toBe("webchat");
expect(sendParams.provider).toBe("discord"); expect(sendParams.channel).toBe("discord");
expect(sendParams.to).toBe("channel:req"); expect(sendParams.to).toBe("channel:req");
expect(sendParams.message ?? "").toContain("announce now"); expect(sendParams.message ?? "").toContain("announce now");
expect(sendParams.message ?? "").toContain("Stats:"); expect(sendParams.message ?? "").toContain("Stats:");
@ -300,7 +300,7 @@ describe("subagents", () => {
callGatewayMock.mockReset(); callGatewayMock.mockReset();
const calls: Array<{ method?: string; params?: unknown }> = []; const calls: Array<{ method?: string; params?: unknown }> = [];
let agentCallCount = 0; let agentCallCount = 0;
let sendParams: { to?: string; provider?: string; message?: string } = {}; let sendParams: { to?: string; channel?: string; message?: string } = {};
let childRunId: string | undefined; let childRunId: string | undefined;
let childSessionKey: string | undefined; let childSessionKey: string | undefined;
const waitCalls: Array<{ runId?: string; timeoutMs?: number }> = []; const waitCalls: Array<{ runId?: string; timeoutMs?: number }> = [];
@ -314,7 +314,7 @@ describe("subagents", () => {
sessions: [ sessions: [
{ {
key: "main", key: "main",
lastProvider: "whatsapp", lastChannel: "whatsapp",
lastTo: "+123", lastTo: "+123",
}, },
], ],
@ -360,11 +360,11 @@ describe("subagents", () => {
} }
if (request.method === "send") { if (request.method === "send") {
const params = request.params as const params = request.params as
| { to?: string; provider?: string; message?: string } | { to?: string; channel?: string; message?: string }
| undefined; | undefined;
sendParams = { sendParams = {
to: params?.to, to: params?.to,
provider: params?.provider, channel: params?.channel,
message: params?.message, message: params?.message,
}; };
return { messageId: "m1" }; return { messageId: "m1" };
@ -377,7 +377,7 @@ describe("subagents", () => {
const tool = createClawdbotTools({ const tool = createClawdbotTools({
agentSessionKey: "main", agentSessionKey: "main",
agentProvider: "whatsapp", agentChannel: "whatsapp",
}).find((candidate) => candidate.name === "sessions_spawn"); }).find((candidate) => candidate.name === "sessions_spawn");
if (!tool) throw new Error("missing sessions_spawn tool"); if (!tool) throw new Error("missing sessions_spawn tool");
@ -407,7 +407,7 @@ describe("subagents", () => {
const childWait = waitCalls.find((call) => call.runId === childRunId); const childWait = waitCalls.find((call) => call.runId === childRunId);
expect(childWait?.timeoutMs).toBe(1000); expect(childWait?.timeoutMs).toBe(1000);
expect(sendParams.provider).toBe("whatsapp"); expect(sendParams.channel).toBe("whatsapp");
expect(sendParams.to).toBe("+123"); expect(sendParams.to).toBe("+123");
expect(sendParams.message ?? "").toContain("hello from sub"); expect(sendParams.message ?? "").toContain("hello from sub");
expect(sendParams.message ?? "").toContain("Stats:"); expect(sendParams.message ?? "").toContain("Stats:");
@ -420,7 +420,7 @@ describe("subagents", () => {
const tool = createClawdbotTools({ const tool = createClawdbotTools({
agentSessionKey: "main", agentSessionKey: "main",
agentProvider: "whatsapp", agentChannel: "whatsapp",
}).find((candidate) => candidate.name === "sessions_spawn"); }).find((candidate) => candidate.name === "sessions_spawn");
if (!tool) throw new Error("missing sessions_spawn tool"); if (!tool) throw new Error("missing sessions_spawn tool");
@ -470,7 +470,7 @@ describe("subagents", () => {
const tool = createClawdbotTools({ const tool = createClawdbotTools({
agentSessionKey: "main", agentSessionKey: "main",
agentProvider: "whatsapp", agentChannel: "whatsapp",
}).find((candidate) => candidate.name === "sessions_spawn"); }).find((candidate) => candidate.name === "sessions_spawn");
if (!tool) throw new Error("missing sessions_spawn tool"); if (!tool) throw new Error("missing sessions_spawn tool");
@ -522,7 +522,7 @@ describe("subagents", () => {
const tool = createClawdbotTools({ const tool = createClawdbotTools({
agentSessionKey: "main", agentSessionKey: "main",
agentProvider: "whatsapp", agentChannel: "whatsapp",
}).find((candidate) => candidate.name === "sessions_spawn"); }).find((candidate) => candidate.name === "sessions_spawn");
if (!tool) throw new Error("missing sessions_spawn tool"); if (!tool) throw new Error("missing sessions_spawn tool");
@ -574,7 +574,7 @@ describe("subagents", () => {
const tool = createClawdbotTools({ const tool = createClawdbotTools({
agentSessionKey: "main", agentSessionKey: "main",
agentProvider: "whatsapp", agentChannel: "whatsapp",
}).find((candidate) => candidate.name === "sessions_spawn"); }).find((candidate) => candidate.name === "sessions_spawn");
if (!tool) throw new Error("missing sessions_spawn tool"); if (!tool) throw new Error("missing sessions_spawn tool");
@ -612,7 +612,7 @@ describe("subagents", () => {
const tool = createClawdbotTools({ const tool = createClawdbotTools({
agentSessionKey: "main", agentSessionKey: "main",
agentProvider: "whatsapp", agentChannel: "whatsapp",
}).find((candidate) => candidate.name === "sessions_spawn"); }).find((candidate) => candidate.name === "sessions_spawn");
if (!tool) throw new Error("missing sessions_spawn tool"); if (!tool) throw new Error("missing sessions_spawn tool");
@ -710,7 +710,7 @@ describe("subagents", () => {
const tool = createClawdbotTools({ const tool = createClawdbotTools({
agentSessionKey: "agent:main:main", agentSessionKey: "agent:main:main",
agentProvider: "discord", agentChannel: "discord",
}).find((candidate) => candidate.name === "sessions_spawn"); }).find((candidate) => candidate.name === "sessions_spawn");
if (!tool) throw new Error("missing sessions_spawn tool"); if (!tool) throw new Error("missing sessions_spawn tool");
@ -754,7 +754,7 @@ describe("subagents", () => {
const tool = createClawdbotTools({ const tool = createClawdbotTools({
agentSessionKey: "agent:research:main", agentSessionKey: "agent:research:main",
agentProvider: "discord", agentChannel: "discord",
}).find((candidate) => candidate.name === "sessions_spawn"); }).find((candidate) => candidate.name === "sessions_spawn");
if (!tool) throw new Error("missing sessions_spawn tool"); if (!tool) throw new Error("missing sessions_spawn tool");
@ -804,7 +804,7 @@ describe("subagents", () => {
const tool = createClawdbotTools({ const tool = createClawdbotTools({
agentSessionKey: "main", agentSessionKey: "main",
agentProvider: "whatsapp", agentChannel: "whatsapp",
}).find((candidate) => candidate.name === "sessions_spawn"); }).find((candidate) => candidate.name === "sessions_spawn");
if (!tool) throw new Error("missing sessions_spawn tool"); if (!tool) throw new Error("missing sessions_spawn tool");
@ -840,7 +840,7 @@ describe("subagents", () => {
const tool = createClawdbotTools({ const tool = createClawdbotTools({
agentSessionKey: "main", agentSessionKey: "main",
agentProvider: "whatsapp", agentChannel: "whatsapp",
}).find((candidate) => candidate.name === "sessions_spawn"); }).find((candidate) => candidate.name === "sessions_spawn");
if (!tool) throw new Error("missing sessions_spawn tool"); if (!tool) throw new Error("missing sessions_spawn tool");

View File

@ -1,6 +1,6 @@
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import { resolvePluginTools } from "../plugins/tools.js"; import { resolvePluginTools } from "../plugins/tools.js";
import type { GatewayMessageProvider } from "../utils/message-provider.js"; import type { GatewayMessageChannel } from "../utils/message-channel.js";
import { resolveSessionAgentId } from "./agent-scope.js"; import { resolveSessionAgentId } from "./agent-scope.js";
import { createAgentsListTool } from "./tools/agents-list-tool.js"; import { createAgentsListTool } from "./tools/agents-list-tool.js";
import { createBrowserTool } from "./tools/browser-tool.js"; import { createBrowserTool } from "./tools/browser-tool.js";
@ -28,7 +28,7 @@ export function createClawdbotTools(options?: {
allowedControlHosts?: string[]; allowedControlHosts?: string[];
allowedControlPorts?: number[]; allowedControlPorts?: number[];
agentSessionKey?: string; agentSessionKey?: string;
agentProvider?: GatewayMessageProvider; agentChannel?: GatewayMessageChannel;
agentAccountId?: string; agentAccountId?: string;
agentDir?: string; agentDir?: string;
sandboxRoot?: string; sandboxRoot?: string;
@ -93,12 +93,12 @@ export function createClawdbotTools(options?: {
}), }),
createSessionsSendTool({ createSessionsSendTool({
agentSessionKey: options?.agentSessionKey, agentSessionKey: options?.agentSessionKey,
agentProvider: options?.agentProvider, agentChannel: options?.agentChannel,
sandboxed: options?.sandboxed, sandboxed: options?.sandboxed,
}), }),
createSessionsSpawnTool({ createSessionsSpawnTool({
agentSessionKey: options?.agentSessionKey, agentSessionKey: options?.agentSessionKey,
agentProvider: options?.agentProvider, agentChannel: options?.agentChannel,
sandboxed: options?.sandboxed, sandboxed: options?.sandboxed,
}), }),
createSessionStatusTool({ createSessionStatusTool({
@ -121,7 +121,7 @@ export function createClawdbotTools(options?: {
config: options?.config, config: options?.config,
}), }),
sessionKey: options?.agentSessionKey, sessionKey: options?.agentSessionKey,
messageProvider: options?.agentProvider, messageChannel: options?.agentChannel,
agentAccountId: options?.agentAccountId, agentAccountId: options?.agentAccountId,
sandboxed: options?.sandboxed, sandboxed: options?.sandboxed,
}, },

View File

@ -1,7 +1,7 @@
import { import {
getProviderPlugin, getChannelPlugin,
normalizeProviderId, normalizeChannelId,
} from "../providers/plugins/index.js"; } from "../channels/plugins/index.js";
export type MessagingToolSend = { export type MessagingToolSend = {
tool: string; tool: string;
@ -15,8 +15,8 @@ const CORE_MESSAGING_TOOLS = new Set(["sessions_send", "message"]);
// Provider docking: any plugin with `actions` opts into messaging tool handling. // Provider docking: any plugin with `actions` opts into messaging tool handling.
export function isMessagingTool(toolName: string): boolean { export function isMessagingTool(toolName: string): boolean {
if (CORE_MESSAGING_TOOLS.has(toolName)) return true; if (CORE_MESSAGING_TOOLS.has(toolName)) return true;
const providerId = normalizeProviderId(toolName); const providerId = normalizeChannelId(toolName);
return Boolean(providerId && getProviderPlugin(providerId)?.actions); return Boolean(providerId && getChannelPlugin(providerId)?.actions);
} }
export function isMessagingToolSendAction( export function isMessagingToolSendAction(
@ -28,9 +28,9 @@ export function isMessagingToolSendAction(
if (toolName === "message") { if (toolName === "message") {
return action === "send" || action === "thread-reply"; return action === "send" || action === "thread-reply";
} }
const providerId = normalizeProviderId(toolName); const providerId = normalizeChannelId(toolName);
if (!providerId) return false; if (!providerId) return false;
const plugin = getProviderPlugin(providerId); const plugin = getChannelPlugin(providerId);
if (!plugin?.actions?.extractToolSend) return false; if (!plugin?.actions?.extractToolSend) return false;
return Boolean(plugin.actions.extractToolSend({ args })?.to); return Boolean(plugin.actions.extractToolSend({ args })?.to);
} }
@ -40,8 +40,8 @@ export function normalizeTargetForProvider(
raw?: string, raw?: string,
): string | undefined { ): string | undefined {
if (!raw) return undefined; if (!raw) return undefined;
const providerId = normalizeProviderId(provider); const providerId = normalizeChannelId(provider);
const plugin = providerId ? getProviderPlugin(providerId) : undefined; const plugin = providerId ? getChannelPlugin(providerId) : undefined;
const normalized = const normalized =
plugin?.messaging?.normalizeTarget?.(raw) ?? plugin?.messaging?.normalizeTarget?.(raw) ??
(raw.trim().toLowerCase() || undefined); (raw.trim().toLowerCase() || undefined);

View File

@ -478,17 +478,23 @@ describe("getDmHistoryLimitFromSessionKey", () => {
}); });
it("returns dmHistoryLimit for telegram provider", () => { it("returns dmHistoryLimit for telegram provider", () => {
const config = { telegram: { dmHistoryLimit: 15 } } as ClawdbotConfig; const config = {
channels: { telegram: { dmHistoryLimit: 15 } },
} as ClawdbotConfig;
expect(getDmHistoryLimitFromSessionKey("telegram:dm:123", config)).toBe(15); expect(getDmHistoryLimitFromSessionKey("telegram:dm:123", config)).toBe(15);
}); });
it("returns dmHistoryLimit for whatsapp provider", () => { it("returns dmHistoryLimit for whatsapp provider", () => {
const config = { whatsapp: { dmHistoryLimit: 20 } } as ClawdbotConfig; const config = {
channels: { whatsapp: { dmHistoryLimit: 20 } },
} as ClawdbotConfig;
expect(getDmHistoryLimitFromSessionKey("whatsapp:dm:123", config)).toBe(20); expect(getDmHistoryLimitFromSessionKey("whatsapp:dm:123", config)).toBe(20);
}); });
it("returns dmHistoryLimit for agent-prefixed session keys", () => { it("returns dmHistoryLimit for agent-prefixed session keys", () => {
const config = { telegram: { dmHistoryLimit: 10 } } as ClawdbotConfig; const config = {
channels: { telegram: { dmHistoryLimit: 10 } },
} as ClawdbotConfig;
expect( expect(
getDmHistoryLimitFromSessionKey("agent:main:telegram:dm:123", config), getDmHistoryLimitFromSessionKey("agent:main:telegram:dm:123", config),
).toBe(10); ).toBe(10);
@ -496,8 +502,10 @@ describe("getDmHistoryLimitFromSessionKey", () => {
it("returns undefined for non-dm session kinds", () => { it("returns undefined for non-dm session kinds", () => {
const config = { const config = {
slack: { dmHistoryLimit: 10 }, channels: {
telegram: { dmHistoryLimit: 15 }, telegram: { dmHistoryLimit: 15 },
slack: { dmHistoryLimit: 10 },
},
} as ClawdbotConfig; } as ClawdbotConfig;
expect( expect(
getDmHistoryLimitFromSessionKey("agent:beta:slack:channel:C1", config), getDmHistoryLimitFromSessionKey("agent:beta:slack:channel:C1", config),
@ -508,14 +516,16 @@ describe("getDmHistoryLimitFromSessionKey", () => {
}); });
it("returns undefined for unknown provider", () => { it("returns undefined for unknown provider", () => {
const config = { telegram: { dmHistoryLimit: 15 } } as ClawdbotConfig; const config = {
channels: { telegram: { dmHistoryLimit: 15 } },
} as ClawdbotConfig;
expect( expect(
getDmHistoryLimitFromSessionKey("unknown:dm:123", config), getDmHistoryLimitFromSessionKey("unknown:dm:123", config),
).toBeUndefined(); ).toBeUndefined();
}); });
it("returns undefined when provider config has no dmHistoryLimit", () => { it("returns undefined when provider config has no dmHistoryLimit", () => {
const config = { telegram: {} } as ClawdbotConfig; const config = { channels: { telegram: {} } } as ClawdbotConfig;
expect( expect(
getDmHistoryLimitFromSessionKey("telegram:dm:123", config), getDmHistoryLimitFromSessionKey("telegram:dm:123", config),
).toBeUndefined(); ).toBeUndefined();
@ -533,7 +543,9 @@ describe("getDmHistoryLimitFromSessionKey", () => {
] as const; ] as const;
for (const provider of providers) { for (const provider of providers) {
const config = { [provider]: { dmHistoryLimit: 5 } } as ClawdbotConfig; const config = {
channels: { [provider]: { dmHistoryLimit: 5 } },
} as ClawdbotConfig;
expect( expect(
getDmHistoryLimitFromSessionKey(`${provider}:dm:123`, config), getDmHistoryLimitFromSessionKey(`${provider}:dm:123`, config),
).toBe(5); ).toBe(5);
@ -554,9 +566,11 @@ describe("getDmHistoryLimitFromSessionKey", () => {
for (const provider of providers) { for (const provider of providers) {
// Test per-DM override takes precedence // Test per-DM override takes precedence
const configWithOverride = { const configWithOverride = {
[provider]: { channels: {
dmHistoryLimit: 20, [provider]: {
dms: { user123: { historyLimit: 7 } }, dmHistoryLimit: 20,
dms: { user123: { historyLimit: 7 } },
},
}, },
} as ClawdbotConfig; } as ClawdbotConfig;
expect( expect(
@ -586,9 +600,11 @@ describe("getDmHistoryLimitFromSessionKey", () => {
it("returns per-DM override when set", () => { it("returns per-DM override when set", () => {
const config = { const config = {
telegram: { channels: {
dmHistoryLimit: 15, telegram: {
dms: { "123": { historyLimit: 5 } }, dmHistoryLimit: 15,
dms: { "123": { historyLimit: 5 } },
},
}, },
} as ClawdbotConfig; } as ClawdbotConfig;
expect(getDmHistoryLimitFromSessionKey("telegram:dm:123", config)).toBe(5); expect(getDmHistoryLimitFromSessionKey("telegram:dm:123", config)).toBe(5);
@ -596,9 +612,11 @@ describe("getDmHistoryLimitFromSessionKey", () => {
it("falls back to provider default when per-DM not set", () => { it("falls back to provider default when per-DM not set", () => {
const config = { const config = {
telegram: { channels: {
dmHistoryLimit: 15, telegram: {
dms: { "456": { historyLimit: 5 } }, dmHistoryLimit: 15,
dms: { "456": { historyLimit: 5 } },
},
}, },
} as ClawdbotConfig; } as ClawdbotConfig;
expect(getDmHistoryLimitFromSessionKey("telegram:dm:123", config)).toBe(15); expect(getDmHistoryLimitFromSessionKey("telegram:dm:123", config)).toBe(15);
@ -606,9 +624,11 @@ describe("getDmHistoryLimitFromSessionKey", () => {
it("returns per-DM override for agent-prefixed keys", () => { it("returns per-DM override for agent-prefixed keys", () => {
const config = { const config = {
telegram: { channels: {
dmHistoryLimit: 20, telegram: {
dms: { "789": { historyLimit: 3 } }, dmHistoryLimit: 20,
dms: { "789": { historyLimit: 3 } },
},
}, },
} as ClawdbotConfig; } as ClawdbotConfig;
expect( expect(
@ -618,9 +638,11 @@ describe("getDmHistoryLimitFromSessionKey", () => {
it("handles userId with colons (e.g., email)", () => { it("handles userId with colons (e.g., email)", () => {
const config = { const config = {
msteams: { channels: {
dmHistoryLimit: 10, msteams: {
dms: { "user@example.com": { historyLimit: 7 } }, dmHistoryLimit: 10,
dms: { "user@example.com": { historyLimit: 7 } },
},
}, },
} as ClawdbotConfig; } as ClawdbotConfig;
expect( expect(
@ -630,8 +652,10 @@ describe("getDmHistoryLimitFromSessionKey", () => {
it("returns undefined when per-DM historyLimit is not set", () => { it("returns undefined when per-DM historyLimit is not set", () => {
const config = { const config = {
telegram: { channels: {
dms: { "123": {} }, telegram: {
dms: { "123": {} },
},
}, },
} as ClawdbotConfig; } as ClawdbotConfig;
expect( expect(
@ -641,9 +665,11 @@ describe("getDmHistoryLimitFromSessionKey", () => {
it("returns 0 when per-DM historyLimit is explicitly 0 (unlimited)", () => { it("returns 0 when per-DM historyLimit is explicitly 0 (unlimited)", () => {
const config = { const config = {
telegram: { channels: {
dmHistoryLimit: 15, telegram: {
dms: { "123": { historyLimit: 0 } }, dmHistoryLimit: 15,
dms: { "123": { historyLimit: 0 } },
},
}, },
} as ClawdbotConfig; } as ClawdbotConfig;
expect(getDmHistoryLimitFromSessionKey("telegram:dm:123", config)).toBe(0); expect(getDmHistoryLimitFromSessionKey("telegram:dm:123", config)).toBe(0);

View File

@ -33,8 +33,8 @@ import type {
} from "../auto-reply/thinking.js"; } from "../auto-reply/thinking.js";
import { formatToolAggregate } from "../auto-reply/tool-meta.js"; import { formatToolAggregate } from "../auto-reply/tool-meta.js";
import { isCacheEnabled, resolveCacheTtlMs } from "../config/cache-utils.js"; import { isCacheEnabled, resolveCacheTtlMs } from "../config/cache-utils.js";
import { resolveChannelCapabilities } from "../config/channel-capabilities.js";
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import { resolveProviderCapabilities } from "../config/provider-capabilities.js";
import { getMachineDisplayName } from "../infra/machine-name.js"; import { getMachineDisplayName } from "../infra/machine-name.js";
import { registerUnhandledRejectionHandler } from "../infra/unhandled-rejections.js"; import { registerUnhandledRejectionHandler } from "../infra/unhandled-rejections.js";
import { createSubsystemLogger } from "../logging.js"; import { createSubsystemLogger } from "../logging.js";
@ -42,7 +42,7 @@ import {
type enqueueCommand, type enqueueCommand,
enqueueCommandInLane, enqueueCommandInLane,
} from "../process/command-queue.js"; } from "../process/command-queue.js";
import { normalizeMessageProvider } from "../utils/message-provider.js"; import { normalizeMessageChannel } from "../utils/message-channel.js";
import { isReasoningTagProvider } from "../utils/provider-utils.js"; import { isReasoningTagProvider } from "../utils/provider-utils.js";
import { resolveUserPath } from "../utils.js"; import { resolveUserPath } from "../utils.js";
import { resolveClawdbotAgentDir } from "./agent-paths.js"; import { resolveClawdbotAgentDir } from "./agent-paths.js";
@ -690,19 +690,19 @@ export function getDmHistoryLimitFromSessionKey(
// Map provider to config key // Map provider to config key
switch (provider) { switch (provider) {
case "telegram": case "telegram":
return getLimit(config.telegram); return getLimit(config.channels?.telegram);
case "whatsapp": case "whatsapp":
return getLimit(config.whatsapp); return getLimit(config.channels?.whatsapp);
case "discord": case "discord":
return getLimit(config.discord); return getLimit(config.channels?.discord);
case "slack": case "slack":
return getLimit(config.slack); return getLimit(config.channels?.slack);
case "signal": case "signal":
return getLimit(config.signal); return getLimit(config.channels?.signal);
case "imessage": case "imessage":
return getLimit(config.imessage); return getLimit(config.channels?.imessage);
case "msteams": case "msteams":
return getLimit(config.msteams); return getLimit(config.channels?.msteams);
default: default:
return undefined; return undefined;
} }
@ -1125,6 +1125,7 @@ function resolveModel(
export async function compactEmbeddedPiSession(params: { export async function compactEmbeddedPiSession(params: {
sessionId: string; sessionId: string;
sessionKey?: string; sessionKey?: string;
messageChannel?: string;
messageProvider?: string; messageProvider?: string;
agentAccountId?: string; agentAccountId?: string;
sessionFile: string; sessionFile: string;
@ -1258,7 +1259,7 @@ export async function compactEmbeddedPiSession(params: {
elevated: params.bashElevated, elevated: params.bashElevated,
}, },
sandbox, sandbox,
messageProvider: params.messageProvider, messageProvider: params.messageChannel ?? params.messageProvider,
agentAccountId: params.agentAccountId, agentAccountId: params.agentAccountId,
sessionKey: params.sessionKey ?? params.sessionId, sessionKey: params.sessionKey ?? params.sessionId,
agentDir, agentDir,
@ -1272,13 +1273,13 @@ export async function compactEmbeddedPiSession(params: {
}); });
logToolSchemasForGoogle({ tools, provider }); logToolSchemasForGoogle({ tools, provider });
const machineName = await getMachineDisplayName(); const machineName = await getMachineDisplayName();
const runtimeProvider = normalizeMessageProvider( const runtimeChannel = normalizeMessageChannel(
params.messageProvider, params.messageChannel ?? params.messageProvider,
); );
const runtimeCapabilities = runtimeProvider const runtimeCapabilities = runtimeChannel
? (resolveProviderCapabilities({ ? (resolveChannelCapabilities({
cfg: params.config, cfg: params.config,
provider: runtimeProvider, channel: runtimeChannel,
accountId: params.agentAccountId, accountId: params.agentAccountId,
}) ?? []) }) ?? [])
: undefined; : undefined;
@ -1288,7 +1289,7 @@ export async function compactEmbeddedPiSession(params: {
arch: os.arch(), arch: os.arch(),
node: process.version, node: process.version,
model: `${provider}/${modelId}`, model: `${provider}/${modelId}`,
provider: runtimeProvider, channel: runtimeChannel,
capabilities: runtimeCapabilities, capabilities: runtimeCapabilities,
}; };
const sandboxInfo = buildEmbeddedSandboxInfo( const sandboxInfo = buildEmbeddedSandboxInfo(
@ -1443,6 +1444,7 @@ export async function compactEmbeddedPiSession(params: {
export async function runEmbeddedPiAgent(params: { export async function runEmbeddedPiAgent(params: {
sessionId: string; sessionId: string;
sessionKey?: string; sessionKey?: string;
messageChannel?: string;
messageProvider?: string; messageProvider?: string;
agentAccountId?: string; agentAccountId?: string;
/** Current channel ID for auto-threading (Slack). */ /** Current channel ID for auto-threading (Slack). */
@ -1639,7 +1641,7 @@ export async function runEmbeddedPiAgent(params: {
attemptedThinking.add(thinkLevel); attemptedThinking.add(thinkLevel);
log.debug( log.debug(
`embedded run start: runId=${params.runId} sessionId=${params.sessionId} provider=${provider} model=${modelId} thinking=${thinkLevel} messageProvider=${params.messageProvider ?? "unknown"}`, `embedded run start: runId=${params.runId} sessionId=${params.sessionId} provider=${provider} model=${modelId} thinking=${thinkLevel} messageChannel=${params.messageChannel ?? params.messageProvider ?? "unknown"}`,
); );
await fs.mkdir(resolvedWorkspace, { recursive: true }); await fs.mkdir(resolvedWorkspace, { recursive: true });
@ -1698,7 +1700,7 @@ export async function runEmbeddedPiAgent(params: {
elevated: params.bashElevated, elevated: params.bashElevated,
}, },
sandbox, sandbox,
messageProvider: params.messageProvider, messageProvider: params.messageChannel ?? params.messageProvider,
agentAccountId: params.agentAccountId, agentAccountId: params.agentAccountId,
sessionKey: params.sessionKey ?? params.sessionId, sessionKey: params.sessionKey ?? params.sessionId,
agentDir, agentDir,

View File

@ -6,13 +6,13 @@ import type { AgentSession } from "@mariozechner/pi-coding-agent";
import { parseReplyDirectives } from "../auto-reply/reply/reply-directives.js"; import { parseReplyDirectives } from "../auto-reply/reply/reply-directives.js";
import type { ReasoningLevel } from "../auto-reply/thinking.js"; import type { ReasoningLevel } from "../auto-reply/thinking.js";
import { formatToolAggregate } from "../auto-reply/tool-meta.js"; import { formatToolAggregate } from "../auto-reply/tool-meta.js";
import {
getChannelPlugin,
normalizeChannelId,
} from "../channels/plugins/index.js";
import { resolveStateDir } from "../config/paths.js"; import { resolveStateDir } from "../config/paths.js";
import { emitAgentEvent } from "../infra/agent-events.js"; import { emitAgentEvent } from "../infra/agent-events.js";
import { createSubsystemLogger } from "../logging.js"; import { createSubsystemLogger } from "../logging.js";
import {
getProviderPlugin,
normalizeProviderId,
} from "../providers/plugins/index.js";
import { truncateUtf16Safe } from "../utils.js"; import { truncateUtf16Safe } from "../utils.js";
import type { BlockReplyChunking } from "./pi-embedded-block-chunker.js"; import type { BlockReplyChunking } from "./pi-embedded-block-chunker.js";
import { EmbeddedBlockChunker } from "./pi-embedded-block-chunker.js"; import { EmbeddedBlockChunker } from "./pi-embedded-block-chunker.js";
@ -124,15 +124,15 @@ function extractMessagingToolSend(
if (!toRaw) return undefined; if (!toRaw) return undefined;
const providerRaw = const providerRaw =
typeof args.provider === "string" ? args.provider.trim() : ""; typeof args.provider === "string" ? args.provider.trim() : "";
const providerId = providerRaw ? normalizeProviderId(providerRaw) : null; const providerId = providerRaw ? normalizeChannelId(providerRaw) : null;
const provider = const provider =
providerId ?? (providerRaw ? providerRaw.toLowerCase() : "message"); providerId ?? (providerRaw ? providerRaw.toLowerCase() : "message");
const to = normalizeTargetForProvider(provider, toRaw); const to = normalizeTargetForProvider(provider, toRaw);
return to ? { tool: toolName, provider, accountId, to } : undefined; return to ? { tool: toolName, provider, accountId, to } : undefined;
} }
const providerId = normalizeProviderId(toolName); const providerId = normalizeChannelId(toolName);
if (!providerId) return undefined; if (!providerId) return undefined;
const plugin = getProviderPlugin(providerId); const plugin = getChannelPlugin(providerId);
const extracted = plugin?.actions?.extractToolSend?.({ args }); const extracted = plugin?.actions?.extractToolSend?.({ args });
if (!extracted?.to) return undefined; if (!extracted?.to) return undefined;
const to = normalizeTargetForProvider(providerId, extracted.to); const to = normalizeTargetForProvider(providerId, extracted.to);

View File

@ -9,7 +9,7 @@ import {
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import { detectMime } from "../media/mime.js"; import { detectMime } from "../media/mime.js";
import { isSubagentSessionKey } from "../routing/session-key.js"; import { isSubagentSessionKey } from "../routing/session-key.js";
import { resolveGatewayMessageProvider } from "../utils/message-provider.js"; import { resolveGatewayMessageChannel } from "../utils/message-channel.js";
import { import {
resolveAgentConfig, resolveAgentConfig,
resolveAgentIdFromSessionKey, resolveAgentIdFromSessionKey,
@ -21,9 +21,9 @@ import {
type ExecToolDefaults, type ExecToolDefaults,
type ProcessToolDefaults, type ProcessToolDefaults,
} from "./bash-tools.js"; } from "./bash-tools.js";
import { listChannelAgentTools } from "./channel-tools.js";
import { createClawdbotTools } from "./clawdbot-tools.js"; import { createClawdbotTools } from "./clawdbot-tools.js";
import type { ModelAuthMode } from "./model-auth.js"; import type { ModelAuthMode } from "./model-auth.js";
import { listProviderAgentTools } from "./provider-tools.js";
import type { SandboxContext, SandboxToolPolicy } from "./sandbox.js"; import type { SandboxContext, SandboxToolPolicy } from "./sandbox.js";
import { assertSandboxPath } from "./sandbox-paths.js"; import { assertSandboxPath } from "./sandbox-paths.js";
import { cleanSchemaForGemini } from "./schema/clean-for-gemini.js"; import { cleanSchemaForGemini } from "./schema/clean-for-gemini.js";
@ -807,8 +807,8 @@ export function createClawdbotCodingTools(options?: {
execTool as unknown as AnyAgentTool, execTool as unknown as AnyAgentTool,
bashTool, bashTool,
processTool as unknown as AnyAgentTool, processTool as unknown as AnyAgentTool,
// Provider docking: include provider-defined agent tools (login, etc.). // Channel docking: include channel-defined agent tools (login, etc.).
...listProviderAgentTools({ cfg: options?.config }), ...listChannelAgentTools({ cfg: options?.config }),
...createClawdbotTools({ ...createClawdbotTools({
browserControlUrl: sandbox?.browser?.controlUrl, browserControlUrl: sandbox?.browser?.controlUrl,
allowHostBrowserControl: sandbox ? sandbox.browserAllowHostControl : true, allowHostBrowserControl: sandbox ? sandbox.browserAllowHostControl : true,
@ -816,7 +816,7 @@ export function createClawdbotCodingTools(options?: {
allowedControlHosts: sandbox?.browserAllowedControlHosts, allowedControlHosts: sandbox?.browserAllowedControlHosts,
allowedControlPorts: sandbox?.browserAllowedControlPorts, allowedControlPorts: sandbox?.browserAllowedControlPorts,
agentSessionKey: options?.sessionKey, agentSessionKey: options?.sessionKey,
agentProvider: resolveGatewayMessageProvider(options?.messageProvider), agentChannel: resolveGatewayMessageChannel(options?.messageProvider),
agentAccountId: options?.agentAccountId, agentAccountId: options?.agentAccountId,
agentDir: options?.agentDir, agentDir: options?.agentDir,
sandboxRoot, sandboxRoot,

View File

@ -1,17 +0,0 @@
import type { ClawdbotConfig } from "../config/config.js";
import { listProviderPlugins } from "../providers/plugins/index.js";
import type { ProviderAgentTool } from "../providers/plugins/types.js";
export function listProviderAgentTools(params: {
cfg?: ClawdbotConfig;
}): ProviderAgentTool[] {
// Provider docking: aggregate provider-owned tools (login, etc.).
const tools: ProviderAgentTool[] = [];
for (const plugin of listProviderPlugins()) {
const entry = plugin.agentTools;
if (!entry) continue;
const resolved = typeof entry === "function" ? entry(params) : entry;
if (Array.isArray(resolved)) tools.push(...resolved);
}
return tools;
}

View File

@ -14,6 +14,7 @@ import {
resolveProfile, resolveProfile,
} from "../browser/config.js"; } from "../browser/config.js";
import { DEFAULT_CLAWD_BROWSER_COLOR } from "../browser/constants.js"; import { DEFAULT_CLAWD_BROWSER_COLOR } from "../browser/constants.js";
import { CHANNEL_IDS } from "../channels/registry.js";
import { import {
type ClawdbotConfig, type ClawdbotConfig,
loadConfig, loadConfig,
@ -23,7 +24,6 @@ import {
canonicalizeMainSessionAlias, canonicalizeMainSessionAlias,
resolveAgentMainSessionKey, resolveAgentMainSessionKey,
} from "../config/sessions.js"; } from "../config/sessions.js";
import { PROVIDER_IDS } from "../providers/registry.js";
import { normalizeAgentId } from "../routing/session-key.js"; import { normalizeAgentId } from "../routing/session-key.js";
import { defaultRuntime } from "../runtime.js"; import { defaultRuntime } from "../runtime.js";
import { resolveUserPath } from "../utils.js"; import { resolveUserPath } from "../utils.js";
@ -188,7 +188,7 @@ const DEFAULT_TOOL_DENY = [
"nodes", "nodes",
"cron", "cron",
"gateway", "gateway",
...PROVIDER_IDS, ...CHANNEL_IDS,
]; ];
export const DEFAULT_SANDBOX_BROWSER_IMAGE = export const DEFAULT_SANDBOX_BROWSER_IMAGE =
"clawdbot-sandbox-browser:bookworm-slim"; "clawdbot-sandbox-browser:bookworm-slim";

View File

@ -8,7 +8,7 @@ import {
resolveStorePath, resolveStorePath,
} from "../config/sessions.js"; } from "../config/sessions.js";
import { callGateway } from "../gateway/call.js"; import { callGateway } from "../gateway/call.js";
import { INTERNAL_MESSAGE_PROVIDER } from "../utils/message-provider.js"; import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel.js";
import { AGENT_LANE_NESTED } from "./lanes.js"; import { AGENT_LANE_NESTED } from "./lanes.js";
import { readLatestAssistantReply, runAgentStep } from "./tools/agent-step.js"; import { readLatestAssistantReply, runAgentStep } from "./tools/agent-step.js";
import { resolveAnnounceTarget } from "./tools/sessions-announce-target.js"; import { resolveAnnounceTarget } from "./tools/sessions-announce-target.js";
@ -139,7 +139,7 @@ async function buildSubagentStatsLine(params: {
export function buildSubagentSystemPrompt(params: { export function buildSubagentSystemPrompt(params: {
requesterSessionKey?: string; requesterSessionKey?: string;
requesterProvider?: string; requesterChannel?: string;
childSessionKey: string; childSessionKey: string;
label?: string; label?: string;
task?: string; task?: string;
@ -182,8 +182,8 @@ export function buildSubagentSystemPrompt(params: {
params.requesterSessionKey params.requesterSessionKey
? `- Requester session: ${params.requesterSessionKey}.` ? `- Requester session: ${params.requesterSessionKey}.`
: undefined, : undefined,
params.requesterProvider params.requesterChannel
? `- Requester provider: ${params.requesterProvider}.` ? `- Requester channel: ${params.requesterChannel}.`
: undefined, : undefined,
`- Your session: ${params.childSessionKey}.`, `- Your session: ${params.childSessionKey}.`,
"", "",
@ -195,7 +195,7 @@ export function buildSubagentSystemPrompt(params: {
function buildSubagentAnnouncePrompt(params: { function buildSubagentAnnouncePrompt(params: {
requesterSessionKey?: string; requesterSessionKey?: string;
requesterProvider?: string; requesterChannel?: string;
announceChannel: string; announceChannel: string;
task: string; task: string;
subagentReply?: string; subagentReply?: string;
@ -205,10 +205,10 @@ function buildSubagentAnnouncePrompt(params: {
params.requesterSessionKey params.requesterSessionKey
? `Requester session: ${params.requesterSessionKey}.` ? `Requester session: ${params.requesterSessionKey}.`
: undefined, : undefined,
params.requesterProvider params.requesterChannel
? `Requester provider: ${params.requesterProvider}.` ? `Requester channel: ${params.requesterChannel}.`
: undefined, : undefined,
`Post target provider: ${params.announceChannel}.`, `Post target channel: ${params.announceChannel}.`,
`Original task: ${params.task}`, `Original task: ${params.task}`,
params.subagentReply params.subagentReply
? `Sub-agent result: ${params.subagentReply}` ? `Sub-agent result: ${params.subagentReply}`
@ -226,7 +226,7 @@ export async function runSubagentAnnounceFlow(params: {
childSessionKey: string; childSessionKey: string;
childRunId: string; childRunId: string;
requesterSessionKey: string; requesterSessionKey: string;
requesterProvider?: string; requesterChannel?: string;
requesterDisplayKey: string; requesterDisplayKey: string;
task: string; task: string;
timeoutMs: number; timeoutMs: number;
@ -269,8 +269,8 @@ export async function runSubagentAnnounceFlow(params: {
const announcePrompt = buildSubagentAnnouncePrompt({ const announcePrompt = buildSubagentAnnouncePrompt({
requesterSessionKey: params.requesterSessionKey, requesterSessionKey: params.requesterSessionKey,
requesterProvider: params.requesterProvider, requesterChannel: params.requesterChannel,
announceChannel: announceTarget.provider, announceChannel: announceTarget.channel,
task: params.task, task: params.task,
subagentReply: reply, subagentReply: reply,
}); });
@ -280,7 +280,7 @@ export async function runSubagentAnnounceFlow(params: {
message: "Sub-agent announce step.", message: "Sub-agent announce step.",
extraSystemPrompt: announcePrompt, extraSystemPrompt: announcePrompt,
timeoutMs: params.timeoutMs, timeoutMs: params.timeoutMs,
provider: INTERNAL_MESSAGE_PROVIDER, channel: INTERNAL_MESSAGE_CHANNEL,
lane: AGENT_LANE_NESTED, lane: AGENT_LANE_NESTED,
}); });
@ -305,7 +305,7 @@ export async function runSubagentAnnounceFlow(params: {
params: { params: {
to: announceTarget.to, to: announceTarget.to,
message, message,
provider: announceTarget.provider, channel: announceTarget.channel,
accountId: announceTarget.accountId, accountId: announceTarget.accountId,
idempotencyKey: crypto.randomUUID(), idempotencyKey: crypto.randomUUID(),
}, },

View File

@ -8,7 +8,7 @@ export type SubagentRunRecord = {
runId: string; runId: string;
childSessionKey: string; childSessionKey: string;
requesterSessionKey: string; requesterSessionKey: string;
requesterProvider?: string; requesterChannel?: string;
requesterDisplayKey: string; requesterDisplayKey: string;
task: string; task: string;
cleanup: "delete" | "keep"; cleanup: "delete" | "keep";
@ -105,7 +105,7 @@ function ensureListener() {
childSessionKey: entry.childSessionKey, childSessionKey: entry.childSessionKey,
childRunId: entry.runId, childRunId: entry.runId,
requesterSessionKey: entry.requesterSessionKey, requesterSessionKey: entry.requesterSessionKey,
requesterProvider: entry.requesterProvider, requesterChannel: entry.requesterChannel,
requesterDisplayKey: entry.requesterDisplayKey, requesterDisplayKey: entry.requesterDisplayKey,
task: entry.task, task: entry.task,
timeoutMs: 30_000, timeoutMs: 30_000,
@ -133,7 +133,7 @@ export function registerSubagentRun(params: {
runId: string; runId: string;
childSessionKey: string; childSessionKey: string;
requesterSessionKey: string; requesterSessionKey: string;
requesterProvider?: string; requesterChannel?: string;
requesterDisplayKey: string; requesterDisplayKey: string;
task: string; task: string;
cleanup: "delete" | "keep"; cleanup: "delete" | "keep";
@ -152,7 +152,7 @@ export function registerSubagentRun(params: {
runId: params.runId, runId: params.runId,
childSessionKey: params.childSessionKey, childSessionKey: params.childSessionKey,
requesterSessionKey: params.requesterSessionKey, requesterSessionKey: params.requesterSessionKey,
requesterProvider: params.requesterProvider, requesterChannel: params.requesterChannel,
requesterDisplayKey: params.requesterDisplayKey, requesterDisplayKey: params.requesterDisplayKey,
task: params.task, task: params.task,
cleanup: params.cleanup, cleanup: params.cleanup,
@ -191,7 +191,7 @@ async function waitForSubagentCompletion(runId: string, waitTimeoutMs: number) {
childSessionKey: entry.childSessionKey, childSessionKey: entry.childSessionKey,
childRunId: entry.runId, childRunId: entry.runId,
requesterSessionKey: entry.requesterSessionKey, requesterSessionKey: entry.requesterSessionKey,
requesterProvider: entry.requesterProvider, requesterChannel: entry.requesterChannel,
requesterDisplayKey: entry.requesterDisplayKey, requesterDisplayKey: entry.requesterDisplayKey,
task: entry.task, task: entry.task,
timeoutMs: 30_000, timeoutMs: 30_000,

View File

@ -153,7 +153,7 @@ describe("buildAgentSystemPrompt", () => {
toolNames: ["message"], toolNames: ["message"],
}); });
expect(prompt).toContain("message: Send messages and provider actions"); expect(prompt).toContain("message: Send messages and channel actions");
expect(prompt).toContain("### message tool"); expect(prompt).toContain("### message tool");
}); });
@ -161,12 +161,12 @@ describe("buildAgentSystemPrompt", () => {
const prompt = buildAgentSystemPrompt({ const prompt = buildAgentSystemPrompt({
workspaceDir: "/tmp/clawd", workspaceDir: "/tmp/clawd",
runtimeInfo: { runtimeInfo: {
provider: "telegram", channel: "telegram",
capabilities: ["inlineButtons"], capabilities: ["inlineButtons"],
}, },
}); });
expect(prompt).toContain("provider=telegram"); expect(prompt).toContain("channel=telegram");
expect(prompt).toContain("capabilities=inlineButtons"); expect(prompt).toContain("capabilities=inlineButtons");
}); });

View File

@ -1,9 +1,9 @@
import type { ReasoningLevel, ThinkLevel } from "../auto-reply/thinking.js"; import type { ReasoningLevel, ThinkLevel } from "../auto-reply/thinking.js";
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js"; import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
import { PROVIDER_IDS } from "../providers/registry.js"; import { CHANNEL_IDS } from "../channels/registry.js";
import type { EmbeddedContextFile } from "./pi-embedded-helpers.js"; import type { EmbeddedContextFile } from "./pi-embedded-helpers.js";
const MESSAGE_PROVIDER_OPTIONS = PROVIDER_IDS.join("|"); const MESSAGE_CHANNEL_OPTIONS = CHANNEL_IDS.join("|");
export function buildAgentSystemPrompt(params: { export function buildAgentSystemPrompt(params: {
workspaceDir: string; workspaceDir: string;
@ -26,7 +26,7 @@ export function buildAgentSystemPrompt(params: {
arch?: string; arch?: string;
node?: string; node?: string;
model?: string; model?: string;
provider?: string; channel?: string;
capabilities?: string[]; capabilities?: string[];
}; };
sandboxInfo?: { sandboxInfo?: {
@ -56,12 +56,12 @@ export function buildAgentSystemPrompt(params: {
ls: "List directory contents", ls: "List directory contents",
exec: "Run shell commands", exec: "Run shell commands",
process: "Manage background exec sessions", process: "Manage background exec sessions",
// Provider docking: add provider login tools here when a provider needs interactive linking. // Channel docking: add login tools here when a channel needs interactive linking.
browser: "Control web browser", browser: "Control web browser",
canvas: "Present/eval/snapshot the Canvas", canvas: "Present/eval/snapshot the Canvas",
nodes: "List/describe/notify/camera/screen on paired nodes", nodes: "List/describe/notify/camera/screen on paired nodes",
cron: "Manage cron jobs and wake events (use for reminders)", cron: "Manage cron jobs and wake events (use for reminders)",
message: "Send messages and provider actions", message: "Send messages and channel actions",
gateway: gateway:
"Restart, apply config, or run updates on the running Clawdbot process", "Restart, apply config, or run updates on the running Clawdbot process",
agents_list: "List agent ids allowed for sessions_spawn", agents_list: "List agent ids allowed for sessions_spawn",
@ -166,7 +166,7 @@ export function buildAgentSystemPrompt(params: {
? `Heartbeat prompt: ${heartbeatPrompt}` ? `Heartbeat prompt: ${heartbeatPrompt}`
: "Heartbeat prompt: (configured)"; : "Heartbeat prompt: (configured)";
const runtimeInfo = params.runtimeInfo; const runtimeInfo = params.runtimeInfo;
const runtimeProvider = runtimeInfo?.provider?.trim().toLowerCase(); const runtimeChannel = runtimeInfo?.channel?.trim().toLowerCase();
const runtimeCapabilities = (runtimeInfo?.capabilities ?? []) const runtimeCapabilities = (runtimeInfo?.capabilities ?? [])
.map((cap) => String(cap).trim()) .map((cap) => String(cap).trim())
.filter(Boolean); .filter(Boolean);
@ -322,23 +322,23 @@ export function buildAgentSystemPrompt(params: {
"- [[reply_to_current]] replies to the triggering message.", "- [[reply_to_current]] replies to the triggering message.",
"- [[reply_to:<id>]] replies to a specific message id when you have it.", "- [[reply_to:<id>]] replies to a specific message id when you have it.",
"Whitespace inside the tag is allowed (e.g. [[ reply_to_current ]] / [[ reply_to: 123 ]]).", "Whitespace inside the tag is allowed (e.g. [[ reply_to_current ]] / [[ reply_to: 123 ]]).",
"Tags are stripped before sending; support depends on the current provider config.", "Tags are stripped before sending; support depends on the current channel config.",
"", "",
"## Messaging", "## Messaging",
"- Reply in current session → automatically routes to the source provider (Signal, Telegram, etc.)", "- Reply in current session → automatically routes to the source channel (Signal, Telegram, etc.)",
"- Cross-session messaging → use sessions_send(sessionKey, message)", "- Cross-session messaging → use sessions_send(sessionKey, message)",
"- Never use exec/curl for provider messaging; Clawdbot handles all routing internally.", "- Never use exec/curl for provider messaging; Clawdbot handles all routing internally.",
availableTools.has("message") availableTools.has("message")
? [ ? [
"", "",
"### message tool", "### message tool",
"- Use `message` for proactive sends + provider actions (polls, reactions, etc.).", "- Use `message` for proactive sends + channel actions (polls, reactions, etc.).",
"- For `action=send`, include `to` and `message`.", "- For `action=send`, include `to` and `message`.",
`- If multiple providers are configured, pass \`provider\` (${MESSAGE_PROVIDER_OPTIONS}).`, `- If multiple channels are configured, pass \`channel\` (${MESSAGE_CHANNEL_OPTIONS}).`,
inlineButtonsEnabled inlineButtonsEnabled
? "- Inline buttons supported. Use `action=send` with `buttons=[[{text,callback_data}]]` (callback_data routes back as a user message)." ? "- Inline buttons supported. Use `action=send` with `buttons=[[{text,callback_data}]]` (callback_data routes back as a user message)."
: runtimeProvider : runtimeChannel
? `- Inline buttons not enabled for ${runtimeProvider}. If you need them, ask to add "inlineButtons" to ${runtimeProvider}.capabilities or ${runtimeProvider}.accounts.<id>.capabilities.` ? `- Inline buttons not enabled for ${runtimeChannel}. If you need them, ask to add "inlineButtons" to ${runtimeChannel}.capabilities or ${runtimeChannel}.accounts.<id>.capabilities.`
: "", : "",
] ]
.filter(Boolean) .filter(Boolean)
@ -397,8 +397,8 @@ export function buildAgentSystemPrompt(params: {
: "", : "",
runtimeInfo?.node ? `node=${runtimeInfo.node}` : "", runtimeInfo?.node ? `node=${runtimeInfo.node}` : "",
runtimeInfo?.model ? `model=${runtimeInfo.model}` : "", runtimeInfo?.model ? `model=${runtimeInfo.model}` : "",
runtimeProvider ? `provider=${runtimeProvider}` : "", runtimeChannel ? `channel=${runtimeChannel}` : "",
runtimeProvider runtimeChannel
? `capabilities=${ ? `capabilities=${
runtimeCapabilities.length > 0 runtimeCapabilities.length > 0
? runtimeCapabilities.join(",") ? runtimeCapabilities.join(",")

View File

@ -1,7 +1,7 @@
import crypto from "node:crypto"; import crypto from "node:crypto";
import { callGateway } from "../../gateway/call.js"; import { callGateway } from "../../gateway/call.js";
import { INTERNAL_MESSAGE_PROVIDER } from "../../utils/message-provider.js"; import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
import { AGENT_LANE_NESTED } from "../lanes.js"; import { AGENT_LANE_NESTED } from "../lanes.js";
import { extractAssistantText, stripToolMessages } from "./sessions-helpers.js"; import { extractAssistantText, stripToolMessages } from "./sessions-helpers.js";
@ -25,7 +25,7 @@ export async function runAgentStep(params: {
message: string; message: string;
extraSystemPrompt: string; extraSystemPrompt: string;
timeoutMs: number; timeoutMs: number;
provider?: string; channel?: string;
lane?: string; lane?: string;
}): Promise<string | undefined> { }): Promise<string | undefined> {
const stepIdem = crypto.randomUUID(); const stepIdem = crypto.randomUUID();
@ -36,7 +36,7 @@ export async function runAgentStep(params: {
sessionKey: params.sessionKey, sessionKey: params.sessionKey,
idempotencyKey: stepIdem, idempotencyKey: stepIdem,
deliver: false, deliver: false,
provider: params.provider ?? INTERNAL_MESSAGE_PROVIDER, channel: params.channel ?? INTERNAL_MESSAGE_CHANNEL,
lane: params.lane ?? AGENT_LANE_NESTED, lane: params.lane ?? AGENT_LANE_NESTED,
extraSystemPrompt: params.extraSystemPrompt, extraSystemPrompt: params.extraSystemPrompt,
}, },

View File

@ -56,7 +56,7 @@ export async function handleDiscordAction(
cfg: ClawdbotConfig, cfg: ClawdbotConfig,
): Promise<AgentToolResult<unknown>> { ): Promise<AgentToolResult<unknown>> {
const action = readStringParam(params, "action", { required: true }); const action = readStringParam(params, "action", { required: true });
const isActionEnabled = createActionGate(cfg.discord?.actions); const isActionEnabled = createActionGate(cfg.channels?.discord?.actions);
if (messagingActions.has(action)) { if (messagingActions.has(action)) {
return await handleDiscordMessagingAction(action, params, isActionEnabled); return await handleDiscordMessagingAction(action, params, isActionEnabled);

View File

@ -2,7 +2,7 @@ import { callGateway } from "../../gateway/call.js";
import { import {
GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_MODES,
GATEWAY_CLIENT_NAMES, GATEWAY_CLIENT_NAMES,
} from "../../utils/message-provider.js"; } from "../../utils/message-channel.js";
export const DEFAULT_GATEWAY_URL = "ws://127.0.0.1:18789"; export const DEFAULT_GATEWAY_URL = "ws://127.0.0.1:18789";

View File

@ -1,5 +1,12 @@
import { Type } from "@sinclair/typebox"; import { Type } from "@sinclair/typebox";
import {
listChannelMessageActions,
supportsChannelMessageButtons,
} from "../../channels/plugins/message-actions.js";
import {
CHANNEL_MESSAGE_ACTION_NAMES,
type ChannelMessageActionName,
} from "../../channels/plugins/types.js";
import type { ClawdbotConfig } from "../../config/config.js"; import type { ClawdbotConfig } from "../../config/config.js";
import { loadConfig } from "../../config/config.js"; import { loadConfig } from "../../config/config.js";
import { import {
@ -7,23 +14,15 @@ import {
GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_MODES,
} from "../../gateway/protocol/client-info.js"; } from "../../gateway/protocol/client-info.js";
import { runMessageAction } from "../../infra/outbound/message-action-runner.js"; import { runMessageAction } from "../../infra/outbound/message-action-runner.js";
import {
listProviderMessageActions,
supportsProviderMessageButtons,
} from "../../providers/plugins/message-actions.js";
import {
PROVIDER_MESSAGE_ACTION_NAMES,
type ProviderMessageActionName,
} from "../../providers/plugins/types.js";
import { normalizeAccountId } from "../../routing/session-key.js"; import { normalizeAccountId } from "../../routing/session-key.js";
import { stringEnum } from "../schema/typebox.js"; import { stringEnum } from "../schema/typebox.js";
import type { AnyAgentTool } from "./common.js"; import type { AnyAgentTool } from "./common.js";
import { jsonResult, readNumberParam, readStringParam } from "./common.js"; import { jsonResult, readNumberParam, readStringParam } from "./common.js";
const AllMessageActions = PROVIDER_MESSAGE_ACTION_NAMES; const AllMessageActions = CHANNEL_MESSAGE_ACTION_NAMES;
const MessageToolCommonSchema = { const MessageToolCommonSchema = {
provider: Type.Optional(Type.String()), channel: Type.Optional(Type.String()),
to: Type.Optional(Type.String()), to: Type.Optional(Type.String()),
message: Type.Optional(Type.String()), message: Type.Optional(Type.String()),
media: Type.Optional(Type.String()), media: Type.Optional(Type.String()),
@ -131,8 +130,8 @@ type MessageToolOptions = {
}; };
function buildMessageToolSchema(cfg: ClawdbotConfig) { function buildMessageToolSchema(cfg: ClawdbotConfig) {
const actions = listProviderMessageActions(cfg); const actions = listChannelMessageActions(cfg);
const includeButtons = supportsProviderMessageButtons(cfg); const includeButtons = supportsChannelMessageButtons(cfg);
return buildMessageToolSchemaFromActions( return buildMessageToolSchemaFromActions(
actions.length > 0 ? actions : ["send"], actions.length > 0 ? actions : ["send"],
{ includeButtons }, { includeButtons },
@ -155,14 +154,14 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
label: "Message", label: "Message",
name: "message", name: "message",
description: description:
"Send messages and provider actions (polls, reactions, pins, threads, etc.) via configured provider plugins.", "Send messages and channel actions (polls, reactions, pins, threads, etc.) via configured channel plugins.",
parameters: schema, parameters: schema,
execute: async (_toolCallId, args) => { execute: async (_toolCallId, args) => {
const params = args as Record<string, unknown>; const params = args as Record<string, unknown>;
const cfg = options?.config ?? loadConfig(); const cfg = options?.config ?? loadConfig();
const action = readStringParam(params, "action", { const action = readStringParam(params, "action", {
required: true, required: true,
}) as ProviderMessageActionName; }) as ChannelMessageActionName;
const accountId = readStringParam(params, "accountId") ?? agentAccountId; const accountId = readStringParam(params, "accountId") ?? agentAccountId;
const gateway = { const gateway = {

View File

@ -322,8 +322,8 @@ export function createSessionStatusTool(opts?: {
const queueSettings = resolveQueueSettings({ const queueSettings = resolveQueueSettings({
cfg, cfg,
provider: channel:
resolved.entry.provider ?? resolved.entry.lastProvider ?? "unknown", resolved.entry.channel ?? resolved.entry.lastChannel ?? "unknown",
sessionEntry: resolved.entry, sessionEntry: resolved.entry,
}); });
const queueKey = resolved.key ?? resolved.entry.sessionId; const queueKey = resolved.key ?? resolved.entry.sessionId;

View File

@ -17,7 +17,7 @@ describe("resolveAnnounceTarget", () => {
sessionKey: "agent:main:discord:group:dev", sessionKey: "agent:main:discord:group:dev",
displayKey: "agent:main:discord:group:dev", displayKey: "agent:main:discord:group:dev",
}); });
expect(target).toEqual({ provider: "discord", to: "channel:dev" }); expect(target).toEqual({ channel: "discord", to: "channel:dev" });
expect(callGatewayMock).not.toHaveBeenCalled(); expect(callGatewayMock).not.toHaveBeenCalled();
}); });
@ -26,7 +26,7 @@ describe("resolveAnnounceTarget", () => {
sessions: [ sessions: [
{ {
key: "agent:main:whatsapp:group:123@g.us", key: "agent:main:whatsapp:group:123@g.us",
lastProvider: "whatsapp", lastChannel: "whatsapp",
lastTo: "123@g.us", lastTo: "123@g.us",
lastAccountId: "work", lastAccountId: "work",
}, },
@ -38,7 +38,7 @@ describe("resolveAnnounceTarget", () => {
displayKey: "agent:main:whatsapp:group:123@g.us", displayKey: "agent:main:whatsapp:group:123@g.us",
}); });
expect(target).toEqual({ expect(target).toEqual({
provider: "whatsapp", channel: "whatsapp",
to: "123@g.us", to: "123@g.us",
accountId: "work", accountId: "work",
}); });

View File

@ -1,8 +1,8 @@
import { callGateway } from "../../gateway/call.js";
import { import {
getProviderPlugin, getChannelPlugin,
normalizeProviderId, normalizeChannelId,
} from "../../providers/plugins/index.js"; } from "../../channels/plugins/index.js";
import { callGateway } from "../../gateway/call.js";
import type { AnnounceTarget } from "./sessions-send-helpers.js"; import type { AnnounceTarget } from "./sessions-send-helpers.js";
import { resolveAnnounceTargetFromKey } from "./sessions-send-helpers.js"; import { resolveAnnounceTargetFromKey } from "./sessions-send-helpers.js";
@ -15,8 +15,8 @@ export async function resolveAnnounceTarget(params: {
const fallback = parsed ?? parsedDisplay ?? null; const fallback = parsed ?? parsedDisplay ?? null;
if (fallback) { if (fallback) {
const normalized = normalizeProviderId(fallback.provider); const normalized = normalizeChannelId(fallback.channel);
const plugin = normalized ? getProviderPlugin(normalized) : null; const plugin = normalized ? getChannelPlugin(normalized) : null;
if (!plugin?.meta?.preferSessionLookupForAnnounceTarget) { if (!plugin?.meta?.preferSessionLookupForAnnounceTarget) {
return fallback; return fallback;
} }
@ -35,14 +35,14 @@ export async function resolveAnnounceTarget(params: {
const match = const match =
sessions.find((entry) => entry?.key === params.sessionKey) ?? sessions.find((entry) => entry?.key === params.sessionKey) ??
sessions.find((entry) => entry?.key === params.displayKey); sessions.find((entry) => entry?.key === params.displayKey);
const provider = const channel =
typeof match?.lastProvider === "string" ? match.lastProvider : undefined; typeof match?.lastChannel === "string" ? match.lastChannel : undefined;
const to = typeof match?.lastTo === "string" ? match.lastTo : undefined; const to = typeof match?.lastTo === "string" ? match.lastTo : undefined;
const accountId = const accountId =
typeof match?.lastAccountId === "string" typeof match?.lastAccountId === "string"
? match.lastAccountId ? match.lastAccountId
: undefined; : undefined;
if (provider && to) return { provider, to, accountId }; if (channel && to) return { channel, to, accountId };
} catch { } catch {
// ignore // ignore
} }

View File

@ -56,11 +56,11 @@ export function classifySessionKind(params: {
return "other"; return "other";
} }
export function deriveProvider(params: { export function deriveChannel(params: {
key: string; key: string;
kind: SessionKind; kind: SessionKind;
provider?: string | null; channel?: string | null;
lastProvider?: string | null; lastChannel?: string | null;
}): string { }): string {
if ( if (
params.kind === "cron" || params.kind === "cron" ||
@ -68,10 +68,10 @@ export function deriveProvider(params: {
params.kind === "node" params.kind === "node"
) )
return "internal"; return "internal";
const provider = normalizeKey(params.provider ?? undefined); const channel = normalizeKey(params.channel ?? undefined);
if (provider) return provider; if (channel) return channel;
const lastProvider = normalizeKey(params.lastProvider ?? undefined); const lastChannel = normalizeKey(params.lastChannel ?? undefined);
if (lastProvider) return lastProvider; if (lastChannel) return lastChannel;
const parts = params.key.split(":").filter(Boolean); const parts = params.key.split(":").filter(Boolean);
if (parts.length >= 3 && (parts[1] === "group" || parts[1] === "channel")) { if (parts.length >= 3 && (parts[1] === "group" || parts[1] === "channel")) {
return parts[0]; return parts[0];

View File

@ -13,7 +13,7 @@ import type { AnyAgentTool } from "./common.js";
import { jsonResult, readStringArrayParam } from "./common.js"; import { jsonResult, readStringArrayParam } from "./common.js";
import { import {
classifySessionKind, classifySessionKind,
deriveProvider, deriveChannel,
resolveDisplaySessionKey, resolveDisplaySessionKey,
resolveInternalSessionKey, resolveInternalSessionKey,
resolveMainSessionAlias, resolveMainSessionAlias,
@ -24,7 +24,7 @@ import {
type SessionListRow = { type SessionListRow = {
key: string; key: string;
kind: SessionKind; kind: SessionKind;
provider: string; channel: string;
label?: string; label?: string;
displayName?: string; displayName?: string;
updatedAt?: number | null; updatedAt?: number | null;
@ -37,7 +37,7 @@ type SessionListRow = {
systemSent?: boolean; systemSent?: boolean;
abortedLastRun?: boolean; abortedLastRun?: boolean;
sendPolicy?: string; sendPolicy?: string;
lastProvider?: string; lastChannel?: string;
lastTo?: string; lastTo?: string;
lastAccountId?: string; lastAccountId?: string;
transcriptPath?: string; transcriptPath?: string;
@ -178,21 +178,19 @@ export function createSessionsListTool(opts?: {
mainKey, mainKey,
}); });
const entryProvider = const entryChannel =
typeof entry.provider === "string" ? entry.provider : undefined; typeof entry.channel === "string" ? entry.channel : undefined;
const lastProvider = const lastChannel =
typeof entry.lastProvider === "string" typeof entry.lastChannel === "string" ? entry.lastChannel : undefined;
? entry.lastProvider
: undefined;
const lastAccountId = const lastAccountId =
typeof entry.lastAccountId === "string" typeof entry.lastAccountId === "string"
? entry.lastAccountId ? entry.lastAccountId
: undefined; : undefined;
const derivedProvider = deriveProvider({ const derivedChannel = deriveChannel({
key, key,
kind, kind,
provider: entryProvider, channel: entryChannel,
lastProvider, lastChannel,
}); });
const sessionId = const sessionId =
@ -205,7 +203,7 @@ export function createSessionsListTool(opts?: {
const row: SessionListRow = { const row: SessionListRow = {
key: displayKey, key: displayKey,
kind, kind,
provider: derivedProvider, channel: derivedChannel,
label: typeof entry.label === "string" ? entry.label : undefined, label: typeof entry.label === "string" ? entry.label : undefined,
displayName: displayName:
typeof entry.displayName === "string" typeof entry.displayName === "string"
@ -241,7 +239,7 @@ export function createSessionsListTool(opts?: {
: undefined, : undefined,
sendPolicy: sendPolicy:
typeof entry.sendPolicy === "string" ? entry.sendPolicy : undefined, typeof entry.sendPolicy === "string" ? entry.sendPolicy : undefined,
lastProvider, lastChannel,
lastTo: typeof entry.lastTo === "string" ? entry.lastTo : undefined, lastTo: typeof entry.lastTo === "string" ? entry.lastTo : undefined,
lastAccountId, lastAccountId,
transcriptPath, transcriptPath,

View File

@ -1,8 +1,8 @@
import type { ClawdbotConfig } from "../../config/config.js";
import { import {
getProviderPlugin, getChannelPlugin,
normalizeProviderId, normalizeChannelId,
} from "../../providers/plugins/index.js"; } from "../../channels/plugins/index.js";
import type { ClawdbotConfig } from "../../config/config.js";
const ANNOUNCE_SKIP_TOKEN = "ANNOUNCE_SKIP"; const ANNOUNCE_SKIP_TOKEN = "ANNOUNCE_SKIP";
const REPLY_SKIP_TOKEN = "REPLY_SKIP"; const REPLY_SKIP_TOKEN = "REPLY_SKIP";
@ -10,7 +10,7 @@ const DEFAULT_PING_PONG_TURNS = 5;
const MAX_PING_PONG_TURNS = 5; const MAX_PING_PONG_TURNS = 5;
export type AnnounceTarget = { export type AnnounceTarget = {
provider: string; channel: string;
to: string; to: string;
accountId?: string; accountId?: string;
}; };
@ -24,29 +24,29 @@ export function resolveAnnounceTargetFromKey(
? rawParts.slice(2) ? rawParts.slice(2)
: rawParts; : rawParts;
if (parts.length < 3) return null; if (parts.length < 3) return null;
const [providerRaw, kind, ...rest] = parts; const [channelRaw, kind, ...rest] = parts;
if (kind !== "group" && kind !== "channel") return null; if (kind !== "group" && kind !== "channel") return null;
const id = rest.join(":").trim(); const id = rest.join(":").trim();
if (!id) return null; if (!id) return null;
if (!providerRaw) return null; if (!channelRaw) return null;
const normalizedProvider = normalizeProviderId(providerRaw); const normalizedChannel = normalizeChannelId(channelRaw);
const provider = normalizedProvider ?? providerRaw.toLowerCase(); const channel = normalizedChannel ?? channelRaw.toLowerCase();
const kindTarget = normalizedProvider const kindTarget = normalizedChannel
? kind === "channel" ? kind === "channel"
? `channel:${id}` ? `channel:${id}`
: `group:${id}` : `group:${id}`
: id; : id;
const normalized = normalizedProvider const normalized = normalizedChannel
? getProviderPlugin(normalizedProvider)?.messaging?.normalizeTarget?.( ? getChannelPlugin(normalizedChannel)?.messaging?.normalizeTarget?.(
kindTarget, kindTarget,
) )
: undefined; : undefined;
return { provider, to: normalized ?? kindTarget }; return { channel, to: normalized ?? kindTarget };
} }
export function buildAgentToAgentMessageContext(params: { export function buildAgentToAgentMessageContext(params: {
requesterSessionKey?: string; requesterSessionKey?: string;
requesterProvider?: string; requesterChannel?: string;
targetSessionKey: string; targetSessionKey: string;
}) { }) {
const lines = [ const lines = [
@ -54,8 +54,8 @@ export function buildAgentToAgentMessageContext(params: {
params.requesterSessionKey params.requesterSessionKey
? `Agent 1 (requester) session: ${params.requesterSessionKey}.` ? `Agent 1 (requester) session: ${params.requesterSessionKey}.`
: undefined, : undefined,
params.requesterProvider params.requesterChannel
? `Agent 1 (requester) provider: ${params.requesterProvider}.` ? `Agent 1 (requester) channel: ${params.requesterChannel}.`
: undefined, : undefined,
`Agent 2 (target) session: ${params.targetSessionKey}.`, `Agent 2 (target) session: ${params.targetSessionKey}.`,
].filter(Boolean); ].filter(Boolean);
@ -64,9 +64,9 @@ export function buildAgentToAgentMessageContext(params: {
export function buildAgentToAgentReplyContext(params: { export function buildAgentToAgentReplyContext(params: {
requesterSessionKey?: string; requesterSessionKey?: string;
requesterProvider?: string; requesterChannel?: string;
targetSessionKey: string; targetSessionKey: string;
targetProvider?: string; targetChannel?: string;
currentRole: "requester" | "target"; currentRole: "requester" | "target";
turn: number; turn: number;
maxTurns: number; maxTurns: number;
@ -82,12 +82,12 @@ export function buildAgentToAgentReplyContext(params: {
params.requesterSessionKey params.requesterSessionKey
? `Agent 1 (requester) session: ${params.requesterSessionKey}.` ? `Agent 1 (requester) session: ${params.requesterSessionKey}.`
: undefined, : undefined,
params.requesterProvider params.requesterChannel
? `Agent 1 (requester) provider: ${params.requesterProvider}.` ? `Agent 1 (requester) channel: ${params.requesterChannel}.`
: undefined, : undefined,
`Agent 2 (target) session: ${params.targetSessionKey}.`, `Agent 2 (target) session: ${params.targetSessionKey}.`,
params.targetProvider params.targetChannel
? `Agent 2 (target) provider: ${params.targetProvider}.` ? `Agent 2 (target) channel: ${params.targetChannel}.`
: undefined, : undefined,
`If you want to stop the ping-pong, reply exactly "${REPLY_SKIP_TOKEN}".`, `If you want to stop the ping-pong, reply exactly "${REPLY_SKIP_TOKEN}".`,
].filter(Boolean); ].filter(Boolean);
@ -96,9 +96,9 @@ export function buildAgentToAgentReplyContext(params: {
export function buildAgentToAgentAnnounceContext(params: { export function buildAgentToAgentAnnounceContext(params: {
requesterSessionKey?: string; requesterSessionKey?: string;
requesterProvider?: string; requesterChannel?: string;
targetSessionKey: string; targetSessionKey: string;
targetProvider?: string; targetChannel?: string;
originalMessage: string; originalMessage: string;
roundOneReply?: string; roundOneReply?: string;
latestReply?: string; latestReply?: string;
@ -108,12 +108,12 @@ export function buildAgentToAgentAnnounceContext(params: {
params.requesterSessionKey params.requesterSessionKey
? `Agent 1 (requester) session: ${params.requesterSessionKey}.` ? `Agent 1 (requester) session: ${params.requesterSessionKey}.`
: undefined, : undefined,
params.requesterProvider params.requesterChannel
? `Agent 1 (requester) provider: ${params.requesterProvider}.` ? `Agent 1 (requester) channel: ${params.requesterChannel}.`
: undefined, : undefined,
`Agent 2 (target) session: ${params.targetSessionKey}.`, `Agent 2 (target) session: ${params.targetSessionKey}.`,
params.targetProvider params.targetChannel
? `Agent 2 (target) provider: ${params.targetProvider}.` ? `Agent 2 (target) channel: ${params.targetChannel}.`
: undefined, : undefined,
`Original request: ${params.originalMessage}`, `Original request: ${params.originalMessage}`,
params.roundOneReply params.roundOneReply
@ -123,7 +123,7 @@ export function buildAgentToAgentAnnounceContext(params: {
? `Latest reply: ${params.latestReply}` ? `Latest reply: ${params.latestReply}`
: "Latest reply: (not available).", : "Latest reply: (not available).",
`If you want to remain silent, reply exactly "${ANNOUNCE_SKIP_TOKEN}".`, `If you want to remain silent, reply exactly "${ANNOUNCE_SKIP_TOKEN}".`,
"Any other reply will be posted to the target provider.", "Any other reply will be posted to the target channel.",
"After this reply, the agent-to-agent conversation is over.", "After this reply, the agent-to-agent conversation is over.",
].filter(Boolean); ].filter(Boolean);
return lines.join("\n"); return lines.join("\n");

View File

@ -28,7 +28,7 @@ describe("sessions_send gating", () => {
it("blocks cross-agent sends when tools.agentToAgent.enabled is false", async () => { it("blocks cross-agent sends when tools.agentToAgent.enabled is false", async () => {
const tool = createSessionsSendTool({ const tool = createSessionsSendTool({
agentSessionKey: "agent:main:main", agentSessionKey: "agent:main:main",
agentProvider: "whatsapp", agentChannel: "whatsapp",
}); });
const result = await tool.execute("call1", { const result = await tool.execute("call1", {

View File

@ -13,9 +13,9 @@ import {
} from "../../routing/session-key.js"; } from "../../routing/session-key.js";
import { SESSION_LABEL_MAX_LENGTH } from "../../sessions/session-label.js"; import { SESSION_LABEL_MAX_LENGTH } from "../../sessions/session-label.js";
import { import {
type GatewayMessageProvider, type GatewayMessageChannel,
INTERNAL_MESSAGE_PROVIDER, INTERNAL_MESSAGE_CHANNEL,
} from "../../utils/message-provider.js"; } from "../../utils/message-channel.js";
import { AGENT_LANE_NESTED } from "../lanes.js"; import { AGENT_LANE_NESTED } from "../lanes.js";
import { readLatestAssistantReply, runAgentStep } from "./agent-step.js"; import { readLatestAssistantReply, runAgentStep } from "./agent-step.js";
import type { AnyAgentTool } from "./common.js"; import type { AnyAgentTool } from "./common.js";
@ -51,7 +51,7 @@ const SessionsSendToolSchema = Type.Object({
export function createSessionsSendTool(opts?: { export function createSessionsSendTool(opts?: {
agentSessionKey?: string; agentSessionKey?: string;
agentProvider?: GatewayMessageProvider; agentChannel?: GatewayMessageChannel;
sandboxed?: boolean; sandboxed?: boolean;
}): AnyAgentTool { }): AnyAgentTool {
return { return {
@ -297,7 +297,7 @@ export function createSessionsSendTool(opts?: {
const agentMessageContext = buildAgentToAgentMessageContext({ const agentMessageContext = buildAgentToAgentMessageContext({
requesterSessionKey: opts?.agentSessionKey, requesterSessionKey: opts?.agentSessionKey,
requesterProvider: opts?.agentProvider, requesterChannel: opts?.agentChannel,
targetSessionKey: displayKey, targetSessionKey: displayKey,
}); });
const sendParams = { const sendParams = {
@ -305,12 +305,12 @@ export function createSessionsSendTool(opts?: {
sessionKey: resolvedKey, sessionKey: resolvedKey,
idempotencyKey, idempotencyKey,
deliver: false, deliver: false,
provider: INTERNAL_MESSAGE_PROVIDER, channel: INTERNAL_MESSAGE_CHANNEL,
lane: AGENT_LANE_NESTED, lane: AGENT_LANE_NESTED,
extraSystemPrompt: agentMessageContext, extraSystemPrompt: agentMessageContext,
}; };
const requesterSessionKey = opts?.agentSessionKey; const requesterSessionKey = opts?.agentSessionKey;
const requesterProvider = opts?.agentProvider; const requesterChannel = opts?.agentChannel;
const maxPingPongTurns = resolvePingPongTurns(cfg); const maxPingPongTurns = resolvePingPongTurns(cfg);
const delivery = { status: "pending", mode: "announce" as const }; const delivery = { status: "pending", mode: "announce" as const };
@ -344,7 +344,7 @@ export function createSessionsSendTool(opts?: {
sessionKey: resolvedKey, sessionKey: resolvedKey,
displayKey, displayKey,
}); });
const targetProvider = announceTarget?.provider ?? "unknown"; const targetChannel = announceTarget?.channel ?? "unknown";
if ( if (
maxPingPongTurns > 0 && maxPingPongTurns > 0 &&
requesterSessionKey && requesterSessionKey &&
@ -360,9 +360,9 @@ export function createSessionsSendTool(opts?: {
: "target"; : "target";
const replyPrompt = buildAgentToAgentReplyContext({ const replyPrompt = buildAgentToAgentReplyContext({
requesterSessionKey, requesterSessionKey,
requesterProvider, requesterChannel,
targetSessionKey: displayKey, targetSessionKey: displayKey,
targetProvider, targetChannel,
currentRole, currentRole,
turn, turn,
maxTurns: maxPingPongTurns, maxTurns: maxPingPongTurns,
@ -386,9 +386,9 @@ export function createSessionsSendTool(opts?: {
} }
const announcePrompt = buildAgentToAgentAnnounceContext({ const announcePrompt = buildAgentToAgentAnnounceContext({
requesterSessionKey, requesterSessionKey,
requesterProvider, requesterChannel,
targetSessionKey: displayKey, targetSessionKey: displayKey,
targetProvider, targetChannel,
originalMessage: message, originalMessage: message,
roundOneReply: primaryReply, roundOneReply: primaryReply,
latestReply, latestReply,
@ -412,7 +412,7 @@ export function createSessionsSendTool(opts?: {
params: { params: {
to: announceTarget.to, to: announceTarget.to,
message: announceReply.trim(), message: announceReply.trim(),
provider: announceTarget.provider, channel: announceTarget.channel,
accountId: announceTarget.accountId, accountId: announceTarget.accountId,
idempotencyKey: crypto.randomUUID(), idempotencyKey: crypto.randomUUID(),
}, },
@ -421,7 +421,7 @@ export function createSessionsSendTool(opts?: {
} catch (err) { } catch (err) {
log.warn("sessions_send announce delivery failed", { log.warn("sessions_send announce delivery failed", {
runId: runContextId, runId: runContextId,
provider: announceTarget.provider, channel: announceTarget.channel,
to: announceTarget.to, to: announceTarget.to,
error: formatErrorMessage(err), error: formatErrorMessage(err),
}); });

View File

@ -9,7 +9,7 @@ import {
normalizeAgentId, normalizeAgentId,
parseAgentSessionKey, parseAgentSessionKey,
} from "../../routing/session-key.js"; } from "../../routing/session-key.js";
import type { GatewayMessageProvider } from "../../utils/message-provider.js"; import type { GatewayMessageChannel } from "../../utils/message-channel.js";
import { resolveAgentConfig } from "../agent-scope.js"; import { resolveAgentConfig } from "../agent-scope.js";
import { AGENT_LANE_SUBAGENT } from "../lanes.js"; import { AGENT_LANE_SUBAGENT } from "../lanes.js";
import { optionalStringEnum } from "../schema/typebox.js"; import { optionalStringEnum } from "../schema/typebox.js";
@ -47,7 +47,7 @@ function normalizeModelSelection(value: unknown): string | undefined {
export function createSessionsSpawnTool(opts?: { export function createSessionsSpawnTool(opts?: {
agentSessionKey?: string; agentSessionKey?: string;
agentProvider?: GatewayMessageProvider; agentChannel?: GatewayMessageChannel;
sandboxed?: boolean; sandboxed?: boolean;
}): AnyAgentTool { }): AnyAgentTool {
return { return {
@ -174,7 +174,7 @@ export function createSessionsSpawnTool(opts?: {
} }
const childSystemPrompt = buildSubagentSystemPrompt({ const childSystemPrompt = buildSubagentSystemPrompt({
requesterSessionKey, requesterSessionKey,
requesterProvider: opts?.agentProvider, requesterChannel: opts?.agentChannel,
childSessionKey, childSessionKey,
label: label || undefined, label: label || undefined,
task, task,
@ -188,7 +188,7 @@ export function createSessionsSpawnTool(opts?: {
params: { params: {
message: task, message: task,
sessionKey: childSessionKey, sessionKey: childSessionKey,
provider: opts?.agentProvider, channel: opts?.agentChannel,
idempotencyKey: childIdem, idempotencyKey: childIdem,
deliver: false, deliver: false,
lane: AGENT_LANE_SUBAGENT, lane: AGENT_LANE_SUBAGENT,
@ -221,7 +221,7 @@ export function createSessionsSpawnTool(opts?: {
runId: childRunId, runId: childRunId,
childSessionKey, childSessionKey,
requesterSessionKey: requesterInternalKey, requesterSessionKey: requesterInternalKey,
requesterProvider: opts?.agentProvider, requesterChannel: opts?.agentChannel,
requesterDisplayKey, requesterDisplayKey,
task, task,
cleanup, cleanup,

View File

@ -36,7 +36,7 @@ vi.mock("../../slack/actions.js", () => ({
describe("handleSlackAction", () => { describe("handleSlackAction", () => {
it("adds reactions", async () => { it("adds reactions", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig; const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
await handleSlackAction( await handleSlackAction(
{ {
action: "react", action: "react",
@ -50,7 +50,7 @@ describe("handleSlackAction", () => {
}); });
it("removes reactions on empty emoji", async () => { it("removes reactions on empty emoji", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig; const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
await handleSlackAction( await handleSlackAction(
{ {
action: "react", action: "react",
@ -64,7 +64,7 @@ describe("handleSlackAction", () => {
}); });
it("removes reactions when remove flag set", async () => { it("removes reactions when remove flag set", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig; const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
await handleSlackAction( await handleSlackAction(
{ {
action: "react", action: "react",
@ -79,7 +79,7 @@ describe("handleSlackAction", () => {
}); });
it("rejects removes without emoji", async () => { it("rejects removes without emoji", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig; const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
await expect( await expect(
handleSlackAction( handleSlackAction(
{ {
@ -96,7 +96,7 @@ describe("handleSlackAction", () => {
it("respects reaction gating", async () => { it("respects reaction gating", async () => {
const cfg = { const cfg = {
slack: { botToken: "tok", actions: { reactions: false } }, channels: { slack: { botToken: "tok", actions: { reactions: false } } },
} as ClawdbotConfig; } as ClawdbotConfig;
await expect( await expect(
handleSlackAction( handleSlackAction(
@ -112,7 +112,7 @@ describe("handleSlackAction", () => {
}); });
it("passes threadTs to sendSlackMessage for thread replies", async () => { it("passes threadTs to sendSlackMessage for thread replies", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig; const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
await handleSlackAction( await handleSlackAction(
{ {
action: "sendMessage", action: "sendMessage",
@ -133,7 +133,7 @@ describe("handleSlackAction", () => {
}); });
it("auto-injects threadTs from context when replyToMode=all", async () => { it("auto-injects threadTs from context when replyToMode=all", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig; const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
sendSlackMessage.mockClear(); sendSlackMessage.mockClear();
await handleSlackAction( await handleSlackAction(
{ {
@ -159,7 +159,7 @@ describe("handleSlackAction", () => {
}); });
it("replyToMode=first threads first message then stops", async () => { it("replyToMode=first threads first message then stops", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig; const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
sendSlackMessage.mockClear(); sendSlackMessage.mockClear();
const hasRepliedRef = { value: false }; const hasRepliedRef = { value: false };
const context = { const context = {
@ -198,7 +198,7 @@ describe("handleSlackAction", () => {
}); });
it("replyToMode=first marks hasRepliedRef even when threadTs is explicit", async () => { it("replyToMode=first marks hasRepliedRef even when threadTs is explicit", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig; const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
sendSlackMessage.mockClear(); sendSlackMessage.mockClear();
const hasRepliedRef = { value: false }; const hasRepliedRef = { value: false };
const context = { const context = {
@ -244,7 +244,7 @@ describe("handleSlackAction", () => {
}); });
it("replyToMode=first without hasRepliedRef does not thread", async () => { it("replyToMode=first without hasRepliedRef does not thread", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig; const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
sendSlackMessage.mockClear(); sendSlackMessage.mockClear();
await handleSlackAction( await handleSlackAction(
{ action: "sendMessage", to: "channel:C123", content: "No ref" }, { action: "sendMessage", to: "channel:C123", content: "No ref" },
@ -263,7 +263,7 @@ describe("handleSlackAction", () => {
}); });
it("does not auto-inject threadTs when replyToMode=off", async () => { it("does not auto-inject threadTs when replyToMode=off", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig; const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
sendSlackMessage.mockClear(); sendSlackMessage.mockClear();
await handleSlackAction( await handleSlackAction(
{ {
@ -285,7 +285,7 @@ describe("handleSlackAction", () => {
}); });
it("does not auto-inject threadTs when sending to different channel", async () => { it("does not auto-inject threadTs when sending to different channel", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig; const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
sendSlackMessage.mockClear(); sendSlackMessage.mockClear();
await handleSlackAction( await handleSlackAction(
{ {
@ -311,7 +311,7 @@ describe("handleSlackAction", () => {
}); });
it("explicit threadTs overrides context threadTs", async () => { it("explicit threadTs overrides context threadTs", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig; const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
sendSlackMessage.mockClear(); sendSlackMessage.mockClear();
await handleSlackAction( await handleSlackAction(
{ {
@ -338,7 +338,7 @@ describe("handleSlackAction", () => {
}); });
it("handles channel target without prefix when replyToMode=all", async () => { it("handles channel target without prefix when replyToMode=all", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig; const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
sendSlackMessage.mockClear(); sendSlackMessage.mockClear();
await handleSlackAction( await handleSlackAction(
{ {

View File

@ -93,7 +93,7 @@ export async function handleSlackAction(
const accountId = readStringParam(params, "accountId"); const accountId = readStringParam(params, "accountId");
const accountOpts = accountId ? { accountId } : undefined; const accountOpts = accountId ? { accountId } : undefined;
const account = resolveSlackAccount({ cfg, accountId }); const account = resolveSlackAccount({ cfg, accountId });
const actionConfig = account.actions ?? cfg.slack?.actions; const actionConfig = account.actions ?? cfg.channels?.slack?.actions;
const isActionEnabled = createActionGate(actionConfig); const isActionEnabled = createActionGate(actionConfig);
if (reactionsActions.has(action)) { if (reactionsActions.has(action)) {

View File

@ -34,7 +34,9 @@ describe("handleTelegramAction", () => {
}); });
it("adds reactions", async () => { it("adds reactions", async () => {
const cfg = { telegram: { botToken: "tok" } } as ClawdbotConfig; const cfg = {
channels: { telegram: { botToken: "tok" } },
} as ClawdbotConfig;
await handleTelegramAction( await handleTelegramAction(
{ {
action: "react", action: "react",
@ -53,7 +55,9 @@ describe("handleTelegramAction", () => {
}); });
it("removes reactions on empty emoji", async () => { it("removes reactions on empty emoji", async () => {
const cfg = { telegram: { botToken: "tok" } } as ClawdbotConfig; const cfg = {
channels: { telegram: { botToken: "tok" } },
} as ClawdbotConfig;
await handleTelegramAction( await handleTelegramAction(
{ {
action: "react", action: "react",
@ -72,7 +76,9 @@ describe("handleTelegramAction", () => {
}); });
it("removes reactions when remove flag set", async () => { it("removes reactions when remove flag set", async () => {
const cfg = { telegram: { botToken: "tok" } } as ClawdbotConfig; const cfg = {
channels: { telegram: { botToken: "tok" } },
} as ClawdbotConfig;
await handleTelegramAction( await handleTelegramAction(
{ {
action: "react", action: "react",
@ -93,7 +99,9 @@ describe("handleTelegramAction", () => {
it("respects reaction gating", async () => { it("respects reaction gating", async () => {
const cfg = { const cfg = {
telegram: { botToken: "tok", actions: { reactions: false } }, channels: {
telegram: { botToken: "tok", actions: { reactions: false } },
},
} as ClawdbotConfig; } as ClawdbotConfig;
await expect( await expect(
handleTelegramAction( handleTelegramAction(
@ -109,7 +117,9 @@ describe("handleTelegramAction", () => {
}); });
it("sends a text message", async () => { it("sends a text message", async () => {
const cfg = { telegram: { botToken: "tok" } } as ClawdbotConfig; const cfg = {
channels: { telegram: { botToken: "tok" } },
} as ClawdbotConfig;
const result = await handleTelegramAction( const result = await handleTelegramAction(
{ {
action: "sendMessage", action: "sendMessage",
@ -130,7 +140,9 @@ describe("handleTelegramAction", () => {
}); });
it("sends a message with media", async () => { it("sends a message with media", async () => {
const cfg = { telegram: { botToken: "tok" } } as ClawdbotConfig; const cfg = {
channels: { telegram: { botToken: "tok" } },
} as ClawdbotConfig;
await handleTelegramAction( await handleTelegramAction(
{ {
action: "sendMessage", action: "sendMessage",
@ -152,7 +164,9 @@ describe("handleTelegramAction", () => {
it("respects sendMessage gating", async () => { it("respects sendMessage gating", async () => {
const cfg = { const cfg = {
telegram: { botToken: "tok", actions: { sendMessage: false } }, channels: {
telegram: { botToken: "tok", actions: { sendMessage: false } },
},
} as ClawdbotConfig; } as ClawdbotConfig;
await expect( await expect(
handleTelegramAction( handleTelegramAction(
@ -182,7 +196,9 @@ describe("handleTelegramAction", () => {
}); });
it("requires inlineButtons capability when buttons are provided", async () => { it("requires inlineButtons capability when buttons are provided", async () => {
const cfg = { telegram: { botToken: "tok" } } as ClawdbotConfig; const cfg = {
channels: { telegram: { botToken: "tok" } },
} as ClawdbotConfig;
await expect( await expect(
handleTelegramAction( handleTelegramAction(
{ {
@ -198,7 +214,9 @@ describe("handleTelegramAction", () => {
it("sends messages with inline keyboard buttons when enabled", async () => { it("sends messages with inline keyboard buttons when enabled", async () => {
const cfg = { const cfg = {
telegram: { botToken: "tok", capabilities: ["inlineButtons"] }, channels: {
telegram: { botToken: "tok", capabilities: ["inlineButtons"] },
},
} as ClawdbotConfig; } as ClawdbotConfig;
await handleTelegramAction( await handleTelegramAction(
{ {

View File

@ -1,7 +1,6 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core"; import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import { resolveChannelCapabilities } from "../../config/channel-capabilities.js";
import type { ClawdbotConfig } from "../../config/config.js"; import type { ClawdbotConfig } from "../../config/config.js";
import { resolveProviderCapabilities } from "../../config/provider-capabilities.js";
import { import {
reactMessageTelegram, reactMessageTelegram,
sendMessageTelegram, sendMessageTelegram,
@ -26,9 +25,9 @@ function hasInlineButtonsCapability(params: {
accountId?: string | undefined; accountId?: string | undefined;
}): boolean { }): boolean {
const caps = const caps =
resolveProviderCapabilities({ resolveChannelCapabilities({
cfg: params.cfg, cfg: params.cfg,
provider: "telegram", channel: "telegram",
accountId: params.accountId, accountId: params.accountId,
}) ?? []; }) ?? [];
return caps.some((cap) => cap.toLowerCase() === "inlinebuttons"); return caps.some((cap) => cap.toLowerCase() === "inlinebuttons");
@ -84,7 +83,7 @@ export async function handleTelegramAction(
): Promise<AgentToolResult<unknown>> { ): Promise<AgentToolResult<unknown>> {
const action = readStringParam(params, "action", { required: true }); const action = readStringParam(params, "action", { required: true });
const accountId = readStringParam(params, "accountId"); const accountId = readStringParam(params, "accountId");
const isActionEnabled = createActionGate(cfg.telegram?.actions); const isActionEnabled = createActionGate(cfg.channels?.telegram?.actions);
if (action === "react") { if (action === "react") {
if (!isActionEnabled("reactions")) { if (!isActionEnabled("reactions")) {
@ -103,7 +102,7 @@ export async function handleTelegramAction(
const token = resolveTelegramToken(cfg, { accountId }).token; const token = resolveTelegramToken(cfg, { accountId }).token;
if (!token) { if (!token) {
throw new Error( throw new Error(
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or telegram.botToken.", "Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or channels.telegram.botToken.",
); );
} }
await reactMessageTelegram(chatId ?? "", messageId ?? 0, emoji ?? "", { await reactMessageTelegram(chatId ?? "", messageId ?? 0, emoji ?? "", {
@ -130,7 +129,7 @@ export async function handleTelegramAction(
!hasInlineButtonsCapability({ cfg, accountId: accountId ?? undefined }) !hasInlineButtonsCapability({ cfg, accountId: accountId ?? undefined })
) { ) {
throw new Error( throw new Error(
'Telegram inline buttons requested but not enabled. Add "inlineButtons" to telegram.capabilities (or telegram.accounts.<id>.capabilities).', 'Telegram inline buttons requested but not enabled. Add "inlineButtons" to channels.telegram.capabilities (or channels.telegram.accounts.<id>.capabilities).',
); );
} }
// Optional threading parameters for forum topics and reply chains // Optional threading parameters for forum topics and reply chains
@ -143,7 +142,7 @@ export async function handleTelegramAction(
const token = resolveTelegramToken(cfg, { accountId }).token; const token = resolveTelegramToken(cfg, { accountId }).token;
if (!token) { if (!token) {
throw new Error( throw new Error(
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or telegram.botToken.", "Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or channels.telegram.botToken.",
); );
} }
const result = await sendMessageTelegram(to, content, { const result = await sendMessageTelegram(to, content, {

View File

@ -10,7 +10,7 @@ vi.mock("../../web/outbound.js", () => ({
})); }));
const enabledConfig = { const enabledConfig = {
whatsapp: { actions: { reactions: true } }, channels: { whatsapp: { actions: { reactions: true } } },
} as ClawdbotConfig; } as ClawdbotConfig;
describe("handleWhatsAppAction", () => { describe("handleWhatsAppAction", () => {
@ -112,7 +112,7 @@ describe("handleWhatsAppAction", () => {
it("respects reaction gating", async () => { it("respects reaction gating", async () => {
const cfg = { const cfg = {
whatsapp: { actions: { reactions: false } }, channels: { whatsapp: { actions: { reactions: false } } },
} as ClawdbotConfig; } as ClawdbotConfig;
await expect( await expect(
handleWhatsAppAction( handleWhatsAppAction(

View File

@ -14,7 +14,7 @@ export async function handleWhatsAppAction(
cfg: ClawdbotConfig, cfg: ClawdbotConfig,
): Promise<AgentToolResult<unknown>> { ): Promise<AgentToolResult<unknown>> {
const action = readStringParam(params, "action", { required: true }); const action = readStringParam(params, "action", { required: true });
const isActionEnabled = createActionGate(cfg.whatsapp?.actions); const isActionEnabled = createActionGate(cfg.channels?.whatsapp?.actions);
if (action === "react") { if (action === "react") {
if (!isActionEnabled("reactions")) { if (!isActionEnabled("reactions")) {

View File

@ -126,18 +126,20 @@ describe("resolveTextChunkLimit", () => {
}); });
it("supports provider overrides", () => { it("supports provider overrides", () => {
const cfg = { telegram: { textChunkLimit: 1234 } }; const cfg = { channels: { telegram: { textChunkLimit: 1234 } } };
expect(resolveTextChunkLimit(cfg, "whatsapp")).toBe(4000); expect(resolveTextChunkLimit(cfg, "whatsapp")).toBe(4000);
expect(resolveTextChunkLimit(cfg, "telegram")).toBe(1234); expect(resolveTextChunkLimit(cfg, "telegram")).toBe(1234);
}); });
it("prefers account overrides when provided", () => { it("prefers account overrides when provided", () => {
const cfg = { const cfg = {
telegram: { channels: {
textChunkLimit: 2000, telegram: {
accounts: { textChunkLimit: 2000,
default: { textChunkLimit: 1234 }, accounts: {
primary: { textChunkLimit: 777 }, default: { textChunkLimit: 1234 },
primary: { textChunkLimit: 777 },
},
}, },
}, },
}; };
@ -147,8 +149,10 @@ describe("resolveTextChunkLimit", () => {
it("uses the matching provider override", () => { it("uses the matching provider override", () => {
const cfg = { const cfg = {
discord: { textChunkLimit: 111 }, channels: {
slack: { textChunkLimit: 222 }, discord: { textChunkLimit: 111 },
slack: { textChunkLimit: 222 },
},
}; };
expect(resolveTextChunkLimit(cfg, "discord")).toBe(111); expect(resolveTextChunkLimit(cfg, "discord")).toBe(111);
expect(resolveTextChunkLimit(cfg, "slack")).toBe(222); expect(resolveTextChunkLimit(cfg, "slack")).toBe(222);

View File

@ -2,17 +2,17 @@
// unintentionally breaking on newlines. Using [\s\S] keeps newlines inside // unintentionally breaking on newlines. Using [\s\S] keeps newlines inside
// the chunk so messages are only split when they truly exceed the limit. // the chunk so messages are only split when they truly exceed the limit.
import type { ChannelId } from "../channels/plugins/types.js";
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import { import {
findFenceSpanAt, findFenceSpanAt,
isSafeFenceBreak, isSafeFenceBreak,
parseFenceSpans, parseFenceSpans,
} from "../markdown/fences.js"; } from "../markdown/fences.js";
import type { ProviderId } from "../providers/plugins/types.js";
import { normalizeAccountId } from "../routing/session-key.js"; import { normalizeAccountId } from "../routing/session-key.js";
import { INTERNAL_MESSAGE_PROVIDER } from "../utils/message-provider.js"; import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel.js";
export type TextChunkProvider = ProviderId | typeof INTERNAL_MESSAGE_PROVIDER; export type TextChunkProvider = ChannelId | typeof INTERNAL_MESSAGE_CHANNEL;
const DEFAULT_CHUNK_LIMIT = 4000; const DEFAULT_CHUNK_LIMIT = 4000;
@ -55,10 +55,12 @@ export function resolveTextChunkLimit(
? opts.fallbackLimit ? opts.fallbackLimit
: DEFAULT_CHUNK_LIMIT; : DEFAULT_CHUNK_LIMIT;
const providerOverride = (() => { const providerOverride = (() => {
if (!provider || provider === INTERNAL_MESSAGE_PROVIDER) return undefined; if (!provider || provider === INTERNAL_MESSAGE_CHANNEL) return undefined;
const providerConfig = (cfg as Record<string, unknown> | undefined)?.[ const channelsConfig = cfg?.channels as Record<string, unknown> | undefined;
provider const providerConfig = (channelsConfig?.[provider] ??
] as ProviderChunkConfig | undefined; (cfg as Record<string, unknown> | undefined)?.[provider]) as
| ProviderChunkConfig
| undefined;
return resolveChunkLimitForProvider(providerConfig, accountId); return resolveChunkLimitForProvider(providerConfig, accountId);
})(); })();
if (typeof providerOverride === "number" && providerOverride > 0) { if (typeof providerOverride === "number" && providerOverride > 0) {

View File

@ -7,7 +7,7 @@ import type { MsgContext } from "./templating.js";
describe("resolveCommandAuthorization", () => { describe("resolveCommandAuthorization", () => {
it("falls back from empty SenderId to SenderE164", () => { it("falls back from empty SenderId to SenderE164", () => {
const cfg = { const cfg = {
whatsapp: { allowFrom: ["+123"] }, channels: { whatsapp: { allowFrom: ["+123"] } },
} as ClawdbotConfig; } as ClawdbotConfig;
const ctx = { const ctx = {

View File

@ -1,12 +1,12 @@
import type { ChannelDock } from "../channels/dock.js";
import { getChannelDock, listChannelDocks } from "../channels/dock.js";
import type { ChannelId } from "../channels/plugins/types.js";
import { normalizeChannelId } from "../channels/registry.js";
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import type { ProviderDock } from "../providers/dock.js";
import { getProviderDock, listProviderDocks } from "../providers/dock.js";
import type { ProviderId } from "../providers/plugins/types.js";
import { normalizeProviderId } from "../providers/registry.js";
import type { MsgContext } from "./templating.js"; import type { MsgContext } from "./templating.js";
export type CommandAuthorization = { export type CommandAuthorization = {
providerId?: ProviderId; providerId?: ChannelId;
ownerList: string[]; ownerList: string[];
senderId?: string; senderId?: string;
isAuthorizedSender: boolean; isAuthorizedSender: boolean;
@ -17,20 +17,20 @@ export type CommandAuthorization = {
function resolveProviderFromContext( function resolveProviderFromContext(
ctx: MsgContext, ctx: MsgContext,
cfg: ClawdbotConfig, cfg: ClawdbotConfig,
): ProviderId | undefined { ): ChannelId | undefined {
const direct = const direct =
normalizeProviderId(ctx.Provider) ?? normalizeChannelId(ctx.Provider) ??
normalizeProviderId(ctx.Surface) ?? normalizeChannelId(ctx.Surface) ??
normalizeProviderId(ctx.OriginatingChannel); normalizeChannelId(ctx.OriginatingChannel);
if (direct) return direct; if (direct) return direct;
const candidates = [ctx.From, ctx.To] const candidates = [ctx.From, ctx.To]
.filter((value): value is string => Boolean(value?.trim())) .filter((value): value is string => Boolean(value?.trim()))
.flatMap((value) => value.split(":").map((part) => part.trim())); .flatMap((value) => value.split(":").map((part) => part.trim()));
for (const candidate of candidates) { for (const candidate of candidates) {
const normalized = normalizeProviderId(candidate); const normalized = normalizeChannelId(candidate);
if (normalized) return normalized; if (normalized) return normalized;
} }
const configured = listProviderDocks() const configured = listChannelDocks()
.map((dock) => { .map((dock) => {
if (!dock.config?.resolveAllowFrom) return null; if (!dock.config?.resolveAllowFrom) return null;
const allowFrom = dock.config.resolveAllowFrom({ const allowFrom = dock.config.resolveAllowFrom({
@ -40,13 +40,13 @@ function resolveProviderFromContext(
if (!Array.isArray(allowFrom) || allowFrom.length === 0) return null; if (!Array.isArray(allowFrom) || allowFrom.length === 0) return null;
return dock.id; return dock.id;
}) })
.filter((value): value is ProviderId => Boolean(value)); .filter((value): value is ChannelId => Boolean(value));
if (configured.length === 1) return configured[0]; if (configured.length === 1) return configured[0];
return undefined; return undefined;
} }
function formatAllowFromList(params: { function formatAllowFromList(params: {
dock?: ProviderDock; dock?: ChannelDock;
cfg: ClawdbotConfig; cfg: ClawdbotConfig;
accountId?: string | null; accountId?: string | null;
allowFrom: Array<string | number>; allowFrom: Array<string | number>;
@ -66,7 +66,7 @@ export function resolveCommandAuthorization(params: {
}): CommandAuthorization { }): CommandAuthorization {
const { ctx, cfg, commandAuthorized } = params; const { ctx, cfg, commandAuthorized } = params;
const providerId = resolveProviderFromContext(ctx, cfg); const providerId = resolveProviderFromContext(ctx, cfg);
const dock = providerId ? getProviderDock(providerId) : undefined; const dock = providerId ? getChannelDock(providerId) : undefined;
const from = (ctx.From ?? "").trim(); const from = (ctx.From ?? "").trim();
const to = (ctx.To ?? "").trim(); const to = (ctx.To ?? "").trim();
const allowFromRaw = dock?.config?.resolveAllowFrom const allowFromRaw = dock?.config?.resolveAllowFrom

Some files were not shown because too many files have changed in this diff Show More