refactor!: rename chat providers to channels
This commit is contained in:
parent
0cd632ba84
commit
90342a4f3a
@ -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).
|
||||||
|
|||||||
42
README.md
42
README.md
@ -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 you’re 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)
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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 didn’t mention it, or
|
- Your config requires mentions and you didn’t 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 can’t 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 can’t verify permissions.
|
||||||
- **DMs don’t work**: `discord.dm.enabled=false`, `discord.dm.policy="disabled"`, or you haven’t been approved yet (`discord.dm.policy="pairing"`).
|
- **DMs don’t work**: `channels.discord.dm.enabled=false`, `channels.discord.dm.policy="disabled"`, or you haven’t 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 Clawdbot’s chat commands.
|
- The registered commands mirror Clawdbot’s 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:
|
||||||
@ -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 grammY’s `client.baseFetch`.
|
- **Proxy:** optional `channels.telegram.proxy` uses `undici.ProxyAgent` through grammY’s `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
|
||||||
@ -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. Don’t 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. Don’t 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
30
docs/channels/index.md
Normal 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).
|
||||||
@ -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.
|
||||||
@ -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 mention‑gated by default).
|
- Set `groupPolicy: "open"` to allow any member (still mention‑gated 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
123
docs/channels/signal.md
Normal 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`.
|
||||||
@ -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.
|
||||||
@ -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 sender’s 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 sender’s 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 doesn’t respond to non-mention messages in a group:**
|
**Bot doesn’t respond to non-mention messages in a group:**
|
||||||
- If you set `telegram.groups.*.requireMention=false`, Telegram’s Bot API **privacy mode** must be disabled.
|
- If you set `channels.telegram.groups.*.requireMention=false`, Telegram’s 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 can’t audit wildcard `"*"` rules).
|
- `clawdbot channels status --probe` can additionally check membership for explicit numeric group IDs (it can’t 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`.
|
||||||
21
docs/channels/troubleshooting.md
Normal file
21
docs/channels/troubleshooting.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
summary: "Channel-specific troubleshooting shortcuts (Discord/Telegram/WhatsApp)"
|
||||||
|
read_when:
|
||||||
|
- A channel connects but messages don’t 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)
|
||||||
@ -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 agent’s 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 agent’s 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. It’s 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. It’s 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.
|
||||||
@ -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`
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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`.
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -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**: per‑provider account instance (when supported).
|
- **AccountId**: per‑channel 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 agent’s **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 agent’s main
|
WebChat attaches to the **selected agent** and defaults to the agent’s main
|
||||||
session. Because of this, WebChat lets you see cross‑provider context for that
|
session. Because of this, WebChat lets you see cross‑channel 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.
|
||||||
@ -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).
|
||||||
|
|
||||||
## What’s implemented (2025-12-03)
|
## What’s implemented (2025-12-03)
|
||||||
- Activation modes: `mention` (default) or `always`. `mention` requires a ping (real WhatsApp @-mentions via `mentionedJids`, regex patterns, or the bot’s 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 bot’s 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 bot’s 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 bot’s 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.
|
||||||
|
|||||||
@ -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 bot’s 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 bot’s 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:
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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).
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -14,7 +14,7 @@ read_when:
|
|||||||
- `/status` in chats: emoji‑rich status card with session tokens + estimated cost (API key only) and provider quota windows when available.
|
- `/status` in chats: emoji‑rich status card with session tokens + estimated cost (API key only) and provider quota windows when available.
|
||||||
- `/cost on|off` in chats: toggles per‑response usage lines (OAuth shows tokens only).
|
- `/cost on|off` in chats: toggles per‑response 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
|
||||||
|
|||||||
140
docs/docs.json
140
docs/docs.json
@ -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"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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` (800–2500ms), `custom` (use `minMs`/`maxMs`).
|
Modes: `off` (default), `natural` (800–2500ms), `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
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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`
|
||||||
|
|||||||
@ -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 409–515 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 409–515 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 409–515 → relink with `clawdbot providers logout` then `clawdbot providers login`.
|
- `logged out` or status 409–515 → 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.
|
||||||
|
|||||||
@ -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`.
|
||||||
|
|||||||
@ -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**.
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -9,7 +9,7 @@ When Clawdbot misbehaves, here's how to fix it.
|
|||||||
|
|
||||||
Start with the FAQ’s [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 FAQ’s [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” doesn’t mean “working” |
|
| `clawdbot status --deep` | Runs gateway health checks (incl. provider probes; requires reachable gateway) | When “configured” doesn’t mean “working” |
|
||||||
| `clawdbot gateway status` | Gateway discovery + reachability (local + remote targets) | When you suspect you’re probing the wrong gateway |
|
| `clawdbot gateway status` | Gateway discovery + reachability (local + remote targets) | When you suspect you’re 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 you’re logged out / unlinked:
|
If you’re 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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -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 space‑lobster assistant.
|
Clawdbot also powers [Clawd](https://clawd.me), the space‑lobster 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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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 what’s available.
|
- Nodes advertise their permission state via `node.list` / `node.describe` so agents know what’s 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
|
||||||
|
|||||||
@ -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).
|
||||||
|
|||||||
@ -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`.
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
---
|
|
||||||
summary: "Provider-specific troubleshooting shortcuts (Discord/Telegram/WhatsApp)"
|
|
||||||
read_when:
|
|
||||||
- A provider connects but messages don’t 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)
|
|
||||||
|
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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).
|
||||||
|
|||||||
@ -16,7 +16,7 @@ You’re 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: {
|
||||||
|
|||||||
@ -271,11 +271,11 @@ without WhatsApp/Telegram.
|
|||||||
|
|
||||||
### Telegram: what goes in `allowFrom`?
|
### Telegram: what goes in `allowFrom`?
|
||||||
|
|
||||||
`telegram.allowFrom` is **the human sender’s 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 sender’s 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 **multi‑agent routing**. Bind each sender’s 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 **multi‑agent routing**. Bind each sender’s 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 hot‑reload:
|
|||||||
|
|
||||||
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”).
|
||||||
- **Sub‑agents:** spawn background work from a main agent when you want parallelism.
|
- **Sub‑agents:** 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 isn’t allowlisted.
|
- You configured `channels.whatsapp.groups` without `"*"` and the group isn’t 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 DM‑capable providers is **pairing**:
|
- Default behavior on DM‑capable 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 didn’t arrive.
|
- Pending requests are capped at **3 per provider**; check `clawdbot pairing list <provider>` if a code didn’t arrive.
|
||||||
@ -1300,7 +1302,7 @@ List pending requests:
|
|||||||
clawdbot pairing list whatsapp
|
clawdbot pairing list whatsapp
|
||||||
```
|
```
|
||||||
|
|
||||||
Wizard phone number prompt: it’s used to set your **allowlist/owner** so your own DMs are permitted. It’s not used for auto-sending. If you run on your personal WhatsApp number, use that number and enable `whatsapp.selfChatMode`.
|
Wizard phone number prompt: it’s used to set your **allowlist/owner** so your own DMs are permitted. It’s 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 won’t stop”
|
## Chat commands, aborting tasks, and “it won’t 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 re‑auth?
|
### WhatsApp logged me out. How do I re‑auth?
|
||||||
|
|
||||||
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` — what’s the standard fix path?
|
### Build errors on `main` — what’s the standard fix path?
|
||||||
|
|||||||
@ -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 won’t respond.
|
**Telegram DM tip:** your first DM returns a pairing code. Approve it (see next step) or the bot won’t respond.
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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** (you’ll be prompted for your phone number)
|
- Telegram + WhatsApp DMs default to **allowlist** (you’ll 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 re‑implementing 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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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`).
|
||||||
|
|||||||
17
src/agents/channel-tools.ts
Normal file
17
src/agents/channel-tools.ts
Normal 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;
|
||||||
|
}
|
||||||
@ -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",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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");
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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";
|
||||||
|
|||||||
@ -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(),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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(",")
|
||||||
|
|||||||
@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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";
|
||||||
|
|
||||||
|
|||||||
@ -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 = {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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",
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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];
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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");
|
||||||
|
|||||||
@ -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", {
|
||||||
|
|||||||
@ -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),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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(
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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)) {
|
||||||
|
|||||||
@ -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(
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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, {
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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")) {
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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 = {
|
||||||
|
|||||||
@ -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
Loading…
Reference in New Issue
Block a user