mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 09:28:37 +00:00
feat: unify message cli and tools
This commit is contained in:
@@ -93,17 +93,17 @@
|
|||||||
- Voice wake forwarding tips:
|
- Voice wake forwarding tips:
|
||||||
- Command template should stay `clawdbot-mac agent --message "${text}" --thinking low`; `VoiceWakeForwarder` already shell-escapes `${text}`. Don’t add extra quotes.
|
- Command template should stay `clawdbot-mac agent --message "${text}" --thinking low`; `VoiceWakeForwarder` already shell-escapes `${text}`. Don’t add extra quotes.
|
||||||
- launchd PATH is minimal; ensure the app’s launch agent PATH includes standard system paths plus your pnpm bin (typically `$HOME/Library/pnpm`) so `pnpm`/`clawdbot` binaries resolve when invoked via `clawdbot-mac`.
|
- launchd PATH is minimal; ensure the app’s launch agent PATH includes standard system paths plus your pnpm bin (typically `$HOME/Library/pnpm`) so `pnpm`/`clawdbot` binaries resolve when invoked via `clawdbot-mac`.
|
||||||
- For manual `clawdbot message send` messages that include `!`, use the heredoc pattern noted below to avoid the Bash tool’s escaping.
|
- For manual `clawdbot message --action send` messages that include `!`, use the heredoc pattern noted below to avoid the Bash tool’s escaping.
|
||||||
|
|
||||||
## Exclamation Mark Escaping Workaround
|
## Exclamation Mark Escaping Workaround
|
||||||
The Claude Code Bash tool escapes `!` to `\\!` in command arguments. When using `clawdbot message send` with messages containing exclamation marks, use heredoc syntax:
|
The Claude Code Bash tool escapes `!` to `\\!` in command arguments. When using `clawdbot message --action send` with messages containing exclamation marks, use heredoc syntax:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# WRONG - will send "Hello\\!" with backslash
|
# WRONG - will send "Hello\\!" with backslash
|
||||||
clawdbot message send --to "+1234" --message 'Hello!'
|
clawdbot message --action send --to "+1234" --message 'Hello!'
|
||||||
|
|
||||||
# CORRECT - use heredoc to avoid escaping
|
# CORRECT - use heredoc to avoid escaping
|
||||||
clawdbot message send --to "+1234" --message "$(cat <<'EOF'
|
clawdbot message --action send --to "+1234" --message "$(cat <<'EOF'
|
||||||
Hello!
|
Hello!
|
||||||
EOF
|
EOF
|
||||||
)"
|
)"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
- Commands: accept /models as an alias for /model.
|
- Commands: accept /models as an alias for /model.
|
||||||
- Debugging: add raw model stream logging flags and document gateway watch mode.
|
- Debugging: add raw model stream logging flags and document gateway watch mode.
|
||||||
- Agent: add claude-cli/opus-4.5 runner via Claude CLI with resume support (tools disabled).
|
- Agent: add claude-cli/opus-4.5 runner via Claude CLI with resume support (tools disabled).
|
||||||
- CLI: replace `send`/`poll` with `message send`/`message poll`, and add the `message` agent tool.
|
- CLI: replace `message send`/`message poll` with `message --action ...`, and fold Discord/Slack/Telegram/WhatsApp tools into `message` (provider required unless only one configured).
|
||||||
- CLI: improve `logs` output (pretty/plain/JSONL), add gateway unreachable hint, and document logging.
|
- CLI: improve `logs` output (pretty/plain/JSONL), add gateway unreachable hint, and document logging.
|
||||||
- WhatsApp: route queued replies to the original sender instead of the bot's own number. (#534) — thanks @mcinteerj
|
- WhatsApp: route queued replies to the original sender instead of the bot's own number. (#534) — thanks @mcinteerj
|
||||||
- Models: add OAuth expiry checks in doctor, expanded `models status` auth output (missing auth + `--check` exit codes). (#538) — thanks @latitudeki5223
|
- Models: add OAuth expiry checks in doctor, expanded `models status` auth output (missing auth + `--check` exit codes). (#538) — thanks @latitudeki5223
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ clawdbot onboard --install-daemon
|
|||||||
clawdbot gateway --port 18789 --verbose
|
clawdbot gateway --port 18789 --verbose
|
||||||
|
|
||||||
# Send a message
|
# Send a message
|
||||||
clawdbot message send --to +1234567890 --message "Hello from Clawdbot"
|
clawdbot message --to +1234567890 --message "Hello from Clawdbot"
|
||||||
|
|
||||||
# Talk to the assistant (optionally deliver back to WhatsApp/Telegram/Slack/Discord)
|
# Talk to the assistant (optionally deliver back to WhatsApp/Telegram/Slack/Discord)
|
||||||
clawdbot agent --message "Ship checklist" --thinking high
|
clawdbot agent --message "Ship checklist" --thinking high
|
||||||
|
|||||||
@@ -15,18 +15,22 @@ read_when:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# WhatsApp
|
# WhatsApp
|
||||||
clawdbot message poll --to +15555550123 -q "Lunch today?" -o "Yes" -o "No" -o "Maybe"
|
clawdbot message --action poll --to +15555550123 \
|
||||||
clawdbot message poll --to 123456789@g.us -q "Meeting time?" -o "10am" -o "2pm" -o "4pm" -s 2
|
--poll-question "Lunch today?" --poll-option "Yes" --poll-option "No" --poll-option "Maybe"
|
||||||
|
clawdbot message --action poll --to 123456789@g.us \
|
||||||
|
--poll-question "Meeting time?" --poll-option "10am" --poll-option "2pm" --poll-option "4pm" --poll-multi
|
||||||
|
|
||||||
# Discord
|
# Discord
|
||||||
clawdbot message poll --to channel:123456789 -q "Snack?" -o "Pizza" -o "Sushi" --provider discord
|
clawdbot message --action poll --provider discord --to channel:123456789 \
|
||||||
clawdbot message poll --to channel:123456789 -q "Plan?" -o "A" -o "B" --provider discord --duration-hours 48
|
--poll-question "Snack?" --poll-option "Pizza" --poll-option "Sushi"
|
||||||
|
clawdbot message --action poll --provider discord --to channel:123456789 \
|
||||||
|
--poll-question "Plan?" --poll-option "A" --poll-option "B" --poll-duration-hours 48
|
||||||
```
|
```
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
- `--provider`: `whatsapp` (default) or `discord`
|
- `--provider`: `whatsapp` (default) or `discord`
|
||||||
- `--max-selections`: how many choices a voter can select (default: 1)
|
- `--poll-multi`: allow selecting multiple options
|
||||||
- `--duration-hours`: Discord-only (defaults to 24 when omitted)
|
- `--poll-duration-hours`: Discord-only (defaults to 24 when omitted)
|
||||||
|
|
||||||
## Gateway RPC
|
## Gateway RPC
|
||||||
|
|
||||||
@@ -45,10 +49,7 @@ Params:
|
|||||||
- WhatsApp: 2-12 options, `maxSelections` must be within option count, ignores `durationHours`.
|
- WhatsApp: 2-12 options, `maxSelections` must be within option count, ignores `durationHours`.
|
||||||
- Discord: 2-10 options, `durationHours` clamped to 1-768 hours (default 24). `maxSelections > 1` enables multi-select; Discord does not support a strict selection count.
|
- Discord: 2-10 options, `durationHours` clamped to 1-768 hours (default 24). `maxSelections > 1` enables multi-select; Discord does not support a strict selection count.
|
||||||
|
|
||||||
## Agent tool (Discord)
|
|
||||||
The Discord tool action `poll` still uses `question`, `answers`, optional `allowMultiselect`, `durationHours`, and `content`. The gateway/CLI poll model maps `allowMultiselect` to `maxSelections > 1`.
|
|
||||||
|
|
||||||
Note: Discord has no “pick exactly N” mode; `maxSelections` is treated as a boolean (`> 1` = multiselect).
|
|
||||||
|
|
||||||
## Agent tool (Message)
|
## Agent tool (Message)
|
||||||
Use the `message` tool with `poll` action (`to`, `question`, `options`, optional `maxSelections`, `durationHours`, `provider`).
|
Use the `message` tool with `poll` action (`to`, `pollQuestion`, `pollOption`, optional `pollMulti`, `pollDurationHours`, `provider`).
|
||||||
|
|
||||||
|
Note: Discord has no “pick exactly N” mode; `pollMulti` maps to multi-select.
|
||||||
|
|||||||
@@ -56,8 +56,6 @@ clawdbot [--dev] [--profile <name>] <command>
|
|||||||
info
|
info
|
||||||
check
|
check
|
||||||
message
|
message
|
||||||
send
|
|
||||||
poll
|
|
||||||
agent
|
agent
|
||||||
agents
|
agents
|
||||||
list
|
list
|
||||||
@@ -285,37 +283,10 @@ Options:
|
|||||||
|
|
||||||
## Messaging + agent
|
## Messaging + agent
|
||||||
|
|
||||||
### `message send`
|
### `message`
|
||||||
Send a message through a provider.
|
Unified outbound messaging + provider actions.
|
||||||
|
|
||||||
Required:
|
See: [/cli/message](/cli/message)
|
||||||
- `--to <dest>`
|
|
||||||
- `--message <text>`
|
|
||||||
|
|
||||||
Options:
|
|
||||||
- `--media <path-or-url>`
|
|
||||||
- `--gif-playback`
|
|
||||||
- `--provider <whatsapp|telegram|discord|slack|signal|imessage>`
|
|
||||||
- `--account <id>` (WhatsApp)
|
|
||||||
- `--dry-run`
|
|
||||||
- `--json`
|
|
||||||
- `--verbose`
|
|
||||||
|
|
||||||
### `message poll`
|
|
||||||
Create a poll (WhatsApp or Discord).
|
|
||||||
|
|
||||||
Required:
|
|
||||||
- `--to <id>`
|
|
||||||
- `--question <text>`
|
|
||||||
- `--option <choice>` (repeat 2-12 times)
|
|
||||||
|
|
||||||
Options:
|
|
||||||
- `--max-selections <n>`
|
|
||||||
- `--duration-hours <n>` (Discord)
|
|
||||||
- `--provider <whatsapp|discord>`
|
|
||||||
- `--dry-run`
|
|
||||||
- `--json`
|
|
||||||
- `--verbose`
|
|
||||||
|
|
||||||
### `agent`
|
### `agent`
|
||||||
Run one agent turn via the Gateway (or `--local` embedded).
|
Run one agent turn via the Gateway (or `--local` embedded).
|
||||||
|
|||||||
210
docs/cli/message.md
Normal file
210
docs/cli/message.md
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
---
|
||||||
|
summary: "CLI reference for `clawdbot message` (send + provider actions)"
|
||||||
|
read_when:
|
||||||
|
- Adding or modifying message CLI actions
|
||||||
|
- Changing outbound provider behavior
|
||||||
|
---
|
||||||
|
|
||||||
|
# `clawdbot message`
|
||||||
|
|
||||||
|
Single outbound command for sending messages and provider actions
|
||||||
|
(Discord/Slack/Telegram/WhatsApp/Signal/iMessage).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
clawdbot message --action <action> [--provider <name>] [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
Defaults:
|
||||||
|
- `--action send`
|
||||||
|
|
||||||
|
Provider selection:
|
||||||
|
- `--provider` required if more than one provider is configured.
|
||||||
|
- If exactly one provider is configured, it becomes the default.
|
||||||
|
- Values: `whatsapp|telegram|discord|slack|signal|imessage`
|
||||||
|
|
||||||
|
Target formats (`--to`):
|
||||||
|
- WhatsApp: E.164 or group JID
|
||||||
|
- Telegram: chat id or `@username`
|
||||||
|
- Discord/Slack: `channel:<id>` or `user:<id>` (raw id ok)
|
||||||
|
- Signal: E.164, `group:<id>`, or `signal:+E.164`
|
||||||
|
- iMessage: handle or `chat_id:<id>`
|
||||||
|
|
||||||
|
## Common flags
|
||||||
|
|
||||||
|
- `--to <dest>`
|
||||||
|
- `--message <text>`
|
||||||
|
- `--media <url>`
|
||||||
|
- `--message-id <id>`
|
||||||
|
- `--reply-to <id>`
|
||||||
|
- `--thread-id <id>` (Telegram forum thread)
|
||||||
|
- `--account <id>` (multi-account providers)
|
||||||
|
- `--dry-run`
|
||||||
|
- `--json`
|
||||||
|
- `--verbose`
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
|
||||||
|
### `send`
|
||||||
|
Providers: whatsapp, telegram, discord, slack, signal, imessage
|
||||||
|
Required: `--to`, `--message`
|
||||||
|
Optional: `--media`, `--reply-to`, `--thread-id`, `--account`, `--gif-playback`
|
||||||
|
|
||||||
|
### `react`
|
||||||
|
Providers: discord, slack, telegram, whatsapp
|
||||||
|
Required: `--to`, `--message-id`
|
||||||
|
Optional: `--emoji`, `--remove`, `--participant`, `--from-me`, `--account`
|
||||||
|
|
||||||
|
### `reactions`
|
||||||
|
Providers: discord, slack
|
||||||
|
Required: `--to`, `--message-id`
|
||||||
|
Optional: `--limit`
|
||||||
|
|
||||||
|
### `read`
|
||||||
|
Providers: discord, slack
|
||||||
|
Required: `--to`
|
||||||
|
Optional: `--limit`, `--before`, `--after`, `--around`
|
||||||
|
|
||||||
|
### `edit`
|
||||||
|
Providers: discord, slack
|
||||||
|
Required: `--to`, `--message-id`, `--message`
|
||||||
|
|
||||||
|
### `delete`
|
||||||
|
Providers: discord, slack
|
||||||
|
Required: `--to`, `--message-id`
|
||||||
|
|
||||||
|
### `pin`
|
||||||
|
Providers: discord, slack
|
||||||
|
Required: `--to`, `--message-id`
|
||||||
|
|
||||||
|
### `unpin`
|
||||||
|
Providers: discord, slack
|
||||||
|
Required: `--to`, `--message-id`
|
||||||
|
|
||||||
|
### `list-pins`
|
||||||
|
Providers: discord, slack
|
||||||
|
Required: `--to`
|
||||||
|
|
||||||
|
### `poll`
|
||||||
|
Providers: whatsapp, discord
|
||||||
|
Required: `--to`, `--poll-question`, `--poll-option` (repeat)
|
||||||
|
Optional: `--poll-multi`, `--poll-duration-hours`, `--message`
|
||||||
|
|
||||||
|
### `sticker`
|
||||||
|
Providers: discord
|
||||||
|
Required: `--to`, `--sticker-id` (repeat)
|
||||||
|
Optional: `--message`
|
||||||
|
|
||||||
|
### `permissions`
|
||||||
|
Providers: discord
|
||||||
|
Required: `--to` (channel id)
|
||||||
|
|
||||||
|
### `thread-create`
|
||||||
|
Providers: discord
|
||||||
|
Required: `--to` (channel id), `--thread-name`
|
||||||
|
Optional: `--message-id`, `--auto-archive-min`
|
||||||
|
|
||||||
|
### `thread-list`
|
||||||
|
Providers: discord
|
||||||
|
Required: `--guild-id`
|
||||||
|
Optional: `--channel-id`, `--include-archived`, `--before`, `--limit`
|
||||||
|
|
||||||
|
### `thread-reply`
|
||||||
|
Providers: discord
|
||||||
|
Required: `--to` (thread id), `--message`
|
||||||
|
Optional: `--media`, `--reply-to`
|
||||||
|
|
||||||
|
### `search`
|
||||||
|
Providers: discord
|
||||||
|
Required: `--guild-id`, `--query`
|
||||||
|
Optional: `--channel-id`, `--channel-ids`, `--author-id`, `--author-ids`, `--limit`
|
||||||
|
|
||||||
|
### `member-info`
|
||||||
|
Providers: discord, slack
|
||||||
|
Required: `--user-id`
|
||||||
|
Discord only: also `--guild-id`
|
||||||
|
|
||||||
|
### `role-info`
|
||||||
|
Providers: discord
|
||||||
|
Required: `--guild-id`
|
||||||
|
|
||||||
|
### `emoji-list`
|
||||||
|
Providers: discord, slack
|
||||||
|
Discord only: `--guild-id`
|
||||||
|
|
||||||
|
### `emoji-upload`
|
||||||
|
Providers: discord
|
||||||
|
Required: `--guild-id`, `--emoji-name`, `--media`
|
||||||
|
Optional: `--role-ids` (repeat)
|
||||||
|
|
||||||
|
### `sticker-upload`
|
||||||
|
Providers: discord
|
||||||
|
Required: `--guild-id`, `--sticker-name`, `--sticker-desc`, `--sticker-tags`, `--media`
|
||||||
|
|
||||||
|
### `role-add`
|
||||||
|
Providers: discord
|
||||||
|
Required: `--guild-id`, `--user-id`, `--role-id`
|
||||||
|
|
||||||
|
### `role-remove`
|
||||||
|
Providers: discord
|
||||||
|
Required: `--guild-id`, `--user-id`, `--role-id`
|
||||||
|
|
||||||
|
### `channel-info`
|
||||||
|
Providers: discord
|
||||||
|
Required: `--channel-id`
|
||||||
|
|
||||||
|
### `channel-list`
|
||||||
|
Providers: discord
|
||||||
|
Required: `--guild-id`
|
||||||
|
|
||||||
|
### `voice-status`
|
||||||
|
Providers: discord
|
||||||
|
Required: `--guild-id`, `--user-id`
|
||||||
|
|
||||||
|
### `event-list`
|
||||||
|
Providers: discord
|
||||||
|
Required: `--guild-id`
|
||||||
|
|
||||||
|
### `event-create`
|
||||||
|
Providers: discord
|
||||||
|
Required: `--guild-id`, `--event-name`, `--start-time`
|
||||||
|
Optional: `--end-time`, `--desc`, `--channel-id`, `--location`, `--event-type`
|
||||||
|
|
||||||
|
### `timeout`
|
||||||
|
Providers: discord
|
||||||
|
Required: `--guild-id`, `--user-id`
|
||||||
|
Optional: `--duration-min`, `--until`, `--reason`
|
||||||
|
|
||||||
|
### `kick`
|
||||||
|
Providers: discord
|
||||||
|
Required: `--guild-id`, `--user-id`
|
||||||
|
Optional: `--reason`
|
||||||
|
|
||||||
|
### `ban`
|
||||||
|
Providers: discord
|
||||||
|
Required: `--guild-id`, `--user-id`
|
||||||
|
Optional: `--reason`, `--delete-days`
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Send a Discord reply:
|
||||||
|
```
|
||||||
|
clawdbot message --action send --provider discord \
|
||||||
|
--to channel:123 --message "hi" --reply-to 456
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a Discord poll:
|
||||||
|
```
|
||||||
|
clawdbot message --action poll --provider discord \
|
||||||
|
--to channel:123 \
|
||||||
|
--poll-question "Snack?" \
|
||||||
|
--poll-option Pizza --poll-option Sushi \
|
||||||
|
--poll-multi --poll-duration-hours 48
|
||||||
|
```
|
||||||
|
|
||||||
|
React in Slack:
|
||||||
|
```
|
||||||
|
clawdbot message --action react --provider slack \
|
||||||
|
--to C123 --message-id 456 --emoji "✅"
|
||||||
|
```
|
||||||
@@ -254,7 +254,7 @@ Windows installs should use **WSL2** and follow the Linux systemd section above.
|
|||||||
|
|
||||||
## CLI helpers
|
## CLI helpers
|
||||||
- `clawdbot gateway health|status` — request health/status over the Gateway WS.
|
- `clawdbot gateway health|status` — request health/status over the Gateway WS.
|
||||||
- `clawdbot message send --to <num> --message "hi" [--media ...]` — send via Gateway (idempotent for WhatsApp).
|
- `clawdbot message --to <num> --message "hi" [--media ...]` — send via Gateway (idempotent for WhatsApp).
|
||||||
- `clawdbot agent --message "hi" --to <num>` — run an agent turn (waits for final by default).
|
- `clawdbot agent --message "hi" --to <num>` — run an agent turn (waits for final by default).
|
||||||
- `clawdbot gateway call <method> --params '{"k":"v"}'` — raw method invoker for debugging.
|
- `clawdbot gateway call <method> --params '{"k":"v"}'` — raw method invoker for debugging.
|
||||||
- `clawdbot daemon stop|restart` — stop/restart the supervised gateway service (launchd/systemd).
|
- `clawdbot daemon stop|restart` — stop/restart the supervised gateway service (launchd/systemd).
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ clawdbot gateway --port 19001
|
|||||||
Send a test message (requires a running Gateway):
|
Send a test message (requires a running Gateway):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot message send --to +15555550123 --message "Hello from CLAWDBOT"
|
clawdbot message --to +15555550123 --message "Hello from CLAWDBOT"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration (optional)
|
## Configuration (optional)
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ read_when:
|
|||||||
CLAWDBOT is now **web-only** (Baileys). This document captures the current media handling rules for send, gateway, and agent replies.
|
CLAWDBOT is now **web-only** (Baileys). This document captures the current media handling rules for send, gateway, and agent replies.
|
||||||
|
|
||||||
## Goals
|
## Goals
|
||||||
- Send media with optional captions via `clawdbot message send --media`.
|
- Send media with optional captions via `clawdbot message --media`.
|
||||||
- Allow auto-replies from the web inbox to include media alongside text.
|
- Allow auto-replies from the web inbox to include media alongside text.
|
||||||
- Keep per-type limits sane and predictable.
|
- Keep per-type limits sane and predictable.
|
||||||
|
|
||||||
## CLI Surface
|
## CLI Surface
|
||||||
- `clawdbot message send --media <path-or-url> [--message <caption>]`
|
- `clawdbot message --media <path-or-url> [--message <caption>]`
|
||||||
- `--media` optional; caption can be empty for media-only sends.
|
- `--media` optional; caption can be empty for media-only sends.
|
||||||
- `--dry-run` prints the resolved payload; `--json` emits `{ provider, to, messageId, mediaUrl, caption }`.
|
- `--dry-run` prints the resolved payload; `--json` emits `{ provider, to, messageId, mediaUrl, caption }`.
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ CLAWDBOT is now **web-only** (Baileys). This document captures the current media
|
|||||||
|
|
||||||
## Auto-Reply Pipeline
|
## Auto-Reply Pipeline
|
||||||
- `getReplyFromConfig` returns `{ text?, mediaUrl?, mediaUrls? }`.
|
- `getReplyFromConfig` returns `{ text?, mediaUrl?, mediaUrls? }`.
|
||||||
- When media is present, the web sender resolves local paths or URLs using the same pipeline as `clawdbot message send`.
|
- When media is present, the web sender resolves local paths or URLs using the same pipeline as `clawdbot message`.
|
||||||
- Multiple media entries are sent sequentially if provided.
|
- Multiple media entries are sent sequentially if provided.
|
||||||
|
|
||||||
## Inbound Media to Commands (Pi)
|
## Inbound Media to Commands (Pi)
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ Outbound Telegram API calls retry on transient network/429 errors with exponenti
|
|||||||
|
|
||||||
## 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.
|
||||||
- Example: `clawdbot message send --provider telegram --to 123456789 --message "hi"`.
|
- Example: `clawdbot message --provider telegram --to 123456789 --message "hi"`.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ Behavior:
|
|||||||
- Caption only on first media item.
|
- Caption only on first media item.
|
||||||
- Media fetch supports HTTP(S) and local paths.
|
- Media fetch supports HTTP(S) and local paths.
|
||||||
- Animated GIFs: WhatsApp expects MP4 with `gifPlayback: true` for inline looping.
|
- Animated GIFs: WhatsApp expects MP4 with `gifPlayback: true` for inline looping.
|
||||||
- CLI: `clawdbot message send --media <mp4> --gif-playback`
|
- CLI: `clawdbot message --media <mp4> --gif-playback`
|
||||||
- Gateway: `send` params include `gifPlayback: true`
|
- Gateway: `send` params include `gifPlayback: true`
|
||||||
|
|
||||||
## Media limits + optimization
|
## Media limits + optimization
|
||||||
|
|||||||
@@ -560,7 +560,7 @@ Outbound attachments from the agent must include a `MEDIA:<path-or-url>` line (o
|
|||||||
CLI sending:
|
CLI sending:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot message send --to +15555550123 --message "Here you go" --media /path/to/file.png
|
clawdbot message --to +15555550123 --message "Here you go" --media /path/to/file.png
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: images are resized/recompressed (max side 2048px) to hit size limits. See [Images](/nodes/images).
|
Note: images are resized/recompressed (max side 2048px) to hit size limits. See [Images](/nodes/images).
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ In a new terminal:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot health
|
clawdbot health
|
||||||
clawdbot message send --to +15555550123 --message "Hello from Clawdbot"
|
clawdbot message --to +15555550123 --message "Hello from Clawdbot"
|
||||||
```
|
```
|
||||||
|
|
||||||
If `health` shows “no auth configured”, go back to the wizard and set OAuth/key auth — the agent won’t be able to respond without it.
|
If `health` shows “no auth configured”, go back to the wizard and set OAuth/key auth — the agent won’t be able to respond without it.
|
||||||
|
|||||||
@@ -149,15 +149,28 @@ Notes:
|
|||||||
- Uses the image model directly (independent of the main chat model).
|
- Uses the image model directly (independent of the main chat model).
|
||||||
|
|
||||||
### `message`
|
### `message`
|
||||||
Send messages and polls across providers.
|
Send messages and provider actions across Discord/Slack/Telegram/WhatsApp/Signal/iMessage.
|
||||||
|
|
||||||
Core actions:
|
Core actions:
|
||||||
- `send` (text + optional media)
|
- `send` (text + optional media)
|
||||||
- `poll` (WhatsApp/Discord polls)
|
- `poll` (WhatsApp/Discord polls)
|
||||||
|
- `react` / `reactions` / `read` / `edit` / `delete`
|
||||||
|
- `pin` / `unpin` / `list-pins`
|
||||||
|
- `permissions`
|
||||||
|
- `thread-create` / `thread-list` / `thread-reply`
|
||||||
|
- `search`
|
||||||
|
- `sticker`
|
||||||
|
- `member-info` / `role-info`
|
||||||
|
- `emoji-list` / `emoji-upload` / `sticker-upload`
|
||||||
|
- `role-add` / `role-remove`
|
||||||
|
- `channel-info` / `channel-list`
|
||||||
|
- `voice-status`
|
||||||
|
- `event-list` / `event-create`
|
||||||
|
- `timeout` / `kick` / `ban`
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- `send` routes WhatsApp via the Gateway and other providers directly.
|
- `send` routes WhatsApp via the Gateway; other providers go direct.
|
||||||
- `poll` always routes via the Gateway.
|
- `poll` uses the Gateway for WhatsApp and direct Discord API for Discord.
|
||||||
|
|
||||||
### `cron`
|
### `cron`
|
||||||
Manage Gateway cron jobs and wakeups.
|
Manage Gateway cron jobs and wakeups.
|
||||||
@@ -209,70 +222,6 @@ Notes:
|
|||||||
- Result is restricted to per-agent allowlists (`routing.agents.<agentId>.subagents.allowAgents`).
|
- Result is restricted to per-agent allowlists (`routing.agents.<agentId>.subagents.allowAgents`).
|
||||||
- When `["*"]` is configured, the tool includes all configured agents and marks `allowAny: true`.
|
- When `["*"]` is configured, the tool includes all configured agents and marks `allowAny: true`.
|
||||||
|
|
||||||
### `discord`
|
|
||||||
Send Discord reactions, stickers, or polls.
|
|
||||||
|
|
||||||
Core actions:
|
|
||||||
- `react` (`channelId`, `messageId`, `emoji`)
|
|
||||||
- `reactions` (`channelId`, `messageId`, optional `limit`)
|
|
||||||
- `sticker` (`to`, `stickerIds`, optional `content`)
|
|
||||||
- `poll` (`to`, `question`, `answers`, optional `allowMultiselect`, `durationHours`, `content`)
|
|
||||||
- `permissions` (`channelId`)
|
|
||||||
- `readMessages` (`channelId`, optional `limit`/`before`/`after`/`around`)
|
|
||||||
- `sendMessage` (`to`, `content`, optional `mediaUrl`, `replyTo`)
|
|
||||||
- `editMessage` (`channelId`, `messageId`, `content`)
|
|
||||||
- `deleteMessage` (`channelId`, `messageId`)
|
|
||||||
- `threadCreate` (`channelId`, `name`, optional `messageId`, `autoArchiveMinutes`)
|
|
||||||
- `threadList` (`guildId`, optional `channelId`, `includeArchived`, `before`, `limit`)
|
|
||||||
- `threadReply` (`channelId`, `content`, optional `mediaUrl`, `replyTo`)
|
|
||||||
- `pinMessage`/`unpinMessage` (`channelId`, `messageId`)
|
|
||||||
- `listPins` (`channelId`)
|
|
||||||
- `searchMessages` (`guildId`, `content`, optional `channelId`/`channelIds`, `authorId`/`authorIds`, `limit`)
|
|
||||||
- `memberInfo` (`guildId`, `userId`)
|
|
||||||
- `roleInfo` (`guildId`)
|
|
||||||
- `emojiList` (`guildId`)
|
|
||||||
- `roleAdd`/`roleRemove` (`guildId`, `userId`, `roleId`)
|
|
||||||
- `channelInfo` (`channelId`)
|
|
||||||
- `channelList` (`guildId`)
|
|
||||||
- `voiceStatus` (`guildId`, `userId`)
|
|
||||||
- `eventList` (`guildId`)
|
|
||||||
- `eventCreate` (`guildId`, `name`, `startTime`, optional `endTime`, `description`, `channelId`, `entityType`, `location`)
|
|
||||||
- `timeout` (`guildId`, `userId`, optional `durationMinutes`, `until`, `reason`)
|
|
||||||
- `kick` (`guildId`, `userId`, optional `reason`)
|
|
||||||
- `ban` (`guildId`, `userId`, optional `reason`, `deleteMessageDays`)
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- `to` accepts `channel:<id>` or `user:<id>`.
|
|
||||||
- Polls require 2–10 answers and default to 24 hours.
|
|
||||||
- `reactions` returns per-emoji user lists (limited to 100 per reaction).
|
|
||||||
- Reaction removal semantics: see [/tools/reactions](/tools/reactions).
|
|
||||||
- `discord.actions.*` gates Discord tool actions; `roles` + `moderation` default to `false`.
|
|
||||||
- `searchMessages` follows the Discord preview feature constraints (limit max 25, channel/author filters accept arrays).
|
|
||||||
- The tool is only exposed when the current provider is Discord.
|
|
||||||
|
|
||||||
### `whatsapp`
|
|
||||||
Send WhatsApp reactions.
|
|
||||||
|
|
||||||
Core actions:
|
|
||||||
- `react` (`chatJid`, `messageId`, `emoji`, optional `remove`, `participant`, `fromMe`, `accountId`)
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- Reaction removal semantics: see [/tools/reactions](/tools/reactions).
|
|
||||||
- `whatsapp.actions.*` gates WhatsApp tool actions.
|
|
||||||
- The tool is only exposed when the current provider is WhatsApp.
|
|
||||||
|
|
||||||
### `telegram`
|
|
||||||
Send Telegram messages or reactions.
|
|
||||||
|
|
||||||
Core actions:
|
|
||||||
- `sendMessage` (`to`, `content`, optional `mediaUrl`, `replyToMessageId`, `messageThreadId`)
|
|
||||||
- `react` (`chatId`, `messageId`, `emoji`, optional `remove`)
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- Reaction removal semantics: see [/tools/reactions](/tools/reactions).
|
|
||||||
- `telegram.actions.*` gates Telegram tool actions.
|
|
||||||
- The tool is only exposed when the current provider is Telegram.
|
|
||||||
|
|
||||||
## Parameters (common)
|
## Parameters (common)
|
||||||
|
|
||||||
Gateway-backed tools (`canvas`, `nodes`, `cron`):
|
Gateway-backed tools (`canvas`, `nodes`, `cron`):
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { createBrowserTool } from "./tools/browser-tool.js";
|
|||||||
import { createCanvasTool } from "./tools/canvas-tool.js";
|
import { createCanvasTool } from "./tools/canvas-tool.js";
|
||||||
import type { AnyAgentTool } from "./tools/common.js";
|
import type { AnyAgentTool } from "./tools/common.js";
|
||||||
import { createCronTool } from "./tools/cron-tool.js";
|
import { createCronTool } from "./tools/cron-tool.js";
|
||||||
import { createDiscordTool } from "./tools/discord-tool.js";
|
|
||||||
import { createGatewayTool } from "./tools/gateway-tool.js";
|
import { createGatewayTool } from "./tools/gateway-tool.js";
|
||||||
import { createImageTool } from "./tools/image-tool.js";
|
import { createImageTool } from "./tools/image-tool.js";
|
||||||
import { createMessageTool } from "./tools/message-tool.js";
|
import { createMessageTool } from "./tools/message-tool.js";
|
||||||
@@ -13,9 +12,6 @@ import { createSessionsHistoryTool } from "./tools/sessions-history-tool.js";
|
|||||||
import { createSessionsListTool } from "./tools/sessions-list-tool.js";
|
import { createSessionsListTool } from "./tools/sessions-list-tool.js";
|
||||||
import { createSessionsSendTool } from "./tools/sessions-send-tool.js";
|
import { createSessionsSendTool } from "./tools/sessions-send-tool.js";
|
||||||
import { createSessionsSpawnTool } from "./tools/sessions-spawn-tool.js";
|
import { createSessionsSpawnTool } from "./tools/sessions-spawn-tool.js";
|
||||||
import { createSlackTool } from "./tools/slack-tool.js";
|
|
||||||
import { createTelegramTool } from "./tools/telegram-tool.js";
|
|
||||||
import { createWhatsAppTool } from "./tools/whatsapp-tool.js";
|
|
||||||
|
|
||||||
export function createClawdbotTools(options?: {
|
export function createClawdbotTools(options?: {
|
||||||
browserControlUrl?: string;
|
browserControlUrl?: string;
|
||||||
@@ -35,14 +31,10 @@ export function createClawdbotTools(options?: {
|
|||||||
createCanvasTool(),
|
createCanvasTool(),
|
||||||
createNodesTool(),
|
createNodesTool(),
|
||||||
createCronTool(),
|
createCronTool(),
|
||||||
createDiscordTool(),
|
createMessageTool({
|
||||||
createMessageTool(),
|
|
||||||
createSlackTool({
|
|
||||||
agentAccountId: options?.agentAccountId,
|
agentAccountId: options?.agentAccountId,
|
||||||
config: options?.config,
|
config: options?.config,
|
||||||
}),
|
}),
|
||||||
createTelegramTool(),
|
|
||||||
createWhatsAppTool(),
|
|
||||||
createGatewayTool({
|
createGatewayTool({
|
||||||
agentSessionKey: options?.agentSessionKey,
|
agentSessionKey: options?.agentSessionKey,
|
||||||
config: options?.config,
|
config: options?.config,
|
||||||
|
|||||||
@@ -141,36 +141,13 @@ describe("createClawdbotCodingTools", () => {
|
|||||||
expect(offenders).toEqual([]);
|
expect(offenders).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("scopes discord tool to discord provider", () => {
|
it("does not expose provider-specific message tools", () => {
|
||||||
const other = createClawdbotCodingTools({ messageProvider: "whatsapp" });
|
const tools = createClawdbotCodingTools({ messageProvider: "discord" });
|
||||||
expect(other.some((tool) => tool.name === "discord")).toBe(false);
|
const names = new Set(tools.map((tool) => tool.name));
|
||||||
|
expect(names.has("discord")).toBe(false);
|
||||||
const discord = createClawdbotCodingTools({ messageProvider: "discord" });
|
expect(names.has("slack")).toBe(false);
|
||||||
expect(discord.some((tool) => tool.name === "discord")).toBe(true);
|
expect(names.has("telegram")).toBe(false);
|
||||||
});
|
expect(names.has("whatsapp")).toBe(false);
|
||||||
|
|
||||||
it("scopes slack tool to slack provider", () => {
|
|
||||||
const other = createClawdbotCodingTools({ messageProvider: "whatsapp" });
|
|
||||||
expect(other.some((tool) => tool.name === "slack")).toBe(false);
|
|
||||||
|
|
||||||
const slack = createClawdbotCodingTools({ messageProvider: "slack" });
|
|
||||||
expect(slack.some((tool) => tool.name === "slack")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("scopes telegram tool to telegram provider", () => {
|
|
||||||
const other = createClawdbotCodingTools({ messageProvider: "whatsapp" });
|
|
||||||
expect(other.some((tool) => tool.name === "telegram")).toBe(false);
|
|
||||||
|
|
||||||
const telegram = createClawdbotCodingTools({ messageProvider: "telegram" });
|
|
||||||
expect(telegram.some((tool) => tool.name === "telegram")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("scopes whatsapp tool to whatsapp provider", () => {
|
|
||||||
const other = createClawdbotCodingTools({ messageProvider: "slack" });
|
|
||||||
expect(other.some((tool) => tool.name === "whatsapp")).toBe(false);
|
|
||||||
|
|
||||||
const whatsapp = createClawdbotCodingTools({ messageProvider: "whatsapp" });
|
|
||||||
expect(whatsapp.some((tool) => tool.name === "whatsapp")).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("filters session tools for sub-agent sessions by default", () => {
|
it("filters session tools for sub-agent sessions by default", () => {
|
||||||
|
|||||||
@@ -613,37 +613,6 @@ function createClawdbotReadTool(base: AnyAgentTool): AnyAgentTool {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeMessageProvider(
|
|
||||||
messageProvider?: string,
|
|
||||||
): string | undefined {
|
|
||||||
const trimmed = messageProvider?.trim().toLowerCase();
|
|
||||||
return trimmed ? trimmed : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldIncludeDiscordTool(messageProvider?: string): boolean {
|
|
||||||
const normalized = normalizeMessageProvider(messageProvider);
|
|
||||||
if (!normalized) return false;
|
|
||||||
return normalized === "discord" || normalized.startsWith("discord:");
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldIncludeSlackTool(messageProvider?: string): boolean {
|
|
||||||
const normalized = normalizeMessageProvider(messageProvider);
|
|
||||||
if (!normalized) return false;
|
|
||||||
return normalized === "slack" || normalized.startsWith("slack:");
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldIncludeTelegramTool(messageProvider?: string): boolean {
|
|
||||||
const normalized = normalizeMessageProvider(messageProvider);
|
|
||||||
if (!normalized) return false;
|
|
||||||
return normalized === "telegram" || normalized.startsWith("telegram:");
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldIncludeWhatsAppTool(messageProvider?: string): boolean {
|
|
||||||
const normalized = normalizeMessageProvider(messageProvider);
|
|
||||||
if (!normalized) return false;
|
|
||||||
return normalized === "whatsapp" || normalized.startsWith("whatsapp:");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createClawdbotCodingTools(options?: {
|
export function createClawdbotCodingTools(options?: {
|
||||||
bash?: BashToolDefaults & ProcessToolDefaults;
|
bash?: BashToolDefaults & ProcessToolDefaults;
|
||||||
messageProvider?: string;
|
messageProvider?: string;
|
||||||
@@ -724,20 +693,9 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
config: options?.config,
|
config: options?.config,
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
const allowDiscord = shouldIncludeDiscordTool(options?.messageProvider);
|
|
||||||
const allowSlack = shouldIncludeSlackTool(options?.messageProvider);
|
|
||||||
const allowTelegram = shouldIncludeTelegramTool(options?.messageProvider);
|
|
||||||
const allowWhatsApp = shouldIncludeWhatsAppTool(options?.messageProvider);
|
|
||||||
const filtered = tools.filter((tool) => {
|
|
||||||
if (tool.name === "discord") return allowDiscord;
|
|
||||||
if (tool.name === "slack") return allowSlack;
|
|
||||||
if (tool.name === "telegram") return allowTelegram;
|
|
||||||
if (tool.name === "whatsapp") return allowWhatsApp;
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
const toolsFiltered = effectiveToolsPolicy
|
const toolsFiltered = effectiveToolsPolicy
|
||||||
? filterToolsByPolicy(filtered, effectiveToolsPolicy)
|
? filterToolsByPolicy(tools, effectiveToolsPolicy)
|
||||||
: filtered;
|
: tools;
|
||||||
const sandboxed = sandbox
|
const sandboxed = sandbox
|
||||||
? filterToolsByPolicy(toolsFiltered, sandbox.tools)
|
? filterToolsByPolicy(toolsFiltered, sandbox.tools)
|
||||||
: toolsFiltered;
|
: toolsFiltered;
|
||||||
|
|||||||
@@ -154,8 +154,37 @@
|
|||||||
"emoji": "✉️",
|
"emoji": "✉️",
|
||||||
"title": "Message",
|
"title": "Message",
|
||||||
"actions": {
|
"actions": {
|
||||||
"send": { "label": "send", "detailKeys": ["to", "provider", "mediaUrl"] },
|
"send": { "label": "send", "detailKeys": ["provider", "to", "media", "replyTo", "threadId"] },
|
||||||
"poll": { "label": "poll", "detailKeys": ["to", "provider", "question"] }
|
"poll": { "label": "poll", "detailKeys": ["provider", "to", "pollQuestion"] },
|
||||||
|
"react": { "label": "react", "detailKeys": ["provider", "to", "messageId", "emoji", "remove"] },
|
||||||
|
"reactions": { "label": "reactions", "detailKeys": ["provider", "to", "messageId", "limit"] },
|
||||||
|
"read": { "label": "read", "detailKeys": ["provider", "to", "limit"] },
|
||||||
|
"edit": { "label": "edit", "detailKeys": ["provider", "to", "messageId"] },
|
||||||
|
"delete": { "label": "delete", "detailKeys": ["provider", "to", "messageId"] },
|
||||||
|
"pin": { "label": "pin", "detailKeys": ["provider", "to", "messageId"] },
|
||||||
|
"unpin": { "label": "unpin", "detailKeys": ["provider", "to", "messageId"] },
|
||||||
|
"list-pins": { "label": "list pins", "detailKeys": ["provider", "to"] },
|
||||||
|
"permissions": { "label": "permissions", "detailKeys": ["provider", "channelId", "to"] },
|
||||||
|
"thread-create": { "label": "thread create", "detailKeys": ["provider", "channelId", "threadName"] },
|
||||||
|
"thread-list": { "label": "thread list", "detailKeys": ["provider", "guildId", "channelId"] },
|
||||||
|
"thread-reply": { "label": "thread reply", "detailKeys": ["provider", "channelId", "messageId"] },
|
||||||
|
"search": { "label": "search", "detailKeys": ["provider", "guildId", "query"] },
|
||||||
|
"sticker": { "label": "sticker", "detailKeys": ["provider", "to", "stickerId"] },
|
||||||
|
"member-info": { "label": "member", "detailKeys": ["provider", "guildId", "userId"] },
|
||||||
|
"role-info": { "label": "roles", "detailKeys": ["provider", "guildId"] },
|
||||||
|
"emoji-list": { "label": "emoji list", "detailKeys": ["provider", "guildId"] },
|
||||||
|
"emoji-upload": { "label": "emoji upload", "detailKeys": ["provider", "guildId", "emojiName"] },
|
||||||
|
"sticker-upload": { "label": "sticker upload", "detailKeys": ["provider", "guildId", "stickerName"] },
|
||||||
|
"role-add": { "label": "role add", "detailKeys": ["provider", "guildId", "userId", "roleId"] },
|
||||||
|
"role-remove": { "label": "role remove", "detailKeys": ["provider", "guildId", "userId", "roleId"] },
|
||||||
|
"channel-info": { "label": "channel", "detailKeys": ["provider", "channelId"] },
|
||||||
|
"channel-list": { "label": "channels", "detailKeys": ["provider", "guildId"] },
|
||||||
|
"voice-status": { "label": "voice", "detailKeys": ["provider", "guildId", "userId"] },
|
||||||
|
"event-list": { "label": "events", "detailKeys": ["provider", "guildId"] },
|
||||||
|
"event-create": { "label": "event create", "detailKeys": ["provider", "guildId", "eventName"] },
|
||||||
|
"timeout": { "label": "timeout", "detailKeys": ["provider", "guildId", "userId"] },
|
||||||
|
"kick": { "label": "kick", "detailKeys": ["provider", "guildId", "userId"] },
|
||||||
|
"ban": { "label": "ban", "detailKeys": ["provider", "guildId", "userId"] }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"agents_list": {
|
"agents_list": {
|
||||||
@@ -190,77 +219,6 @@
|
|||||||
"start": { "label": "start" },
|
"start": { "label": "start" },
|
||||||
"wait": { "label": "wait" }
|
"wait": { "label": "wait" }
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"discord": {
|
|
||||||
"emoji": "💬",
|
|
||||||
"title": "Discord",
|
|
||||||
"actions": {
|
|
||||||
"react": { "label": "react", "detailKeys": ["channelId", "messageId", "emoji", "remove"] },
|
|
||||||
"reactions": { "label": "reactions", "detailKeys": ["channelId", "messageId"] },
|
|
||||||
"sticker": { "label": "sticker", "detailKeys": ["to", "stickerIds"] },
|
|
||||||
"poll": { "label": "poll", "detailKeys": ["question", "to"] },
|
|
||||||
"permissions": { "label": "permissions", "detailKeys": ["channelId"] },
|
|
||||||
"readMessages": { "label": "read messages", "detailKeys": ["channelId", "limit"] },
|
|
||||||
"sendMessage": { "label": "send", "detailKeys": ["to", "content"] },
|
|
||||||
"editMessage": { "label": "edit", "detailKeys": ["channelId", "messageId"] },
|
|
||||||
"deleteMessage": { "label": "delete", "detailKeys": ["channelId", "messageId"] },
|
|
||||||
"threadCreate": { "label": "thread create", "detailKeys": ["channelId", "name"] },
|
|
||||||
"threadList": { "label": "thread list", "detailKeys": ["guildId", "channelId"] },
|
|
||||||
"threadReply": { "label": "thread reply", "detailKeys": ["channelId", "content"] },
|
|
||||||
"pinMessage": { "label": "pin", "detailKeys": ["channelId", "messageId"] },
|
|
||||||
"unpinMessage": { "label": "unpin", "detailKeys": ["channelId", "messageId"] },
|
|
||||||
"listPins": { "label": "list pins", "detailKeys": ["channelId"] },
|
|
||||||
"searchMessages": { "label": "search", "detailKeys": ["guildId", "content"] },
|
|
||||||
"memberInfo": { "label": "member", "detailKeys": ["guildId", "userId"] },
|
|
||||||
"roleInfo": { "label": "roles", "detailKeys": ["guildId"] },
|
|
||||||
"emojiList": { "label": "emoji list", "detailKeys": ["guildId"] },
|
|
||||||
"emojiUpload": { "label": "emoji upload", "detailKeys": ["guildId", "name"] },
|
|
||||||
"stickerUpload": { "label": "sticker upload", "detailKeys": ["guildId", "name"] },
|
|
||||||
"roleAdd": { "label": "role add", "detailKeys": ["guildId", "userId", "roleId"] },
|
|
||||||
"roleRemove": { "label": "role remove", "detailKeys": ["guildId", "userId", "roleId"] },
|
|
||||||
"channelInfo": { "label": "channel", "detailKeys": ["channelId"] },
|
|
||||||
"channelList": { "label": "channels", "detailKeys": ["guildId"] },
|
|
||||||
"voiceStatus": { "label": "voice", "detailKeys": ["guildId", "userId"] },
|
|
||||||
"eventList": { "label": "events", "detailKeys": ["guildId"] },
|
|
||||||
"eventCreate": { "label": "event create", "detailKeys": ["guildId", "name"] },
|
|
||||||
"timeout": { "label": "timeout", "detailKeys": ["guildId", "userId"] },
|
|
||||||
"kick": { "label": "kick", "detailKeys": ["guildId", "userId"] },
|
|
||||||
"ban": { "label": "ban", "detailKeys": ["guildId", "userId"] }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"slack": {
|
|
||||||
"emoji": "💬",
|
|
||||||
"title": "Slack",
|
|
||||||
"actions": {
|
|
||||||
"react": { "label": "react", "detailKeys": ["channelId", "messageId", "emoji", "remove"] },
|
|
||||||
"reactions": { "label": "reactions", "detailKeys": ["channelId", "messageId"] },
|
|
||||||
"sendMessage": { "label": "send", "detailKeys": ["to", "content"] },
|
|
||||||
"editMessage": { "label": "edit", "detailKeys": ["channelId", "messageId"] },
|
|
||||||
"deleteMessage": { "label": "delete", "detailKeys": ["channelId", "messageId"] },
|
|
||||||
"readMessages": { "label": "read messages", "detailKeys": ["channelId", "limit"] },
|
|
||||||
"pinMessage": { "label": "pin", "detailKeys": ["channelId", "messageId"] },
|
|
||||||
"unpinMessage": { "label": "unpin", "detailKeys": ["channelId", "messageId"] },
|
|
||||||
"listPins": { "label": "list pins", "detailKeys": ["channelId"] },
|
|
||||||
"memberInfo": { "label": "member", "detailKeys": ["userId"] },
|
|
||||||
"emojiList": { "label": "emoji list" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"telegram": {
|
|
||||||
"emoji": "✈️",
|
|
||||||
"title": "Telegram",
|
|
||||||
"actions": {
|
|
||||||
"react": { "label": "react", "detailKeys": ["chatId", "messageId", "emoji", "remove"] }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"whatsapp": {
|
|
||||||
"emoji": "💬",
|
|
||||||
"title": "WhatsApp",
|
|
||||||
"actions": {
|
|
||||||
"react": {
|
|
||||||
"label": "react",
|
|
||||||
"detailKeys": ["chatJid", "messageId", "emoji", "remove", "participant", "accountId", "fromMe"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { Type } from "@sinclair/typebox";
|
import { Type } from "@sinclair/typebox";
|
||||||
|
|
||||||
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
|
import { loadConfig } from "../../config/config.js";
|
||||||
import {
|
import {
|
||||||
type MessagePollResult,
|
type MessagePollResult,
|
||||||
type MessageSendResult,
|
type MessageSendResult,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
sendPoll,
|
sendPoll,
|
||||||
} from "../../infra/outbound/message.js";
|
} from "../../infra/outbound/message.js";
|
||||||
|
import { resolveMessageProviderSelection } from "../../infra/outbound/provider-selection.js";
|
||||||
|
import { normalizeAccountId } from "../../routing/session-key.js";
|
||||||
import type { AnyAgentTool } from "./common.js";
|
import type { AnyAgentTool } from "./common.js";
|
||||||
import {
|
import {
|
||||||
jsonResult,
|
jsonResult,
|
||||||
@@ -13,36 +17,131 @@ import {
|
|||||||
readStringArrayParam,
|
readStringArrayParam,
|
||||||
readStringParam,
|
readStringParam,
|
||||||
} from "./common.js";
|
} from "./common.js";
|
||||||
|
import { handleDiscordAction } from "./discord-actions.js";
|
||||||
|
import { handleSlackAction } from "./slack-actions.js";
|
||||||
|
import { handleTelegramAction } from "./telegram-actions.js";
|
||||||
|
import { handleWhatsAppAction } from "./whatsapp-actions.js";
|
||||||
|
|
||||||
|
const MessageActionSchema = Type.Union([
|
||||||
|
Type.Literal("send"),
|
||||||
|
Type.Literal("poll"),
|
||||||
|
Type.Literal("react"),
|
||||||
|
Type.Literal("reactions"),
|
||||||
|
Type.Literal("read"),
|
||||||
|
Type.Literal("edit"),
|
||||||
|
Type.Literal("delete"),
|
||||||
|
Type.Literal("pin"),
|
||||||
|
Type.Literal("unpin"),
|
||||||
|
Type.Literal("list-pins"),
|
||||||
|
Type.Literal("permissions"),
|
||||||
|
Type.Literal("thread-create"),
|
||||||
|
Type.Literal("thread-list"),
|
||||||
|
Type.Literal("thread-reply"),
|
||||||
|
Type.Literal("search"),
|
||||||
|
Type.Literal("sticker"),
|
||||||
|
Type.Literal("member-info"),
|
||||||
|
Type.Literal("role-info"),
|
||||||
|
Type.Literal("emoji-list"),
|
||||||
|
Type.Literal("emoji-upload"),
|
||||||
|
Type.Literal("sticker-upload"),
|
||||||
|
Type.Literal("role-add"),
|
||||||
|
Type.Literal("role-remove"),
|
||||||
|
Type.Literal("channel-info"),
|
||||||
|
Type.Literal("channel-list"),
|
||||||
|
Type.Literal("voice-status"),
|
||||||
|
Type.Literal("event-list"),
|
||||||
|
Type.Literal("event-create"),
|
||||||
|
Type.Literal("timeout"),
|
||||||
|
Type.Literal("kick"),
|
||||||
|
Type.Literal("ban"),
|
||||||
|
]);
|
||||||
|
|
||||||
const MessageToolSchema = Type.Object({
|
const MessageToolSchema = Type.Object({
|
||||||
action: Type.Union([Type.Literal("send"), Type.Literal("poll")]),
|
action: MessageActionSchema,
|
||||||
to: Type.Optional(Type.String()),
|
|
||||||
content: Type.Optional(Type.String()),
|
|
||||||
mediaUrl: Type.Optional(Type.String()),
|
|
||||||
gifPlayback: Type.Optional(Type.Boolean()),
|
|
||||||
provider: Type.Optional(Type.String()),
|
provider: Type.Optional(Type.String()),
|
||||||
|
to: Type.Optional(Type.String()),
|
||||||
|
message: Type.Optional(Type.String()),
|
||||||
|
media: Type.Optional(Type.String()),
|
||||||
|
messageId: Type.Optional(Type.String()),
|
||||||
|
replyTo: Type.Optional(Type.String()),
|
||||||
|
threadId: Type.Optional(Type.String()),
|
||||||
accountId: Type.Optional(Type.String()),
|
accountId: Type.Optional(Type.String()),
|
||||||
dryRun: Type.Optional(Type.Boolean()),
|
dryRun: Type.Optional(Type.Boolean()),
|
||||||
bestEffort: Type.Optional(Type.Boolean()),
|
bestEffort: Type.Optional(Type.Boolean()),
|
||||||
question: Type.Optional(Type.String()),
|
gifPlayback: Type.Optional(Type.Boolean()),
|
||||||
options: Type.Optional(Type.Array(Type.String())),
|
emoji: Type.Optional(Type.String()),
|
||||||
maxSelections: Type.Optional(Type.Number()),
|
remove: Type.Optional(Type.Boolean()),
|
||||||
durationHours: Type.Optional(Type.Number()),
|
limit: Type.Optional(Type.Number()),
|
||||||
|
before: Type.Optional(Type.String()),
|
||||||
|
after: Type.Optional(Type.String()),
|
||||||
|
around: Type.Optional(Type.String()),
|
||||||
|
pollQuestion: Type.Optional(Type.String()),
|
||||||
|
pollOption: Type.Optional(Type.Array(Type.String())),
|
||||||
|
pollDurationHours: Type.Optional(Type.Number()),
|
||||||
|
pollMulti: Type.Optional(Type.Boolean()),
|
||||||
|
channelId: Type.Optional(Type.String()),
|
||||||
|
channelIds: Type.Optional(Type.Array(Type.String())),
|
||||||
|
guildId: Type.Optional(Type.String()),
|
||||||
|
userId: Type.Optional(Type.String()),
|
||||||
|
authorId: Type.Optional(Type.String()),
|
||||||
|
authorIds: Type.Optional(Type.Array(Type.String())),
|
||||||
|
roleId: Type.Optional(Type.String()),
|
||||||
|
roleIds: Type.Optional(Type.Array(Type.String())),
|
||||||
|
emojiName: Type.Optional(Type.String()),
|
||||||
|
stickerId: Type.Optional(Type.Array(Type.String())),
|
||||||
|
stickerName: Type.Optional(Type.String()),
|
||||||
|
stickerDesc: Type.Optional(Type.String()),
|
||||||
|
stickerTags: Type.Optional(Type.String()),
|
||||||
|
threadName: Type.Optional(Type.String()),
|
||||||
|
autoArchiveMin: Type.Optional(Type.Number()),
|
||||||
|
query: Type.Optional(Type.String()),
|
||||||
|
eventName: Type.Optional(Type.String()),
|
||||||
|
eventType: Type.Optional(Type.String()),
|
||||||
|
startTime: Type.Optional(Type.String()),
|
||||||
|
endTime: Type.Optional(Type.String()),
|
||||||
|
desc: Type.Optional(Type.String()),
|
||||||
|
location: Type.Optional(Type.String()),
|
||||||
|
durationMin: Type.Optional(Type.Number()),
|
||||||
|
until: Type.Optional(Type.String()),
|
||||||
|
reason: Type.Optional(Type.String()),
|
||||||
|
deleteDays: Type.Optional(Type.Number()),
|
||||||
|
includeArchived: Type.Optional(Type.Boolean()),
|
||||||
|
participant: Type.Optional(Type.String()),
|
||||||
|
fromMe: Type.Optional(Type.Boolean()),
|
||||||
gatewayUrl: Type.Optional(Type.String()),
|
gatewayUrl: Type.Optional(Type.String()),
|
||||||
gatewayToken: Type.Optional(Type.String()),
|
gatewayToken: Type.Optional(Type.String()),
|
||||||
timeoutMs: Type.Optional(Type.Number()),
|
timeoutMs: Type.Optional(Type.Number()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function createMessageTool(): AnyAgentTool {
|
type MessageToolOptions = {
|
||||||
|
agentAccountId?: string;
|
||||||
|
config?: ClawdbotConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveAgentAccountId(value?: string): string | undefined {
|
||||||
|
const trimmed = value?.trim();
|
||||||
|
if (!trimmed) return undefined;
|
||||||
|
return normalizeAccountId(trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
|
||||||
|
const agentAccountId = resolveAgentAccountId(options?.agentAccountId);
|
||||||
return {
|
return {
|
||||||
label: "Message",
|
label: "Message",
|
||||||
name: "message",
|
name: "message",
|
||||||
description:
|
description:
|
||||||
"Send messages and polls across providers (send/poll). Prefer this for general outbound messaging.",
|
"Send messages and provider-specific actions (Discord/Slack/Telegram/WhatsApp/Signal/iMessage).",
|
||||||
parameters: MessageToolSchema,
|
parameters: MessageToolSchema,
|
||||||
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 action = readStringParam(params, "action", { required: true });
|
const action = readStringParam(params, "action", { required: true });
|
||||||
|
const providerSelection = await resolveMessageProviderSelection({
|
||||||
|
cfg,
|
||||||
|
provider: readStringParam(params, "provider"),
|
||||||
|
});
|
||||||
|
const provider = providerSelection.provider;
|
||||||
|
const accountId = readStringParam(params, "accountId") ?? agentAccountId;
|
||||||
const gateway = {
|
const gateway = {
|
||||||
url: readStringParam(params, "gatewayUrl", { trim: false }),
|
url: readStringParam(params, "gatewayUrl", { trim: false }),
|
||||||
token: readStringParam(params, "gatewayToken", { trim: false }),
|
token: readStringParam(params, "gatewayToken", { trim: false }),
|
||||||
@@ -54,13 +153,13 @@ export function createMessageTool(): AnyAgentTool {
|
|||||||
|
|
||||||
if (action === "send") {
|
if (action === "send") {
|
||||||
const to = readStringParam(params, "to", { required: true });
|
const to = readStringParam(params, "to", { required: true });
|
||||||
const content = readStringParam(params, "content", {
|
const message = readStringParam(params, "message", {
|
||||||
required: true,
|
required: true,
|
||||||
allowEmpty: true,
|
allowEmpty: true,
|
||||||
});
|
});
|
||||||
const mediaUrl = readStringParam(params, "mediaUrl", { trim: false });
|
const mediaUrl = readStringParam(params, "media", { trim: false });
|
||||||
const provider = readStringParam(params, "provider");
|
const replyTo = readStringParam(params, "replyTo");
|
||||||
const accountId = readStringParam(params, "accountId");
|
const threadId = readStringParam(params, "threadId");
|
||||||
const gifPlayback =
|
const gifPlayback =
|
||||||
typeof params.gifPlayback === "boolean" ? params.gifPlayback : false;
|
typeof params.gifPlayback === "boolean" ? params.gifPlayback : false;
|
||||||
const bestEffort =
|
const bestEffort =
|
||||||
@@ -68,12 +167,66 @@ export function createMessageTool(): AnyAgentTool {
|
|||||||
? params.bestEffort
|
? params.bestEffort
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
if (dryRun) {
|
||||||
|
const result: MessageSendResult = await sendMessage({
|
||||||
|
to,
|
||||||
|
content: message,
|
||||||
|
mediaUrl: mediaUrl || undefined,
|
||||||
|
provider: provider || undefined,
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
|
gifPlayback,
|
||||||
|
dryRun,
|
||||||
|
bestEffort,
|
||||||
|
gateway,
|
||||||
|
});
|
||||||
|
return jsonResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider === "discord") {
|
||||||
|
return await handleDiscordAction(
|
||||||
|
{
|
||||||
|
action: "sendMessage",
|
||||||
|
to,
|
||||||
|
content: message,
|
||||||
|
mediaUrl: mediaUrl ?? undefined,
|
||||||
|
replyTo: replyTo ?? undefined,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (provider === "slack") {
|
||||||
|
return await handleSlackAction(
|
||||||
|
{
|
||||||
|
action: "sendMessage",
|
||||||
|
to,
|
||||||
|
content: message,
|
||||||
|
mediaUrl: mediaUrl ?? undefined,
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
|
threadTs: threadId ?? replyTo ?? undefined,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (provider === "telegram") {
|
||||||
|
return await handleTelegramAction(
|
||||||
|
{
|
||||||
|
action: "sendMessage",
|
||||||
|
to,
|
||||||
|
content: message,
|
||||||
|
mediaUrl: mediaUrl ?? undefined,
|
||||||
|
replyToMessageId: replyTo ?? undefined,
|
||||||
|
messageThreadId: threadId ?? undefined,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const result: MessageSendResult = await sendMessage({
|
const result: MessageSendResult = await sendMessage({
|
||||||
to,
|
to,
|
||||||
content,
|
content: message,
|
||||||
mediaUrl: mediaUrl || undefined,
|
mediaUrl: mediaUrl || undefined,
|
||||||
provider: provider || undefined,
|
provider: provider || undefined,
|
||||||
accountId: accountId || undefined,
|
accountId: accountId ?? undefined,
|
||||||
gifPlayback,
|
gifPlayback,
|
||||||
dryRun,
|
dryRun,
|
||||||
bestEffort,
|
bestEffort,
|
||||||
@@ -84,32 +237,679 @@ export function createMessageTool(): AnyAgentTool {
|
|||||||
|
|
||||||
if (action === "poll") {
|
if (action === "poll") {
|
||||||
const to = readStringParam(params, "to", { required: true });
|
const to = readStringParam(params, "to", { required: true });
|
||||||
const question = readStringParam(params, "question", {
|
const question = readStringParam(params, "pollQuestion", {
|
||||||
required: true,
|
required: true,
|
||||||
});
|
});
|
||||||
const options =
|
const options =
|
||||||
readStringArrayParam(params, "options", { required: true }) ?? [];
|
readStringArrayParam(params, "pollOption", { required: true }) ?? [];
|
||||||
const maxSelections = readNumberParam(params, "maxSelections", {
|
const allowMultiselect =
|
||||||
|
typeof params.pollMulti === "boolean" ? params.pollMulti : undefined;
|
||||||
|
const durationHours = readNumberParam(params, "pollDurationHours", {
|
||||||
integer: true,
|
integer: true,
|
||||||
});
|
});
|
||||||
const durationHours = readNumberParam(params, "durationHours", {
|
|
||||||
integer: true,
|
|
||||||
});
|
|
||||||
const provider = readStringParam(params, "provider");
|
|
||||||
|
|
||||||
|
if (dryRun) {
|
||||||
|
const maxSelections = allowMultiselect
|
||||||
|
? Math.max(2, options.length)
|
||||||
|
: 1;
|
||||||
|
const result: MessagePollResult = await sendPoll({
|
||||||
|
to,
|
||||||
|
question,
|
||||||
|
options,
|
||||||
|
maxSelections,
|
||||||
|
durationHours: durationHours ?? undefined,
|
||||||
|
provider,
|
||||||
|
dryRun,
|
||||||
|
gateway,
|
||||||
|
});
|
||||||
|
return jsonResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider === "discord") {
|
||||||
|
return await handleDiscordAction(
|
||||||
|
{
|
||||||
|
action: "poll",
|
||||||
|
to,
|
||||||
|
question,
|
||||||
|
answers: options,
|
||||||
|
allowMultiselect,
|
||||||
|
durationHours: durationHours ?? undefined,
|
||||||
|
content: readStringParam(params, "message"),
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxSelections = allowMultiselect
|
||||||
|
? Math.max(2, options.length)
|
||||||
|
: 1;
|
||||||
const result: MessagePollResult = await sendPoll({
|
const result: MessagePollResult = await sendPoll({
|
||||||
to,
|
to,
|
||||||
question,
|
question,
|
||||||
options,
|
options,
|
||||||
maxSelections,
|
maxSelections,
|
||||||
durationHours,
|
durationHours: durationHours ?? undefined,
|
||||||
provider: provider || undefined,
|
provider,
|
||||||
dryRun,
|
dryRun,
|
||||||
gateway,
|
gateway,
|
||||||
});
|
});
|
||||||
return jsonResult(result);
|
return jsonResult(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resolveChannelId = (label: string) =>
|
||||||
|
readStringParam(params, label) ??
|
||||||
|
readStringParam(params, "to", { required: true });
|
||||||
|
|
||||||
|
const resolveChatId = (label: string) =>
|
||||||
|
readStringParam(params, label) ??
|
||||||
|
readStringParam(params, "to", { required: true });
|
||||||
|
|
||||||
|
if (action === "react") {
|
||||||
|
const messageId = readStringParam(params, "messageId", {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const emoji = readStringParam(params, "emoji", { allowEmpty: true });
|
||||||
|
const remove =
|
||||||
|
typeof params.remove === "boolean" ? params.remove : undefined;
|
||||||
|
if (provider === "discord") {
|
||||||
|
return await handleDiscordAction(
|
||||||
|
{
|
||||||
|
action: "react",
|
||||||
|
channelId: resolveChannelId("channelId"),
|
||||||
|
messageId,
|
||||||
|
emoji,
|
||||||
|
remove,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (provider === "slack") {
|
||||||
|
return await handleSlackAction(
|
||||||
|
{
|
||||||
|
action: "react",
|
||||||
|
channelId: resolveChannelId("channelId"),
|
||||||
|
messageId,
|
||||||
|
emoji,
|
||||||
|
remove,
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (provider === "telegram") {
|
||||||
|
return await handleTelegramAction(
|
||||||
|
{
|
||||||
|
action: "react",
|
||||||
|
chatId: resolveChatId("chatId"),
|
||||||
|
messageId,
|
||||||
|
emoji,
|
||||||
|
remove,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (provider === "whatsapp") {
|
||||||
|
return await handleWhatsAppAction(
|
||||||
|
{
|
||||||
|
action: "react",
|
||||||
|
chatJid: resolveChatId("chatJid"),
|
||||||
|
messageId,
|
||||||
|
emoji,
|
||||||
|
remove,
|
||||||
|
participant: readStringParam(params, "participant"),
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
|
fromMe:
|
||||||
|
typeof params.fromMe === "boolean" ? params.fromMe : undefined,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new Error(`React is not supported for provider ${provider}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "reactions") {
|
||||||
|
const messageId = readStringParam(params, "messageId", {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const limit = readNumberParam(params, "limit", { integer: true });
|
||||||
|
if (provider === "discord") {
|
||||||
|
return await handleDiscordAction(
|
||||||
|
{
|
||||||
|
action: "reactions",
|
||||||
|
channelId: resolveChannelId("channelId"),
|
||||||
|
messageId,
|
||||||
|
limit,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (provider === "slack") {
|
||||||
|
return await handleSlackAction(
|
||||||
|
{
|
||||||
|
action: "reactions",
|
||||||
|
channelId: resolveChannelId("channelId"),
|
||||||
|
messageId,
|
||||||
|
limit,
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`Reactions are not supported for provider ${provider}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "read") {
|
||||||
|
const limit = readNumberParam(params, "limit", { integer: true });
|
||||||
|
const before = readStringParam(params, "before");
|
||||||
|
const after = readStringParam(params, "after");
|
||||||
|
const around = readStringParam(params, "around");
|
||||||
|
if (provider === "discord") {
|
||||||
|
return await handleDiscordAction(
|
||||||
|
{
|
||||||
|
action: "readMessages",
|
||||||
|
channelId: resolveChannelId("channelId"),
|
||||||
|
limit,
|
||||||
|
before,
|
||||||
|
after,
|
||||||
|
around,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (provider === "slack") {
|
||||||
|
return await handleSlackAction(
|
||||||
|
{
|
||||||
|
action: "readMessages",
|
||||||
|
channelId: resolveChannelId("channelId"),
|
||||||
|
limit,
|
||||||
|
before,
|
||||||
|
after,
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new Error(`Read is not supported for provider ${provider}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "edit") {
|
||||||
|
const messageId = readStringParam(params, "messageId", {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const message = readStringParam(params, "message", { required: true });
|
||||||
|
if (provider === "discord") {
|
||||||
|
return await handleDiscordAction(
|
||||||
|
{
|
||||||
|
action: "editMessage",
|
||||||
|
channelId: resolveChannelId("channelId"),
|
||||||
|
messageId,
|
||||||
|
content: message,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (provider === "slack") {
|
||||||
|
return await handleSlackAction(
|
||||||
|
{
|
||||||
|
action: "editMessage",
|
||||||
|
channelId: resolveChannelId("channelId"),
|
||||||
|
messageId,
|
||||||
|
content: message,
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new Error(`Edit is not supported for provider ${provider}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "delete") {
|
||||||
|
const messageId = readStringParam(params, "messageId", {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
if (provider === "discord") {
|
||||||
|
return await handleDiscordAction(
|
||||||
|
{
|
||||||
|
action: "deleteMessage",
|
||||||
|
channelId: resolveChannelId("channelId"),
|
||||||
|
messageId,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (provider === "slack") {
|
||||||
|
return await handleSlackAction(
|
||||||
|
{
|
||||||
|
action: "deleteMessage",
|
||||||
|
channelId: resolveChannelId("channelId"),
|
||||||
|
messageId,
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new Error(`Delete is not supported for provider ${provider}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "pin" || action === "unpin" || action === "list-pins") {
|
||||||
|
const messageId =
|
||||||
|
action === "list-pins"
|
||||||
|
? undefined
|
||||||
|
: readStringParam(params, "messageId", { required: true });
|
||||||
|
const channelId = resolveChannelId("channelId");
|
||||||
|
if (provider === "discord") {
|
||||||
|
const discordAction =
|
||||||
|
action === "pin"
|
||||||
|
? "pinMessage"
|
||||||
|
: action === "unpin"
|
||||||
|
? "unpinMessage"
|
||||||
|
: "listPins";
|
||||||
|
return await handleDiscordAction(
|
||||||
|
{
|
||||||
|
action: discordAction,
|
||||||
|
channelId,
|
||||||
|
messageId,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (provider === "slack") {
|
||||||
|
const slackAction =
|
||||||
|
action === "pin"
|
||||||
|
? "pinMessage"
|
||||||
|
: action === "unpin"
|
||||||
|
? "unpinMessage"
|
||||||
|
: "listPins";
|
||||||
|
return await handleSlackAction(
|
||||||
|
{
|
||||||
|
action: slackAction,
|
||||||
|
channelId,
|
||||||
|
messageId,
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new Error(`Pins are not supported for provider ${provider}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "permissions") {
|
||||||
|
if (provider !== "discord") {
|
||||||
|
throw new Error(
|
||||||
|
`Permissions are only supported for Discord (provider=${provider}).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return await handleDiscordAction(
|
||||||
|
{
|
||||||
|
action: "permissions",
|
||||||
|
channelId: resolveChannelId("channelId"),
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "thread-create") {
|
||||||
|
if (provider !== "discord") {
|
||||||
|
throw new Error(
|
||||||
|
`Thread create is only supported for Discord (provider=${provider}).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const name = readStringParam(params, "threadName", { required: true });
|
||||||
|
const messageId = readStringParam(params, "messageId");
|
||||||
|
const autoArchiveMinutes = readNumberParam(params, "autoArchiveMin", {
|
||||||
|
integer: true,
|
||||||
|
});
|
||||||
|
return await handleDiscordAction(
|
||||||
|
{
|
||||||
|
action: "threadCreate",
|
||||||
|
channelId: resolveChannelId("channelId"),
|
||||||
|
name,
|
||||||
|
messageId,
|
||||||
|
autoArchiveMinutes,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "thread-list") {
|
||||||
|
if (provider !== "discord") {
|
||||||
|
throw new Error(
|
||||||
|
`Thread list is only supported for Discord (provider=${provider}).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const guildId = readStringParam(params, "guildId", { required: true });
|
||||||
|
const channelId = readStringParam(params, "channelId");
|
||||||
|
const includeArchived =
|
||||||
|
typeof params.includeArchived === "boolean"
|
||||||
|
? params.includeArchived
|
||||||
|
: undefined;
|
||||||
|
const before = readStringParam(params, "before");
|
||||||
|
const limit = readNumberParam(params, "limit", { integer: true });
|
||||||
|
return await handleDiscordAction(
|
||||||
|
{
|
||||||
|
action: "threadList",
|
||||||
|
guildId,
|
||||||
|
channelId,
|
||||||
|
includeArchived,
|
||||||
|
before,
|
||||||
|
limit,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "thread-reply") {
|
||||||
|
if (provider !== "discord") {
|
||||||
|
throw new Error(
|
||||||
|
`Thread reply is only supported for Discord (provider=${provider}).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const content = readStringParam(params, "message", { required: true });
|
||||||
|
const mediaUrl = readStringParam(params, "media", { trim: false });
|
||||||
|
const replyTo = readStringParam(params, "replyTo");
|
||||||
|
return await handleDiscordAction(
|
||||||
|
{
|
||||||
|
action: "threadReply",
|
||||||
|
channelId: resolveChannelId("channelId"),
|
||||||
|
content,
|
||||||
|
mediaUrl: mediaUrl ?? undefined,
|
||||||
|
replyTo: replyTo ?? undefined,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "search") {
|
||||||
|
if (provider !== "discord") {
|
||||||
|
throw new Error(
|
||||||
|
`Search is only supported for Discord (provider=${provider}).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const guildId = readStringParam(params, "guildId", { required: true });
|
||||||
|
const query = readStringParam(params, "query", { required: true });
|
||||||
|
const channelId = readStringParam(params, "channelId");
|
||||||
|
const channelIds = readStringArrayParam(params, "channelIds");
|
||||||
|
const authorId = readStringParam(params, "authorId");
|
||||||
|
const authorIds = readStringArrayParam(params, "authorIds");
|
||||||
|
const limit = readNumberParam(params, "limit", { integer: true });
|
||||||
|
return await handleDiscordAction(
|
||||||
|
{
|
||||||
|
action: "searchMessages",
|
||||||
|
guildId,
|
||||||
|
content: query,
|
||||||
|
channelId,
|
||||||
|
channelIds,
|
||||||
|
authorId,
|
||||||
|
authorIds,
|
||||||
|
limit,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "sticker") {
|
||||||
|
if (provider !== "discord") {
|
||||||
|
throw new Error(
|
||||||
|
`Sticker send is only supported for Discord (provider=${provider}).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const stickerIds =
|
||||||
|
readStringArrayParam(params, "stickerId", {
|
||||||
|
required: true,
|
||||||
|
label: "sticker-id",
|
||||||
|
}) ?? [];
|
||||||
|
const content = readStringParam(params, "message");
|
||||||
|
return await handleDiscordAction(
|
||||||
|
{
|
||||||
|
action: "sticker",
|
||||||
|
to: readStringParam(params, "to", { required: true }),
|
||||||
|
stickerIds,
|
||||||
|
content,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "member-info") {
|
||||||
|
const userId = readStringParam(params, "userId", { required: true });
|
||||||
|
if (provider === "discord") {
|
||||||
|
const guildId = readStringParam(params, "guildId", {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
return await handleDiscordAction(
|
||||||
|
{ action: "memberInfo", guildId, userId },
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (provider === "slack") {
|
||||||
|
return await handleSlackAction(
|
||||||
|
{ action: "memberInfo", userId, accountId: accountId ?? undefined },
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`Member info is not supported for provider ${provider}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "role-info") {
|
||||||
|
if (provider !== "discord") {
|
||||||
|
throw new Error(
|
||||||
|
`Role info is only supported for Discord (provider=${provider}).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const guildId = readStringParam(params, "guildId", { required: true });
|
||||||
|
return await handleDiscordAction({ action: "roleInfo", guildId }, cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "emoji-list") {
|
||||||
|
if (provider === "discord") {
|
||||||
|
const guildId = readStringParam(params, "guildId", {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
return await handleDiscordAction(
|
||||||
|
{ action: "emojiList", guildId },
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (provider === "slack") {
|
||||||
|
return await handleSlackAction(
|
||||||
|
{ action: "emojiList", accountId: accountId ?? undefined },
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`Emoji list is not supported for provider ${provider}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "emoji-upload") {
|
||||||
|
if (provider !== "discord") {
|
||||||
|
throw new Error(
|
||||||
|
`Emoji upload is only supported for Discord (provider=${provider}).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const guildId = readStringParam(params, "guildId", { required: true });
|
||||||
|
const name = readStringParam(params, "emojiName", { required: true });
|
||||||
|
const mediaUrl = readStringParam(params, "media", {
|
||||||
|
required: true,
|
||||||
|
trim: false,
|
||||||
|
});
|
||||||
|
const roleIds = readStringArrayParam(params, "roleIds");
|
||||||
|
return await handleDiscordAction(
|
||||||
|
{
|
||||||
|
action: "emojiUpload",
|
||||||
|
guildId,
|
||||||
|
name,
|
||||||
|
mediaUrl,
|
||||||
|
roleIds,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "sticker-upload") {
|
||||||
|
if (provider !== "discord") {
|
||||||
|
throw new Error(
|
||||||
|
`Sticker upload is only supported for Discord (provider=${provider}).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const guildId = readStringParam(params, "guildId", { required: true });
|
||||||
|
const name = readStringParam(params, "stickerName", { required: true });
|
||||||
|
const description = readStringParam(params, "stickerDesc", {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const tags = readStringParam(params, "stickerTags", { required: true });
|
||||||
|
const mediaUrl = readStringParam(params, "media", {
|
||||||
|
required: true,
|
||||||
|
trim: false,
|
||||||
|
});
|
||||||
|
return await handleDiscordAction(
|
||||||
|
{
|
||||||
|
action: "stickerUpload",
|
||||||
|
guildId,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
tags,
|
||||||
|
mediaUrl,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "role-add" || action === "role-remove") {
|
||||||
|
if (provider !== "discord") {
|
||||||
|
throw new Error(
|
||||||
|
`Role changes are only supported for Discord (provider=${provider}).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const guildId = readStringParam(params, "guildId", { required: true });
|
||||||
|
const userId = readStringParam(params, "userId", { required: true });
|
||||||
|
const roleId = readStringParam(params, "roleId", { required: true });
|
||||||
|
const discordAction = action === "role-add" ? "roleAdd" : "roleRemove";
|
||||||
|
return await handleDiscordAction(
|
||||||
|
{ action: discordAction, guildId, userId, roleId },
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "channel-info") {
|
||||||
|
if (provider !== "discord") {
|
||||||
|
throw new Error(
|
||||||
|
`Channel info is only supported for Discord (provider=${provider}).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const channelId = readStringParam(params, "channelId", {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
return await handleDiscordAction(
|
||||||
|
{ action: "channelInfo", channelId },
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "channel-list") {
|
||||||
|
if (provider !== "discord") {
|
||||||
|
throw new Error(
|
||||||
|
`Channel list is only supported for Discord (provider=${provider}).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const guildId = readStringParam(params, "guildId", { required: true });
|
||||||
|
return await handleDiscordAction(
|
||||||
|
{ action: "channelList", guildId },
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "voice-status") {
|
||||||
|
if (provider !== "discord") {
|
||||||
|
throw new Error(
|
||||||
|
`Voice status is only supported for Discord (provider=${provider}).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const guildId = readStringParam(params, "guildId", { required: true });
|
||||||
|
const userId = readStringParam(params, "userId", { required: true });
|
||||||
|
return await handleDiscordAction(
|
||||||
|
{ action: "voiceStatus", guildId, userId },
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "event-list") {
|
||||||
|
if (provider !== "discord") {
|
||||||
|
throw new Error(
|
||||||
|
`Event list is only supported for Discord (provider=${provider}).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const guildId = readStringParam(params, "guildId", { required: true });
|
||||||
|
return await handleDiscordAction({ action: "eventList", guildId }, cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "event-create") {
|
||||||
|
if (provider !== "discord") {
|
||||||
|
throw new Error(
|
||||||
|
`Event create is only supported for Discord (provider=${provider}).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const guildId = readStringParam(params, "guildId", { required: true });
|
||||||
|
const name = readStringParam(params, "eventName", { required: true });
|
||||||
|
const startTime = readStringParam(params, "startTime", {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const endTime = readStringParam(params, "endTime");
|
||||||
|
const description = readStringParam(params, "desc");
|
||||||
|
const channelId = readStringParam(params, "channelId");
|
||||||
|
const location = readStringParam(params, "location");
|
||||||
|
const entityType = readStringParam(params, "eventType");
|
||||||
|
return await handleDiscordAction(
|
||||||
|
{
|
||||||
|
action: "eventCreate",
|
||||||
|
guildId,
|
||||||
|
name,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
description,
|
||||||
|
channelId,
|
||||||
|
location,
|
||||||
|
entityType,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "timeout" || action === "kick" || action === "ban") {
|
||||||
|
if (provider !== "discord") {
|
||||||
|
throw new Error(
|
||||||
|
`Moderation actions are only supported for Discord (provider=${provider}).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const guildId = readStringParam(params, "guildId", { required: true });
|
||||||
|
const userId = readStringParam(params, "userId", { required: true });
|
||||||
|
const durationMinutes = readNumberParam(params, "durationMin", {
|
||||||
|
integer: true,
|
||||||
|
});
|
||||||
|
const until = readStringParam(params, "until");
|
||||||
|
const reason = readStringParam(params, "reason");
|
||||||
|
const deleteMessageDays = readNumberParam(params, "deleteDays", {
|
||||||
|
integer: true,
|
||||||
|
});
|
||||||
|
const discordAction = action as "timeout" | "kick" | "ban";
|
||||||
|
return await handleDiscordAction(
|
||||||
|
{
|
||||||
|
action: discordAction,
|
||||||
|
guildId,
|
||||||
|
userId,
|
||||||
|
durationMinutes,
|
||||||
|
until,
|
||||||
|
reason,
|
||||||
|
deleteMessageDays,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error(`Unknown action: ${action}`);
|
throw new Error(`Unknown action: ${action}`);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -91,9 +91,11 @@ export async function handleSlackAction(
|
|||||||
const to = readStringParam(params, "to", { required: true });
|
const to = readStringParam(params, "to", { required: true });
|
||||||
const content = readStringParam(params, "content", { required: true });
|
const content = readStringParam(params, "content", { required: true });
|
||||||
const mediaUrl = readStringParam(params, "mediaUrl");
|
const mediaUrl = readStringParam(params, "mediaUrl");
|
||||||
|
const threadTs = readStringParam(params, "threadTs");
|
||||||
const result = await sendSlackMessage(to, content, {
|
const result = await sendSlackMessage(to, content, {
|
||||||
accountId: accountId ?? undefined,
|
accountId: accountId ?? undefined,
|
||||||
mediaUrl: mediaUrl ?? undefined,
|
mediaUrl: mediaUrl ?? undefined,
|
||||||
|
threadTs: threadTs ?? undefined,
|
||||||
});
|
});
|
||||||
return jsonResult({ ok: true, result });
|
return jsonResult({ ok: true, result });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const SlackToolSchema = Type.Union([
|
|||||||
to: Type.String(),
|
to: Type.String(),
|
||||||
content: Type.String(),
|
content: Type.String(),
|
||||||
mediaUrl: Type.Optional(Type.String()),
|
mediaUrl: Type.Optional(Type.String()),
|
||||||
|
threadTs: Type.Optional(Type.String()),
|
||||||
accountId: Type.Optional(Type.String()),
|
accountId: Type.Optional(Type.String()),
|
||||||
}),
|
}),
|
||||||
Type.Object({
|
Type.Object({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as fs from "node:fs/promises";
|
import * as fs from "node:fs/promises";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
const messageSendCommand = vi.fn();
|
const messageCommand = vi.fn();
|
||||||
const statusCommand = vi.fn();
|
const statusCommand = vi.fn();
|
||||||
const configureCommand = vi.fn();
|
const configureCommand = vi.fn();
|
||||||
const setupCommand = vi.fn();
|
const setupCommand = vi.fn();
|
||||||
@@ -19,8 +19,7 @@ const runtime = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
vi.mock("../commands/message.js", () => ({
|
vi.mock("../commands/message.js", () => ({
|
||||||
messageSendCommand,
|
messageCommand,
|
||||||
messagePollCommand: vi.fn(),
|
|
||||||
}));
|
}));
|
||||||
vi.mock("../commands/status.js", () => ({ statusCommand }));
|
vi.mock("../commands/status.js", () => ({ statusCommand }));
|
||||||
vi.mock("../commands/configure.js", () => ({ configureCommand }));
|
vi.mock("../commands/configure.js", () => ({ configureCommand }));
|
||||||
@@ -46,15 +45,12 @@ describe("cli program", () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("runs message send with required options", async () => {
|
it("runs message with required options", async () => {
|
||||||
const program = buildProgram();
|
const program = buildProgram();
|
||||||
await program.parseAsync(
|
await program.parseAsync(["message", "--to", "+1", "--message", "hi"], {
|
||||||
["message", "send", "--to", "+1", "--message", "hi"],
|
from: "user",
|
||||||
{
|
});
|
||||||
from: "user",
|
expect(messageCommand).toHaveBeenCalled();
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(messageSendCommand).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("runs status command", async () => {
|
it("runs status command", async () => {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
import { configureCommand } from "../commands/configure.js";
|
import { configureCommand } from "../commands/configure.js";
|
||||||
import { doctorCommand } from "../commands/doctor.js";
|
import { doctorCommand } from "../commands/doctor.js";
|
||||||
import { healthCommand } from "../commands/health.js";
|
import { healthCommand } from "../commands/health.js";
|
||||||
import { messagePollCommand, messageSendCommand } from "../commands/message.js";
|
import { messageCommand } from "../commands/message.js";
|
||||||
import { onboardCommand } from "../commands/onboard.js";
|
import { onboardCommand } from "../commands/onboard.js";
|
||||||
import { sessionsCommand } from "../commands/sessions.js";
|
import { sessionsCommand } from "../commands/sessions.js";
|
||||||
import { setupCommand } from "../commands/setup.js";
|
import { setupCommand } from "../commands/setup.js";
|
||||||
@@ -408,41 +408,100 @@ export function buildProgram() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const message = program
|
program
|
||||||
.command("message")
|
.command("message")
|
||||||
.description("Send messages and polls across providers")
|
.description("Send messages and provider actions")
|
||||||
.action(() => {
|
.option(
|
||||||
message.outputHelp();
|
"-a, --action <action>",
|
||||||
defaultRuntime.error(
|
"Action: send|poll|react|reactions|read|edit|delete|pin|unpin|list-pins|permissions|thread-create|thread-list|thread-reply|search|sticker|member-info|role-info|emoji-list|emoji-upload|sticker-upload|role-add|role-remove|channel-info|channel-list|voice-status|event-list|event-create|timeout|kick|ban",
|
||||||
danger('Missing subcommand. Try: "clawdbot message send"'),
|
"send",
|
||||||
);
|
|
||||||
defaultRuntime.exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
message
|
|
||||||
.command("send")
|
|
||||||
.description(
|
|
||||||
"Send a message (WhatsApp Web, Telegram bot, Discord, Slack, Signal, iMessage)",
|
|
||||||
)
|
)
|
||||||
.requiredOption(
|
.option(
|
||||||
"-t, --to <number>",
|
"-t, --to <dest>",
|
||||||
"Recipient: E.164 for WhatsApp/Signal, Telegram chat id/@username, Discord channel/user, or iMessage handle/chat_id",
|
"Recipient/channel: E.164 for WhatsApp/Signal, Telegram chat id/@username, Discord/Slack channel/user, or iMessage handle/chat_id",
|
||||||
)
|
)
|
||||||
.requiredOption("-m, --message <text>", "Message body")
|
.option("-m, --message <text>", "Message body")
|
||||||
.option(
|
.option(
|
||||||
"--media <path-or-url>",
|
"--media <path-or-url>",
|
||||||
"Attach media (image/audio/video/document). Accepts local paths or URLs.",
|
"Attach media (image/audio/video/document). Accepts local paths or URLs.",
|
||||||
)
|
)
|
||||||
|
.option("--message-id <id>", "Message id (edit/delete/react/pin)")
|
||||||
|
.option("--reply-to <id>", "Reply-to message id")
|
||||||
|
.option("--thread-id <id>", "Thread id (Telegram forum thread)")
|
||||||
|
.option("--account <id>", "Provider account id")
|
||||||
|
.option(
|
||||||
|
"--provider <provider>",
|
||||||
|
"Provider: whatsapp|telegram|discord|slack|signal|imessage",
|
||||||
|
)
|
||||||
|
.option("--emoji <emoji>", "Emoji for reactions")
|
||||||
|
.option("--remove", "Remove reaction", false)
|
||||||
|
.option("--limit <n>", "Result limit for read/reactions/search")
|
||||||
|
.option("--before <id>", "Read/search before id")
|
||||||
|
.option("--after <id>", "Read/search after id")
|
||||||
|
.option("--around <id>", "Read around id (Discord)")
|
||||||
|
.option("--poll-question <text>", "Poll question")
|
||||||
|
.option(
|
||||||
|
"--poll-option <choice>",
|
||||||
|
"Poll option (repeat 2-12 times)",
|
||||||
|
(value: string, previous: string[]) => previous.concat([value]),
|
||||||
|
[] as string[],
|
||||||
|
)
|
||||||
|
.option("--poll-multi", "Allow multiple selections", false)
|
||||||
|
.option("--poll-duration-hours <n>", "Poll duration (Discord)")
|
||||||
|
.option("--channel-id <id>", "Channel id")
|
||||||
|
.option(
|
||||||
|
"--channel-ids <id>",
|
||||||
|
"Channel id (repeat)",
|
||||||
|
(value: string, previous: string[]) => previous.concat([value]),
|
||||||
|
[] as string[],
|
||||||
|
)
|
||||||
|
.option("--guild-id <id>", "Guild id")
|
||||||
|
.option("--user-id <id>", "User id")
|
||||||
|
.option("--author-id <id>", "Author id")
|
||||||
|
.option(
|
||||||
|
"--author-ids <id>",
|
||||||
|
"Author id (repeat)",
|
||||||
|
(value: string, previous: string[]) => previous.concat([value]),
|
||||||
|
[] as string[],
|
||||||
|
)
|
||||||
|
.option("--role-id <id>", "Role id")
|
||||||
|
.option(
|
||||||
|
"--role-ids <id>",
|
||||||
|
"Role id (repeat)",
|
||||||
|
(value: string, previous: string[]) => previous.concat([value]),
|
||||||
|
[] as string[],
|
||||||
|
)
|
||||||
|
.option("--emoji-name <name>", "Emoji name")
|
||||||
|
.option(
|
||||||
|
"--sticker-id <id>",
|
||||||
|
"Sticker id (repeat)",
|
||||||
|
(value: string, previous: string[]) => previous.concat([value]),
|
||||||
|
[] as string[],
|
||||||
|
)
|
||||||
|
.option("--sticker-name <name>", "Sticker name")
|
||||||
|
.option("--sticker-desc <text>", "Sticker description")
|
||||||
|
.option("--sticker-tags <tags>", "Sticker tags")
|
||||||
|
.option("--thread-name <name>", "Thread name")
|
||||||
|
.option("--auto-archive-min <n>", "Thread auto-archive minutes")
|
||||||
|
.option("--query <text>", "Search query")
|
||||||
|
.option("--event-name <name>", "Event name")
|
||||||
|
.option("--event-type <stage|external|voice>", "Event type")
|
||||||
|
.option("--start-time <iso>", "Event start time")
|
||||||
|
.option("--end-time <iso>", "Event end time")
|
||||||
|
.option("--desc <text>", "Event description")
|
||||||
|
.option("--location <text>", "Event location")
|
||||||
|
.option("--duration-min <n>", "Timeout duration minutes")
|
||||||
|
.option("--until <iso>", "Timeout until")
|
||||||
|
.option("--reason <text>", "Moderation reason")
|
||||||
|
.option("--delete-days <n>", "Ban delete message days")
|
||||||
|
.option("--include-archived", "Include archived threads", false)
|
||||||
|
.option("--participant <id>", "WhatsApp reaction participant")
|
||||||
|
.option("--from-me", "WhatsApp reaction fromMe", false)
|
||||||
.option(
|
.option(
|
||||||
"--gif-playback",
|
"--gif-playback",
|
||||||
"Treat video media as GIF playback (WhatsApp only).",
|
"Treat video media as GIF playback (WhatsApp only).",
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
.option(
|
|
||||||
"--provider <provider>",
|
|
||||||
"Delivery provider: whatsapp|telegram|discord|slack|signal|imessage (default: whatsapp)",
|
|
||||||
)
|
|
||||||
.option("--account <id>", "WhatsApp account id (accountId)")
|
|
||||||
.option("--dry-run", "Print payload and skip sending", false)
|
.option("--dry-run", "Print payload and skip sending", false)
|
||||||
.option("--json", "Output result as JSON", false)
|
.option("--json", "Output result as JSON", false)
|
||||||
.option("--verbose", "Verbose logging", false)
|
.option("--verbose", "Verbose logging", false)
|
||||||
@@ -450,16 +509,16 @@ export function buildProgram() {
|
|||||||
"after",
|
"after",
|
||||||
`
|
`
|
||||||
Examples:
|
Examples:
|
||||||
clawdbot message send --to +15555550123 --message "Hi"
|
clawdbot message --to +15555550123 --message "Hi"
|
||||||
clawdbot message send --to +15555550123 --message "Hi" --media photo.jpg
|
clawdbot message --action send --to +15555550123 --message "Hi" --media photo.jpg
|
||||||
clawdbot message send --to +15555550123 --message "Hi" --dry-run # print payload only
|
clawdbot message --action poll --provider discord --to channel:123 --poll-question "Snack?" --poll-option Pizza --poll-option Sushi
|
||||||
clawdbot message send --to +15555550123 --message "Hi" --json # machine-readable result`,
|
clawdbot message --action react --provider discord --to 123 --message-id 456 --emoji "✅"`,
|
||||||
)
|
)
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
setVerbose(Boolean(opts.verbose));
|
setVerbose(Boolean(opts.verbose));
|
||||||
const deps = createDefaultDeps();
|
const deps = createDefaultDeps();
|
||||||
try {
|
try {
|
||||||
await messageSendCommand(
|
await messageCommand(
|
||||||
{
|
{
|
||||||
...opts,
|
...opts,
|
||||||
account: opts.account as string | undefined,
|
account: opts.account as string | undefined,
|
||||||
@@ -473,55 +532,6 @@ Examples:
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
message
|
|
||||||
.command("poll")
|
|
||||||
.description("Create a poll via WhatsApp or Discord")
|
|
||||||
.requiredOption(
|
|
||||||
"-t, --to <id>",
|
|
||||||
"Recipient: WhatsApp JID/number or Discord channel/user",
|
|
||||||
)
|
|
||||||
.requiredOption("-q, --question <text>", "Poll question")
|
|
||||||
.requiredOption(
|
|
||||||
"-o, --option <choice>",
|
|
||||||
"Poll option (use multiple times, 2-12 required)",
|
|
||||||
(value: string, previous: string[]) => previous.concat([value]),
|
|
||||||
[] as string[],
|
|
||||||
)
|
|
||||||
.option(
|
|
||||||
"-s, --max-selections <n>",
|
|
||||||
"How many options can be selected (default: 1)",
|
|
||||||
)
|
|
||||||
.option(
|
|
||||||
"--duration-hours <n>",
|
|
||||||
"Poll duration in hours (Discord only, default: 24)",
|
|
||||||
)
|
|
||||||
.option(
|
|
||||||
"--provider <provider>",
|
|
||||||
"Delivery provider: whatsapp|discord (default: whatsapp)",
|
|
||||||
)
|
|
||||||
.option("--dry-run", "Print payload and skip sending", false)
|
|
||||||
.option("--json", "Output result as JSON", false)
|
|
||||||
.option("--verbose", "Verbose logging", false)
|
|
||||||
.addHelpText(
|
|
||||||
"after",
|
|
||||||
`
|
|
||||||
Examples:
|
|
||||||
clawdbot message poll --to +15555550123 -q "Lunch today?" -o "Yes" -o "No" -o "Maybe"
|
|
||||||
clawdbot message poll --to 123456789@g.us -q "Meeting time?" -o "10am" -o "2pm" -o "4pm" -s 2
|
|
||||||
clawdbot message poll --to channel:123456789 -q "Snack?" -o "Pizza" -o "Sushi" --provider discord
|
|
||||||
clawdbot message poll --to channel:123456789 -q "Plan?" -o "A" -o "B" --provider discord --duration-hours 48`,
|
|
||||||
)
|
|
||||||
.action(async (opts) => {
|
|
||||||
setVerbose(Boolean(opts.verbose));
|
|
||||||
const deps = createDefaultDeps();
|
|
||||||
try {
|
|
||||||
await messagePollCommand(opts, deps, defaultRuntime);
|
|
||||||
} catch (err) {
|
|
||||||
defaultRuntime.error(String(err));
|
|
||||||
defaultRuntime.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
program
|
program
|
||||||
.command("agent")
|
.command("agent")
|
||||||
.description("Run an agent turn via the Gateway (use --local for embedded)")
|
.description("Run an agent turn via the Gateway (use --local for embedded)")
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
|
|
||||||
import type { CliDeps } from "../cli/deps.js";
|
import type { CliDeps } from "../cli/deps.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import { messagePollCommand, messageSendCommand } from "./message.js";
|
import { messageCommand } from "./message.js";
|
||||||
|
|
||||||
let testConfig: Record<string, unknown> = {};
|
let testConfig: Record<string, unknown> = {};
|
||||||
vi.mock("../config/config.js", async (importOriginal) => {
|
vi.mock("../config/config.js", async (importOriginal) => {
|
||||||
@@ -19,13 +19,44 @@ vi.mock("../gateway/call.js", () => ({
|
|||||||
randomIdempotencyKey: () => "idem-1",
|
randomIdempotencyKey: () => "idem-1",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const webAuthExists = vi.fn(async () => false);
|
||||||
|
vi.mock("../web/session.js", () => ({
|
||||||
|
webAuthExists: (...args: unknown[]) => webAuthExists(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const handleDiscordAction = vi.fn(async () => ({ details: { ok: true } }));
|
||||||
|
vi.mock("../agents/tools/discord-actions.js", () => ({
|
||||||
|
handleDiscordAction: (...args: unknown[]) => handleDiscordAction(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const handleSlackAction = vi.fn(async () => ({ details: { ok: true } }));
|
||||||
|
vi.mock("../agents/tools/slack-actions.js", () => ({
|
||||||
|
handleSlackAction: (...args: unknown[]) => handleSlackAction(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const handleTelegramAction = vi.fn(async () => ({ details: { ok: true } }));
|
||||||
|
vi.mock("../agents/tools/telegram-actions.js", () => ({
|
||||||
|
handleTelegramAction: (...args: unknown[]) => handleTelegramAction(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const handleWhatsAppAction = vi.fn(async () => ({ details: { ok: true } }));
|
||||||
|
vi.mock("../agents/tools/whatsapp-actions.js", () => ({
|
||||||
|
handleWhatsAppAction: (...args: unknown[]) => handleWhatsAppAction(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
const originalTelegramToken = process.env.TELEGRAM_BOT_TOKEN;
|
const originalTelegramToken = process.env.TELEGRAM_BOT_TOKEN;
|
||||||
const originalDiscordToken = process.env.DISCORD_BOT_TOKEN;
|
const originalDiscordToken = process.env.DISCORD_BOT_TOKEN;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
process.env.TELEGRAM_BOT_TOKEN = "token-abc";
|
process.env.TELEGRAM_BOT_TOKEN = "";
|
||||||
process.env.DISCORD_BOT_TOKEN = "token-discord";
|
process.env.DISCORD_BOT_TOKEN = "";
|
||||||
testConfig = {};
|
testConfig = {};
|
||||||
|
callGatewayMock.mockReset();
|
||||||
|
webAuthExists.mockReset().mockResolvedValue(false);
|
||||||
|
handleDiscordAction.mockReset();
|
||||||
|
handleSlackAction.mockReset();
|
||||||
|
handleTelegramAction.mockReset();
|
||||||
|
handleWhatsAppAction.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
@@ -51,26 +82,44 @@ const makeDeps = (overrides: Partial<CliDeps> = {}): CliDeps => ({
|
|||||||
...overrides,
|
...overrides,
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("messageSendCommand", () => {
|
describe("messageCommand", () => {
|
||||||
it("skips send on dry-run", async () => {
|
it("defaults provider when only one configured", async () => {
|
||||||
|
process.env.TELEGRAM_BOT_TOKEN = "token-abc";
|
||||||
const deps = makeDeps();
|
const deps = makeDeps();
|
||||||
await messageSendCommand(
|
await messageCommand(
|
||||||
{
|
{
|
||||||
to: "+1",
|
to: "123",
|
||||||
message: "hi",
|
message: "hi",
|
||||||
dryRun: true,
|
|
||||||
},
|
},
|
||||||
deps,
|
deps,
|
||||||
runtime,
|
runtime,
|
||||||
);
|
);
|
||||||
expect(deps.sendMessageWhatsApp).not.toHaveBeenCalled();
|
expect(handleTelegramAction).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sends via gateway", async () => {
|
it("requires provider when multiple configured", async () => {
|
||||||
|
process.env.TELEGRAM_BOT_TOKEN = "token-abc";
|
||||||
|
process.env.DISCORD_BOT_TOKEN = "token-discord";
|
||||||
|
const deps = makeDeps();
|
||||||
|
await expect(
|
||||||
|
messageCommand(
|
||||||
|
{
|
||||||
|
to: "123",
|
||||||
|
message: "hi",
|
||||||
|
},
|
||||||
|
deps,
|
||||||
|
runtime,
|
||||||
|
),
|
||||||
|
).rejects.toThrow(/Provider is required/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends via gateway for WhatsApp", async () => {
|
||||||
callGatewayMock.mockResolvedValueOnce({ messageId: "g1" });
|
callGatewayMock.mockResolvedValueOnce({ messageId: "g1" });
|
||||||
const deps = makeDeps();
|
const deps = makeDeps();
|
||||||
await messageSendCommand(
|
await messageCommand(
|
||||||
{
|
{
|
||||||
|
action: "send",
|
||||||
|
provider: "whatsapp",
|
||||||
to: "+1",
|
to: "+1",
|
||||||
message: "hi",
|
message: "hi",
|
||||||
},
|
},
|
||||||
@@ -78,261 +127,27 @@ describe("messageSendCommand", () => {
|
|||||||
runtime,
|
runtime,
|
||||||
);
|
);
|
||||||
expect(callGatewayMock).toHaveBeenCalled();
|
expect(callGatewayMock).toHaveBeenCalled();
|
||||||
expect(runtime.log).toHaveBeenCalledWith(expect.stringContaining("g1"));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not override remote gateway URL", async () => {
|
it("routes discord polls through message action", async () => {
|
||||||
callGatewayMock.mockResolvedValueOnce({ messageId: "g2" });
|
|
||||||
testConfig = {
|
|
||||||
gateway: { mode: "remote", remote: { url: "wss://remote.example" } },
|
|
||||||
};
|
|
||||||
const deps = makeDeps();
|
const deps = makeDeps();
|
||||||
await messageSendCommand(
|
await messageCommand(
|
||||||
{
|
{
|
||||||
to: "+1",
|
action: "poll",
|
||||||
message: "hi",
|
|
||||||
},
|
|
||||||
deps,
|
|
||||||
runtime,
|
|
||||||
);
|
|
||||||
const args = callGatewayMock.mock.calls.at(-1)?.[0] as
|
|
||||||
| Record<string, unknown>
|
|
||||||
| undefined;
|
|
||||||
expect(args?.url).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("passes gifPlayback to gateway send", async () => {
|
|
||||||
callGatewayMock.mockClear();
|
|
||||||
callGatewayMock.mockResolvedValueOnce({ messageId: "g1" });
|
|
||||||
const deps = makeDeps();
|
|
||||||
await messageSendCommand(
|
|
||||||
{
|
|
||||||
to: "+1",
|
|
||||||
message: "hi",
|
|
||||||
gifPlayback: true,
|
|
||||||
},
|
|
||||||
deps,
|
|
||||||
runtime,
|
|
||||||
);
|
|
||||||
expect(callGatewayMock).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
method: "send",
|
|
||||||
params: expect.objectContaining({ gifPlayback: true }),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("routes to telegram provider", async () => {
|
|
||||||
const deps = makeDeps({
|
|
||||||
sendMessageTelegram: vi
|
|
||||||
.fn()
|
|
||||||
.mockResolvedValue({ messageId: "t1", chatId: "123" }),
|
|
||||||
});
|
|
||||||
testConfig = { telegram: { botToken: "token-abc" } };
|
|
||||||
await messageSendCommand(
|
|
||||||
{ to: "123", message: "hi", provider: "telegram" },
|
|
||||||
deps,
|
|
||||||
runtime,
|
|
||||||
);
|
|
||||||
expect(deps.sendMessageTelegram).toHaveBeenCalledWith(
|
|
||||||
"123",
|
|
||||||
"hi",
|
|
||||||
expect.objectContaining({ accountId: undefined, verbose: false }),
|
|
||||||
);
|
|
||||||
expect(deps.sendMessageWhatsApp).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("uses config token for telegram when env is missing", async () => {
|
|
||||||
process.env.TELEGRAM_BOT_TOKEN = "";
|
|
||||||
testConfig = { telegram: { botToken: "cfg-token" } };
|
|
||||||
const deps = makeDeps({
|
|
||||||
sendMessageTelegram: vi
|
|
||||||
.fn()
|
|
||||||
.mockResolvedValue({ messageId: "t1", chatId: "123" }),
|
|
||||||
});
|
|
||||||
await messageSendCommand(
|
|
||||||
{ to: "123", message: "hi", provider: "telegram" },
|
|
||||||
deps,
|
|
||||||
runtime,
|
|
||||||
);
|
|
||||||
expect(deps.sendMessageTelegram).toHaveBeenCalledWith(
|
|
||||||
"123",
|
|
||||||
"hi",
|
|
||||||
expect.objectContaining({ accountId: undefined, verbose: false }),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("routes to discord provider", async () => {
|
|
||||||
const deps = makeDeps({
|
|
||||||
sendMessageDiscord: vi
|
|
||||||
.fn()
|
|
||||||
.mockResolvedValue({ messageId: "d1", channelId: "chan" }),
|
|
||||||
});
|
|
||||||
await messageSendCommand(
|
|
||||||
{ to: "channel:chan", message: "hi", provider: "discord" },
|
|
||||||
deps,
|
|
||||||
runtime,
|
|
||||||
);
|
|
||||||
expect(deps.sendMessageDiscord).toHaveBeenCalledWith(
|
|
||||||
"channel:chan",
|
|
||||||
"hi",
|
|
||||||
expect.objectContaining({ verbose: false }),
|
|
||||||
);
|
|
||||||
expect(deps.sendMessageWhatsApp).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("routes to signal provider", async () => {
|
|
||||||
const deps = makeDeps({
|
|
||||||
sendMessageSignal: vi.fn().mockResolvedValue({ messageId: "s1" }),
|
|
||||||
});
|
|
||||||
await messageSendCommand(
|
|
||||||
{ to: "+15551234567", message: "hi", provider: "signal" },
|
|
||||||
deps,
|
|
||||||
runtime,
|
|
||||||
);
|
|
||||||
expect(deps.sendMessageSignal).toHaveBeenCalledWith(
|
|
||||||
"+15551234567",
|
|
||||||
"hi",
|
|
||||||
expect.objectContaining({ maxBytes: undefined }),
|
|
||||||
);
|
|
||||||
expect(deps.sendMessageWhatsApp).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("routes to slack provider", async () => {
|
|
||||||
const deps = makeDeps({
|
|
||||||
sendMessageSlack: vi
|
|
||||||
.fn()
|
|
||||||
.mockResolvedValue({ messageId: "s1", channelId: "C123" }),
|
|
||||||
});
|
|
||||||
await messageSendCommand(
|
|
||||||
{ to: "channel:C123", message: "hi", provider: "slack" },
|
|
||||||
deps,
|
|
||||||
runtime,
|
|
||||||
);
|
|
||||||
expect(deps.sendMessageSlack).toHaveBeenCalledWith(
|
|
||||||
"channel:C123",
|
|
||||||
"hi",
|
|
||||||
expect.objectContaining({ accountId: undefined }),
|
|
||||||
);
|
|
||||||
expect(deps.sendMessageWhatsApp).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("routes to imessage provider", async () => {
|
|
||||||
const deps = makeDeps({
|
|
||||||
sendMessageIMessage: vi.fn().mockResolvedValue({ messageId: "i1" }),
|
|
||||||
});
|
|
||||||
await messageSendCommand(
|
|
||||||
{ to: "chat_id:42", message: "hi", provider: "imessage" },
|
|
||||||
deps,
|
|
||||||
runtime,
|
|
||||||
);
|
|
||||||
expect(deps.sendMessageIMessage).toHaveBeenCalledWith(
|
|
||||||
"chat_id:42",
|
|
||||||
"hi",
|
|
||||||
expect.objectContaining({ maxBytes: undefined }),
|
|
||||||
);
|
|
||||||
expect(deps.sendMessageWhatsApp).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("emits json output", async () => {
|
|
||||||
callGatewayMock.mockResolvedValueOnce({ messageId: "direct2" });
|
|
||||||
const deps = makeDeps();
|
|
||||||
await messageSendCommand(
|
|
||||||
{
|
|
||||||
to: "+1",
|
|
||||||
message: "hi",
|
|
||||||
json: true,
|
|
||||||
},
|
|
||||||
deps,
|
|
||||||
runtime,
|
|
||||||
);
|
|
||||||
expect(runtime.log).toHaveBeenCalledWith(
|
|
||||||
expect.stringContaining('"provider": "whatsapp"'),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("messagePollCommand", () => {
|
|
||||||
const deps: CliDeps = {
|
|
||||||
sendMessageWhatsApp: vi.fn(),
|
|
||||||
sendMessageTelegram: vi.fn(),
|
|
||||||
sendMessageDiscord: vi.fn(),
|
|
||||||
sendMessageSlack: vi.fn(),
|
|
||||||
sendMessageSignal: vi.fn(),
|
|
||||||
sendMessageIMessage: vi.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
callGatewayMock.mockReset();
|
|
||||||
runtime.log.mockReset();
|
|
||||||
runtime.error.mockReset();
|
|
||||||
runtime.exit.mockReset();
|
|
||||||
testConfig = {};
|
|
||||||
});
|
|
||||||
|
|
||||||
it("routes through gateway", async () => {
|
|
||||||
callGatewayMock.mockResolvedValueOnce({ messageId: "p1" });
|
|
||||||
await messagePollCommand(
|
|
||||||
{
|
|
||||||
to: "+1",
|
|
||||||
question: "hi?",
|
|
||||||
option: ["y", "n"],
|
|
||||||
},
|
|
||||||
deps,
|
|
||||||
runtime,
|
|
||||||
);
|
|
||||||
expect(callGatewayMock).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({ method: "poll" }),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not override remote gateway URL", async () => {
|
|
||||||
callGatewayMock.mockResolvedValueOnce({ messageId: "p1" });
|
|
||||||
testConfig = {
|
|
||||||
gateway: { mode: "remote", remote: { url: "wss://remote.example" } },
|
|
||||||
};
|
|
||||||
await messagePollCommand(
|
|
||||||
{
|
|
||||||
to: "+1",
|
|
||||||
question: "hi?",
|
|
||||||
option: ["y", "n"],
|
|
||||||
},
|
|
||||||
deps,
|
|
||||||
runtime,
|
|
||||||
);
|
|
||||||
const args = callGatewayMock.mock.calls.at(-1)?.[0] as
|
|
||||||
| Record<string, unknown>
|
|
||||||
| undefined;
|
|
||||||
expect(args?.url).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("emits json output with gateway metadata", async () => {
|
|
||||||
callGatewayMock.mockResolvedValueOnce({ messageId: "p1", channelId: "C1" });
|
|
||||||
await messagePollCommand(
|
|
||||||
{
|
|
||||||
to: "channel:C1",
|
|
||||||
question: "hi?",
|
|
||||||
option: ["y", "n"],
|
|
||||||
provider: "discord",
|
provider: "discord",
|
||||||
json: true,
|
to: "channel:123",
|
||||||
|
pollQuestion: "Snack?",
|
||||||
|
pollOption: ["Pizza", "Sushi"],
|
||||||
},
|
},
|
||||||
deps,
|
deps,
|
||||||
runtime,
|
runtime,
|
||||||
);
|
);
|
||||||
const lastLog = runtime.log.mock.calls.at(-1)?.[0] as string | undefined;
|
expect(handleDiscordAction).toHaveBeenCalledWith(
|
||||||
expect(lastLog).toBeDefined();
|
expect.objectContaining({
|
||||||
const payload = JSON.parse(lastLog ?? "{}") as Record<string, unknown>;
|
action: "poll",
|
||||||
expect(payload).toMatchObject({
|
to: "channel:123",
|
||||||
provider: "discord",
|
}),
|
||||||
via: "gateway",
|
expect.any(Object),
|
||||||
to: "channel:C1",
|
);
|
||||||
messageId: "p1",
|
|
||||||
channelId: "C1",
|
|
||||||
mediaUrl: null,
|
|
||||||
question: "hi?",
|
|
||||||
options: ["y", "n"],
|
|
||||||
maxSelections: 1,
|
|
||||||
durationHours: null,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
113
src/infra/outbound/provider-selection.ts
Normal file
113
src/infra/outbound/provider-selection.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
|
import { listEnabledDiscordAccounts } from "../../discord/accounts.js";
|
||||||
|
import { listEnabledIMessageAccounts } from "../../imessage/accounts.js";
|
||||||
|
import { listEnabledSignalAccounts } from "../../signal/accounts.js";
|
||||||
|
import { listEnabledSlackAccounts } from "../../slack/accounts.js";
|
||||||
|
import { listEnabledTelegramAccounts } from "../../telegram/accounts.js";
|
||||||
|
import { normalizeMessageProvider } from "../../utils/message-provider.js";
|
||||||
|
import {
|
||||||
|
listEnabledWhatsAppAccounts,
|
||||||
|
resolveWhatsAppAccount,
|
||||||
|
} from "../../web/accounts.js";
|
||||||
|
import { webAuthExists } from "../../web/session.js";
|
||||||
|
|
||||||
|
export type MessageProviderId =
|
||||||
|
| "whatsapp"
|
||||||
|
| "telegram"
|
||||||
|
| "discord"
|
||||||
|
| "slack"
|
||||||
|
| "signal"
|
||||||
|
| "imessage";
|
||||||
|
|
||||||
|
const MESSAGE_PROVIDERS: MessageProviderId[] = [
|
||||||
|
"whatsapp",
|
||||||
|
"telegram",
|
||||||
|
"discord",
|
||||||
|
"slack",
|
||||||
|
"signal",
|
||||||
|
"imessage",
|
||||||
|
];
|
||||||
|
|
||||||
|
function isKnownProvider(value: string): value is MessageProviderId {
|
||||||
|
return (MESSAGE_PROVIDERS as string[]).includes(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isWhatsAppConfigured(cfg: ClawdbotConfig): Promise<boolean> {
|
||||||
|
const accounts = listEnabledWhatsAppAccounts(cfg);
|
||||||
|
if (accounts.length === 0) {
|
||||||
|
const fallback = resolveWhatsAppAccount({ cfg });
|
||||||
|
return await webAuthExists(fallback.authDir);
|
||||||
|
}
|
||||||
|
for (const account of accounts) {
|
||||||
|
if (await webAuthExists(account.authDir)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTelegramConfigured(cfg: ClawdbotConfig): boolean {
|
||||||
|
return listEnabledTelegramAccounts(cfg).some(
|
||||||
|
(account) => account.token.trim().length > 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDiscordConfigured(cfg: ClawdbotConfig): boolean {
|
||||||
|
return listEnabledDiscordAccounts(cfg).some(
|
||||||
|
(account) => account.token.trim().length > 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSlackConfigured(cfg: ClawdbotConfig): boolean {
|
||||||
|
return listEnabledSlackAccounts(cfg).some(
|
||||||
|
(account) => (account.botToken ?? "").trim().length > 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSignalConfigured(cfg: ClawdbotConfig): boolean {
|
||||||
|
return listEnabledSignalAccounts(cfg).some((account) => account.configured);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isIMessageConfigured(cfg: ClawdbotConfig): boolean {
|
||||||
|
return listEnabledIMessageAccounts(cfg).some((account) => account.configured);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listConfiguredMessageProviders(
|
||||||
|
cfg: ClawdbotConfig,
|
||||||
|
): Promise<MessageProviderId[]> {
|
||||||
|
const providers: MessageProviderId[] = [];
|
||||||
|
if (await isWhatsAppConfigured(cfg)) providers.push("whatsapp");
|
||||||
|
if (isTelegramConfigured(cfg)) providers.push("telegram");
|
||||||
|
if (isDiscordConfigured(cfg)) providers.push("discord");
|
||||||
|
if (isSlackConfigured(cfg)) providers.push("slack");
|
||||||
|
if (isSignalConfigured(cfg)) providers.push("signal");
|
||||||
|
if (isIMessageConfigured(cfg)) providers.push("imessage");
|
||||||
|
return providers;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveMessageProviderSelection(params: {
|
||||||
|
cfg: ClawdbotConfig;
|
||||||
|
provider?: string | null;
|
||||||
|
}): Promise<{ provider: MessageProviderId; configured: MessageProviderId[] }> {
|
||||||
|
const normalized = normalizeMessageProvider(params.provider);
|
||||||
|
if (normalized) {
|
||||||
|
if (!isKnownProvider(normalized)) {
|
||||||
|
throw new Error(`Unknown provider: ${normalized}`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
provider: normalized,
|
||||||
|
configured: await listConfiguredMessageProviders(params.cfg),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const configured = await listConfiguredMessageProviders(params.cfg);
|
||||||
|
if (configured.length === 1) {
|
||||||
|
return { provider: configured[0], configured };
|
||||||
|
}
|
||||||
|
if (configured.length === 0) {
|
||||||
|
throw new Error("Provider is required (no configured providers detected).");
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`Provider is required when multiple providers are configured: ${configured.join(
|
||||||
|
", ",
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -145,13 +145,14 @@ export async function listSlackReactions(
|
|||||||
export async function sendSlackMessage(
|
export async function sendSlackMessage(
|
||||||
to: string,
|
to: string,
|
||||||
content: string,
|
content: string,
|
||||||
opts: SlackActionClientOpts & { mediaUrl?: string } = {},
|
opts: SlackActionClientOpts & { mediaUrl?: string; threadTs?: string } = {},
|
||||||
) {
|
) {
|
||||||
return await sendMessageSlack(to, content, {
|
return await sendMessageSlack(to, content, {
|
||||||
accountId: opts.accountId,
|
accountId: opts.accountId,
|
||||||
token: opts.token,
|
token: opts.token,
|
||||||
mediaUrl: opts.mediaUrl,
|
mediaUrl: opts.mediaUrl,
|
||||||
client: opts.client,
|
client: opts.client,
|
||||||
|
threadTs: opts.threadTs,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user