From f9be9ad426c88086cfba5985db6a33f19693236b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 9 Jan 2026 08:59:54 +0100 Subject: [PATCH] feat: switch message cli to subcommands --- AGENTS.md | 8 +- CHANGELOG.md | 2 +- README.md | 2 +- docs/automation/poll.md | 8 +- docs/cli/message.md | 205 +++++------- docs/gateway/index.md | 2 +- docs/index.md | 2 +- docs/nodes/images.md | 6 +- docs/providers/telegram.md | 2 +- docs/providers/whatsapp.md | 2 +- docs/start/faq.md | 2 +- docs/start/getting-started.md | 2 +- src/cli/program.test.ts | 9 +- src/cli/program.ts | 572 +++++++++++++++++++++++++++------- 14 files changed, 561 insertions(+), 263 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f120b43ee..42088930c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -93,17 +93,17 @@ - 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. - 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 --action send` messages that include `!`, use the heredoc pattern noted below to avoid the Bash tool’s escaping. +- For manual `clawdbot message send` messages that include `!`, use the heredoc pattern noted below to avoid the Bash tool’s escaping. ## Exclamation Mark Escaping Workaround -The Claude Code Bash tool escapes `!` to `\\!` in command arguments. When using `clawdbot message --action send` with messages containing exclamation marks, use heredoc syntax: +The Claude Code Bash tool escapes `!` to `\\!` in command arguments. When using `clawdbot message send` with messages containing exclamation marks, use heredoc syntax: ```bash # WRONG - will send "Hello\\!" with backslash -clawdbot message --action send --to "+1234" --message 'Hello!' +clawdbot message send --to "+1234" --message 'Hello!' # CORRECT - use heredoc to avoid escaping -clawdbot message --action send --to "+1234" --message "$(cat <<'EOF' +clawdbot message send --to "+1234" --message "$(cat <<'EOF' Hello! EOF )" diff --git a/CHANGELOG.md b/CHANGELOG.md index e7ed87ec1..70f9aeca8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ - Commands: accept /models as an alias for /model. - 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). -- 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: move `clawdbot message` to subcommands (`message send|poll|…`), fold Discord/Slack/Telegram/WhatsApp tools into `message`, and require `--provider` unless only one provider is configured. - 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 - Models: add OAuth expiry checks in doctor, expanded `models status` auth output (missing auth + `--check` exit codes). (#538) — thanks @latitudeki5223 diff --git a/README.md b/README.md index 84cd78435..18ea6ca16 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ clawdbot onboard --install-daemon clawdbot gateway --port 18789 --verbose # Send a message -clawdbot message --to +1234567890 --message "Hello from Clawdbot" +clawdbot message send --to +1234567890 --message "Hello from Clawdbot" # Talk to the assistant (optionally deliver back to WhatsApp/Telegram/Slack/Discord) clawdbot agent --message "Ship checklist" --thinking high diff --git a/docs/automation/poll.md b/docs/automation/poll.md index 071b4a071..39307f946 100644 --- a/docs/automation/poll.md +++ b/docs/automation/poll.md @@ -15,15 +15,15 @@ read_when: ```bash # WhatsApp -clawdbot message --action poll --to +15555550123 \ +clawdbot message poll --to +15555550123 \ --poll-question "Lunch today?" --poll-option "Yes" --poll-option "No" --poll-option "Maybe" -clawdbot message --action poll --to 123456789@g.us \ +clawdbot message poll --to 123456789@g.us \ --poll-question "Meeting time?" --poll-option "10am" --poll-option "2pm" --poll-option "4pm" --poll-multi # Discord -clawdbot message --action poll --provider discord --to channel:123456789 \ +clawdbot message poll --provider discord --to channel:123456789 \ --poll-question "Snack?" --poll-option "Pizza" --poll-option "Sushi" -clawdbot message --action poll --provider discord --to channel:123456789 \ +clawdbot message poll --provider discord --to channel:123456789 \ --poll-question "Plan?" --poll-option "A" --poll-option "B" --poll-duration-hours 48 ``` diff --git a/docs/cli/message.md b/docs/cli/message.md index 7ddc33fda..47aa67320 100644 --- a/docs/cli/message.md +++ b/docs/cli/message.md @@ -13,12 +13,9 @@ Single outbound command for sending messages and provider actions ## Usage ``` -clawdbot message --action [--provider ] [flags] +clawdbot message [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. @@ -33,170 +30,124 @@ Target formats (`--to`): ## Common flags -- `--to ` -- `--message ` -- `--media ` -- `--message-id ` -- `--reply-to ` -- `--thread-id ` (Telegram forum thread) -- `--account ` (multi-account providers) -- `--dry-run` +- `--provider ` +- `--account ` - `--json` +- `--dry-run` - `--verbose` ## Actions -### `send` -Providers: whatsapp, telegram, discord, slack, signal, imessage -Required: `--to`, `--message` -Optional: `--media`, `--reply-to`, `--thread-id`, `--account`, `--gif-playback` +### Core -### `react` -Providers: discord, slack, telegram, whatsapp -Required: `--to`, `--message-id` -Optional: `--emoji`, `--remove`, `--participant`, `--from-me`, `--account` +- `send` + - Required: `--to`, `--message` + - Optional: `--media`, `--reply-to`, `--thread-id`, `--gif-playback` -### `reactions` -Providers: discord, slack -Required: `--to`, `--message-id` -Optional: `--limit` +- `poll` + - Required: `--to`, `--poll-question`, `--poll-option` (repeat) + - Optional: `--poll-multi`, `--poll-duration-hours`, `--message` -### `read` -Providers: discord, slack -Required: `--to` -Optional: `--limit`, `--before`, `--after`, `--around` +- `react` + - Required: `--to`, `--message-id` + - Optional: `--emoji`, `--remove`, `--participant`, `--from-me`, `--channel-id` -### `edit` -Providers: discord, slack -Required: `--to`, `--message-id`, `--message` +- `reactions` + - Required: `--to`, `--message-id` + - Optional: `--limit`, `--channel-id` -### `delete` -Providers: discord, slack -Required: `--to`, `--message-id` +- `read` + - Required: `--to` + - Optional: `--limit`, `--before`, `--after`, `--around`, `--channel-id` -### `pin` -Providers: discord, slack -Required: `--to`, `--message-id` +- `edit` + - Required: `--to`, `--message-id`, `--message` + - Optional: `--channel-id` -### `unpin` -Providers: discord, slack -Required: `--to`, `--message-id` +- `delete` + - Required: `--to`, `--message-id` + - Optional: `--channel-id` -### `list-pins` -Providers: discord, slack -Required: `--to` +- `pin` / `unpin` + - Required: `--to`, `--message-id` + - Optional: `--channel-id` -### `poll` -Providers: whatsapp, discord -Required: `--to`, `--poll-question`, `--poll-option` (repeat) -Optional: `--poll-multi`, `--poll-duration-hours`, `--message` +- `pins` (list) + - Required: `--to` + - Optional: `--channel-id` -### `sticker` -Providers: discord -Required: `--to`, `--sticker-id` (repeat) -Optional: `--message` +- `permissions` + - Required: `--to` + - Optional: `--channel-id` -### `permissions` -Providers: discord -Required: `--to` (channel id) +- `search` + - Required: `--guild-id`, `--query` + - Optional: `--channel-id`, `--channel-ids` (repeat), `--author-id`, `--author-ids` (repeat), `--limit` -### `thread-create` -Providers: discord -Required: `--to` (channel id), `--thread-name` -Optional: `--message-id`, `--auto-archive-min` +### Threads -### `thread-list` -Providers: discord -Required: `--guild-id` -Optional: `--channel-id`, `--include-archived`, `--before`, `--limit` +- `thread create` + - Required: `--thread-name`, `--to` (channel id) or `--channel-id` + - Optional: `--message-id`, `--auto-archive-min` -### `thread-reply` -Providers: discord -Required: `--to` (thread id), `--message` -Optional: `--media`, `--reply-to` +- `thread list` + - Required: `--guild-id` + - Optional: `--channel-id`, `--include-archived`, `--before`, `--limit` -### `search` -Providers: discord -Required: `--guild-id`, `--query` -Optional: `--channel-id`, `--channel-ids`, `--author-id`, `--author-ids`, `--limit` +- `thread reply` + - Required: `--to` (thread id), `--message` + - Optional: `--media`, `--reply-to` -### `member-info` -Providers: discord, slack -Required: `--user-id` -Discord only: also `--guild-id` +### Emojis -### `role-info` -Providers: discord -Required: `--guild-id` +- `emoji list` + - Discord: `--guild-id` -### `emoji-list` -Providers: discord, slack -Discord only: `--guild-id` +- `emoji upload` + - Required: `--guild-id`, `--emoji-name`, `--media` + - Optional: `--role-ids` (repeat) -### `emoji-upload` -Providers: discord -Required: `--guild-id`, `--emoji-name`, `--media` -Optional: `--role-ids` (repeat) +### Stickers -### `sticker-upload` -Providers: discord -Required: `--guild-id`, `--sticker-name`, `--sticker-desc`, `--sticker-tags`, `--media` +- `sticker send` + - Required: `--to`, `--sticker-id` (repeat) + - Optional: `--message` -### `role-add` -Providers: discord -Required: `--guild-id`, `--user-id`, `--role-id` +- `sticker upload` + - Required: `--guild-id`, `--sticker-name`, `--sticker-desc`, `--sticker-tags`, `--media` -### `role-remove` -Providers: discord -Required: `--guild-id`, `--user-id`, `--role-id` +### Roles / Channels / Members / Voice -### `channel-info` -Providers: discord -Required: `--channel-id` +- `role info` (Discord): `--guild-id` +- `role add` / `role remove` (Discord): `--guild-id`, `--user-id`, `--role-id` +- `channel info` (Discord): `--channel-id` +- `channel list` (Discord): `--guild-id` +- `member info` (Discord/Slack): `--user-id` (+ `--guild-id` for Discord) +- `voice status` (Discord): `--guild-id`, `--user-id` -### `channel-list` -Providers: discord -Required: `--guild-id` +### Events -### `voice-status` -Providers: discord -Required: `--guild-id`, `--user-id` +- `event list` (Discord): `--guild-id` +- `event create` (Discord): `--guild-id`, `--event-name`, `--start-time` + - Optional: `--end-time`, `--desc`, `--channel-id`, `--location`, `--event-type` -### `event-list` -Providers: discord -Required: `--guild-id` +### Moderation (Discord) -### `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` +- `timeout`: `--guild-id`, `--user-id` (+ `--duration-min` or `--until`) +- `kick`: `--guild-id`, `--user-id` +- `ban`: `--guild-id`, `--user-id` (+ `--delete-days`) ## Examples Send a Discord reply: ``` -clawdbot message --action send --provider discord \ +clawdbot message send --provider discord \ --to channel:123 --message "hi" --reply-to 456 ``` Create a Discord poll: ``` -clawdbot message --action poll --provider discord \ +clawdbot message poll --provider discord \ --to channel:123 \ --poll-question "Snack?" \ --poll-option Pizza --poll-option Sushi \ @@ -205,6 +156,6 @@ clawdbot message --action poll --provider discord \ React in Slack: ``` -clawdbot message --action react --provider slack \ +clawdbot message react --provider slack \ --to C123 --message-id 456 --emoji "✅" ``` diff --git a/docs/gateway/index.md b/docs/gateway/index.md index 712ed6246..9b2e3dcf2 100644 --- a/docs/gateway/index.md +++ b/docs/gateway/index.md @@ -254,7 +254,7 @@ Windows installs should use **WSL2** and follow the Linux systemd section above. ## CLI helpers - `clawdbot gateway health|status` — request health/status over the Gateway WS. -- `clawdbot message --to --message "hi" [--media ...]` — send via Gateway (idempotent for WhatsApp). +- `clawdbot message send --to --message "hi" [--media ...]` — send via Gateway (idempotent for WhatsApp). - `clawdbot agent --message "hi" --to ` — run an agent turn (waits for final by default). - `clawdbot gateway call --params '{"k":"v"}'` — raw method invoker for debugging. - `clawdbot daemon stop|restart` — stop/restart the supervised gateway service (launchd/systemd). diff --git a/docs/index.md b/docs/index.md index e01f53e61..35332ecf1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -134,7 +134,7 @@ clawdbot gateway --port 19001 Send a test message (requires a running Gateway): ```bash -clawdbot message --to +15555550123 --message "Hello from CLAWDBOT" +clawdbot message send --to +15555550123 --message "Hello from CLAWDBOT" ``` ## Configuration (optional) diff --git a/docs/nodes/images.md b/docs/nodes/images.md index 8235cc992..84c1a3008 100644 --- a/docs/nodes/images.md +++ b/docs/nodes/images.md @@ -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. ## Goals -- Send media with optional captions via `clawdbot message --media`. +- Send media with optional captions via `clawdbot message send --media`. - Allow auto-replies from the web inbox to include media alongside text. - Keep per-type limits sane and predictable. ## CLI Surface -- `clawdbot message --media [--message ]` +- `clawdbot message send --media [--message ]` - `--media` optional; caption can be empty for media-only sends. - `--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 - `getReplyFromConfig` returns `{ text?, mediaUrl?, mediaUrls? }`. -- When media is present, the web sender resolves local paths or URLs using the same pipeline as `clawdbot message`. +- When media is present, the web sender resolves local paths or URLs using the same pipeline as `clawdbot message send`. - Multiple media entries are sent sequentially if provided. ## Inbound Media to Commands (Pi) diff --git a/docs/providers/telegram.md b/docs/providers/telegram.md index b9f14f1ad..42cf31cf2 100644 --- a/docs/providers/telegram.md +++ b/docs/providers/telegram.md @@ -223,7 +223,7 @@ Outbound Telegram API calls retry on transient network/429 errors with exponenti ## Delivery targets (CLI/cron) - Use a chat id (`123456789`) or a username (`@name`) as the target. -- Example: `clawdbot message --provider telegram --to 123456789 --message "hi"`. +- Example: `clawdbot message send --provider telegram --to 123456789 --message "hi"`. ## Troubleshooting diff --git a/docs/providers/whatsapp.md b/docs/providers/whatsapp.md index ecb9ee190..4cec0dc62 100644 --- a/docs/providers/whatsapp.md +++ b/docs/providers/whatsapp.md @@ -158,7 +158,7 @@ Behavior: - Caption only on first media item. - Media fetch supports HTTP(S) and local paths. - Animated GIFs: WhatsApp expects MP4 with `gifPlayback: true` for inline looping. - - CLI: `clawdbot message --media --gif-playback` + - CLI: `clawdbot message send --media --gif-playback` - Gateway: `send` params include `gifPlayback: true` ## Media limits + optimization diff --git a/docs/start/faq.md b/docs/start/faq.md index aecd25afb..06be764b1 100644 --- a/docs/start/faq.md +++ b/docs/start/faq.md @@ -560,7 +560,7 @@ Outbound attachments from the agent must include a `MEDIA:` line (o CLI sending: ```bash -clawdbot message --to +15555550123 --message "Here you go" --media /path/to/file.png +clawdbot message send --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). diff --git a/docs/start/getting-started.md b/docs/start/getting-started.md index 8bc36d54f..f81d70a20 100644 --- a/docs/start/getting-started.md +++ b/docs/start/getting-started.md @@ -152,7 +152,7 @@ In a new terminal: ```bash clawdbot health -clawdbot message --to +15555550123 --message "Hello from Clawdbot" +clawdbot message send --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. diff --git a/src/cli/program.test.ts b/src/cli/program.test.ts index 3d44f0fbd..20bbf62cc 100644 --- a/src/cli/program.test.ts +++ b/src/cli/program.test.ts @@ -47,9 +47,12 @@ describe("cli program", () => { it("runs message with required options", async () => { const program = buildProgram(); - await program.parseAsync(["message", "--to", "+1", "--message", "hi"], { - from: "user", - }); + await program.parseAsync( + ["message", "send", "--to", "+1", "--message", "hi"], + { + from: "user", + }, + ); expect(messageCommand).toHaveBeenCalled(); }); diff --git a/src/cli/program.ts b/src/cli/program.ts index 1033ef145..23eac4ccb 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -408,128 +408,472 @@ export function buildProgram() { } }); - program + const message = program .command("message") .description("Send messages and provider actions") - .option( - "-a, --action ", - "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", - "send", - ) - .option( - "-t, --to ", - "Recipient/channel: E.164 for WhatsApp/Signal, Telegram chat id/@username, Discord/Slack channel/user, or iMessage handle/chat_id", - ) - .option("-m, --message ", "Message body") - .option( - "--media ", - "Attach media (image/audio/video/document). Accepts local paths or URLs.", - ) - .option("--message-id ", "Message id (edit/delete/react/pin)") - .option("--reply-to ", "Reply-to message id") - .option("--thread-id ", "Thread id (Telegram forum thread)") - .option("--account ", "Provider account id") - .option( - "--provider ", - "Provider: whatsapp|telegram|discord|slack|signal|imessage", - ) - .option("--emoji ", "Emoji for reactions") - .option("--remove", "Remove reaction", false) - .option("--limit ", "Result limit for read/reactions/search") - .option("--before ", "Read/search before id") - .option("--after ", "Read/search after id") - .option("--around ", "Read around id (Discord)") - .option("--poll-question ", "Poll question") - .option( - "--poll-option ", - "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 ", "Poll duration (Discord)") - .option("--channel-id ", "Channel id") - .option( - "--channel-ids ", - "Channel id (repeat)", - (value: string, previous: string[]) => previous.concat([value]), - [] as string[], - ) - .option("--guild-id ", "Guild id") - .option("--user-id ", "User id") - .option("--author-id ", "Author id") - .option( - "--author-ids ", - "Author id (repeat)", - (value: string, previous: string[]) => previous.concat([value]), - [] as string[], - ) - .option("--role-id ", "Role id") - .option( - "--role-ids ", - "Role id (repeat)", - (value: string, previous: string[]) => previous.concat([value]), - [] as string[], - ) - .option("--emoji-name ", "Emoji name") - .option( - "--sticker-id ", - "Sticker id (repeat)", - (value: string, previous: string[]) => previous.concat([value]), - [] as string[], - ) - .option("--sticker-name ", "Sticker name") - .option("--sticker-desc ", "Sticker description") - .option("--sticker-tags ", "Sticker tags") - .option("--thread-name ", "Thread name") - .option("--auto-archive-min ", "Thread auto-archive minutes") - .option("--query ", "Search query") - .option("--event-name ", "Event name") - .option("--event-type ", "Event type") - .option("--start-time ", "Event start time") - .option("--end-time ", "Event end time") - .option("--desc ", "Event description") - .option("--location ", "Event location") - .option("--duration-min ", "Timeout duration minutes") - .option("--until ", "Timeout until") - .option("--reason ", "Moderation reason") - .option("--delete-days ", "Ban delete message days") - .option("--include-archived", "Include archived threads", false) - .option("--participant ", "WhatsApp reaction participant") - .option("--from-me", "WhatsApp reaction fromMe", false) - .option( - "--gif-playback", - "Treat video media as GIF playback (WhatsApp only).", - false, - ) - .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 --to +15555550123 --message "Hi" - clawdbot message --action send --to +15555550123 --message "Hi" --media photo.jpg - clawdbot message --action poll --provider discord --to channel:123 --poll-question "Snack?" --poll-option Pizza --poll-option Sushi - clawdbot message --action react --provider discord --to 123 --message-id 456 --emoji "✅"`, + clawdbot message send --to +15555550123 --message "Hi" + clawdbot message send --to +15555550123 --message "Hi" --media photo.jpg + clawdbot message poll --provider discord --to channel:123 --poll-question "Snack?" --poll-option Pizza --poll-option Sushi + clawdbot message react --provider discord --to 123 --message-id 456 --emoji "✅"`, + ) + .action(() => { + message.help({ error: true }); + }); + + const withMessageBase = (command: Command) => + command + .option( + "--provider ", + "Provider: whatsapp|telegram|discord|slack|signal|imessage", + ) + .option("--account ", "Provider account id") + .option("--json", "Output result as JSON", false) + .option("--dry-run", "Print payload and skip sending", false) + .option("--verbose", "Verbose logging", false); + + const withMessageTarget = (command: Command) => + command.option( + "-t, --to ", + "Recipient/channel: E.164 for WhatsApp/Signal, Telegram chat id/@username, Discord/Slack channel/user, or iMessage handle/chat_id", + ); + const withRequiredMessageTarget = (command: Command) => + command.requiredOption( + "-t, --to ", + "Recipient/channel: E.164 for WhatsApp/Signal, Telegram chat id/@username, Discord/Slack channel/user, or iMessage handle/chat_id", + ); + + const runMessageAction = async ( + action: string, + opts: Record, + ) => { + setVerbose(Boolean(opts.verbose)); + const deps = createDefaultDeps(); + try { + await messageCommand( + { + ...opts, + action, + account: opts.account as string | undefined, + }, + deps, + defaultRuntime, + ); + } catch (err) { + defaultRuntime.error(String(err)); + defaultRuntime.exit(1); + } + }; + + withMessageBase( + withRequiredMessageTarget( + message + .command("send") + .description("Send a message") + .requiredOption("-m, --message ", "Message body"), + ) + .option( + "--media ", + "Attach media (image/audio/video/document). Accepts local paths or URLs.", + ) + .option("--reply-to ", "Reply-to message id") + .option("--thread-id ", "Thread id (Telegram forum thread)") + .option( + "--gif-playback", + "Treat video media as GIF playback (WhatsApp only).", + false, + ), + ).action(async (opts) => { + await runMessageAction("send", opts); + }); + + withMessageBase( + withRequiredMessageTarget( + message.command("poll").description("Send a poll"), + ), + ) + .requiredOption("--poll-question ", "Poll question") + .option( + "--poll-option ", + "Poll option (repeat 2-12 times)", + collectOption, + [] as string[], + ) + .option("--poll-multi", "Allow multiple selections", false) + .option("--poll-duration-hours ", "Poll duration (Discord)") + .option("-m, --message ", "Optional message body") + .action(async (opts) => { + await runMessageAction("poll", opts); + }); + + withMessageBase( + withMessageTarget( + message.command("react").description("Add or remove a reaction"), + ), + ) + .requiredOption("--message-id ", "Message id") + .option("--emoji ", "Emoji for reactions") + .option("--remove", "Remove reaction", false) + .option("--participant ", "WhatsApp reaction participant") + .option("--from-me", "WhatsApp reaction fromMe", false) + .option("--channel-id ", "Channel id (defaults to --to)") + .action(async (opts) => { + await runMessageAction("react", opts); + }); + + withMessageBase( + withMessageTarget( + message.command("reactions").description("List reactions on a message"), + ), + ) + .requiredOption("--message-id ", "Message id") + .option("--limit ", "Result limit") + .option("--channel-id ", "Channel id (defaults to --to)") + .action(async (opts) => { + await runMessageAction("reactions", opts); + }); + + withMessageBase( + withMessageTarget( + message.command("read").description("Read recent messages"), + ), + ) + .option("--limit ", "Result limit") + .option("--before ", "Read/search before id") + .option("--after ", "Read/search after id") + .option("--around ", "Read around id (Discord)") + .option("--channel-id ", "Channel id (defaults to --to)") + .action(async (opts) => { + await runMessageAction("read", opts); + }); + + withMessageBase( + withMessageTarget( + message + .command("edit") + .description("Edit a message") + .requiredOption("-m, --message ", "Message body"), + ), + ) + .requiredOption("--message-id ", "Message id") + .option("--channel-id ", "Channel id (defaults to --to)") + .action(async (opts) => { + await runMessageAction("edit", opts); + }); + + withMessageBase( + withMessageTarget( + message.command("delete").description("Delete a message"), + ), + ) + .requiredOption("--message-id ", "Message id") + .option("--channel-id ", "Channel id (defaults to --to)") + .action(async (opts) => { + await runMessageAction("delete", opts); + }); + + withMessageBase( + withMessageTarget(message.command("pin").description("Pin a message")), + ) + .requiredOption("--message-id ", "Message id") + .option("--channel-id ", "Channel id (defaults to --to)") + .action(async (opts) => { + await runMessageAction("pin", opts); + }); + + withMessageBase( + withMessageTarget(message.command("unpin").description("Unpin a message")), + ) + .option("--message-id ", "Message id") + .option("--channel-id ", "Channel id (defaults to --to)") + .action(async (opts) => { + await runMessageAction("unpin", opts); + }); + + withMessageBase( + withMessageTarget( + message.command("pins").description("List pinned messages"), + ), + ) + .option("--channel-id ", "Channel id (defaults to --to)") + .action(async (opts) => { + await runMessageAction("list-pins", opts); + }); + + withMessageBase( + withMessageTarget( + message.command("permissions").description("Fetch channel permissions"), + ), + ) + .option("--channel-id ", "Channel id (defaults to --to)") + .action(async (opts) => { + await runMessageAction("permissions", opts); + }); + + withMessageBase( + message.command("search").description("Search Discord messages"), + ) + .requiredOption("--guild-id ", "Guild id") + .requiredOption("--query ", "Search query") + .option("--channel-id ", "Channel id") + .option( + "--channel-ids ", + "Channel id (repeat)", + collectOption, + [] as string[], + ) + .option("--author-id ", "Author id") + .option( + "--author-ids ", + "Author id (repeat)", + collectOption, + [] as string[], + ) + .option("--limit ", "Result limit") + .action(async (opts) => { + await runMessageAction("search", opts); + }); + + const thread = message.command("thread").description("Thread actions"); + + withMessageBase( + withMessageTarget( + thread + .command("create") + .description("Create a thread") + .requiredOption("--thread-name ", "Thread name"), + ), + ) + .option("--channel-id ", "Channel id (defaults to --to)") + .option("--message-id ", "Message id (optional)") + .option("--auto-archive-min ", "Thread auto-archive minutes") + .action(async (opts) => { + await runMessageAction("thread-create", opts); + }); + + withMessageBase( + thread + .command("list") + .description("List threads") + .requiredOption("--guild-id ", "Guild id"), + ) + .option("--channel-id ", "Channel id") + .option("--include-archived", "Include archived threads", false) + .option("--before ", "Read/search before id") + .option("--limit ", "Result limit") + .action(async (opts) => { + await runMessageAction("thread-list", opts); + }); + + withMessageBase( + withRequiredMessageTarget( + thread + .command("reply") + .description("Reply in a thread") + .requiredOption("-m, --message ", "Message body"), + ), + ) + .option( + "--media ", + "Attach media (image/audio/video/document). Accepts local paths or URLs.", + ) + .option("--reply-to ", "Reply-to message id") + .action(async (opts) => { + await runMessageAction("thread-reply", opts); + }); + + const emoji = message.command("emoji").description("Emoji actions"); + withMessageBase(emoji.command("list").description("List emojis")) + .option("--guild-id ", "Guild id (Discord)") + .action(async (opts) => { + await runMessageAction("emoji-list", opts); + }); + + withMessageBase( + emoji + .command("upload") + .description("Upload an emoji") + .requiredOption("--guild-id ", "Guild id"), + ) + .requiredOption("--emoji-name ", "Emoji name") + .requiredOption("--media ", "Emoji media (path or URL)") + .option( + "--role-ids ", + "Role id (repeat)", + collectOption, + [] as string[], ) .action(async (opts) => { - setVerbose(Boolean(opts.verbose)); - const deps = createDefaultDeps(); - try { - await messageCommand( - { - ...opts, - account: opts.account as string | undefined, - }, - deps, - defaultRuntime, - ); - } catch (err) { - defaultRuntime.error(String(err)); - defaultRuntime.exit(1); - } + await runMessageAction("emoji-upload", opts); + }); + + const sticker = message.command("sticker").description("Sticker actions"); + withMessageBase( + withRequiredMessageTarget( + sticker.command("send").description("Send stickers"), + ), + ) + .requiredOption("--sticker-id ", "Sticker id (repeat)", collectOption) + .option("-m, --message ", "Optional message body") + .action(async (opts) => { + await runMessageAction("sticker", opts); + }); + + withMessageBase( + sticker + .command("upload") + .description("Upload a sticker") + .requiredOption("--guild-id ", "Guild id"), + ) + .requiredOption("--sticker-name ", "Sticker name") + .requiredOption("--sticker-desc ", "Sticker description") + .requiredOption("--sticker-tags ", "Sticker tags") + .requiredOption("--media ", "Sticker media (path or URL)") + .action(async (opts) => { + await runMessageAction("sticker-upload", opts); + }); + + const role = message.command("role").description("Role actions"); + withMessageBase( + role + .command("info") + .description("List roles") + .requiredOption("--guild-id ", "Guild id"), + ).action(async (opts) => { + await runMessageAction("role-info", opts); + }); + + withMessageBase( + role + .command("add") + .description("Add role to a member") + .requiredOption("--guild-id ", "Guild id") + .requiredOption("--user-id ", "User id") + .requiredOption("--role-id ", "Role id"), + ).action(async (opts) => { + await runMessageAction("role-add", opts); + }); + + withMessageBase( + role + .command("remove") + .description("Remove role from a member") + .requiredOption("--guild-id ", "Guild id") + .requiredOption("--user-id ", "User id") + .requiredOption("--role-id ", "Role id"), + ).action(async (opts) => { + await runMessageAction("role-remove", opts); + }); + + const channel = message.command("channel").description("Channel actions"); + withMessageBase( + channel + .command("info") + .description("Fetch channel info") + .requiredOption("--channel-id ", "Channel id"), + ).action(async (opts) => { + await runMessageAction("channel-info", opts); + }); + + withMessageBase( + channel + .command("list") + .description("List channels") + .requiredOption("--guild-id ", "Guild id"), + ).action(async (opts) => { + await runMessageAction("channel-list", opts); + }); + + const member = message.command("member").description("Member actions"); + withMessageBase( + member + .command("info") + .description("Fetch member info") + .requiredOption("--user-id ", "User id"), + ) + .option("--guild-id ", "Guild id (Discord)") + .action(async (opts) => { + await runMessageAction("member-info", opts); + }); + + const voice = message.command("voice").description("Voice actions"); + withMessageBase( + voice + .command("status") + .description("Fetch voice status") + .requiredOption("--guild-id ", "Guild id") + .requiredOption("--user-id ", "User id"), + ).action(async (opts) => { + await runMessageAction("voice-status", opts); + }); + + const event = message.command("event").description("Event actions"); + withMessageBase( + event + .command("list") + .description("List scheduled events") + .requiredOption("--guild-id ", "Guild id"), + ).action(async (opts) => { + await runMessageAction("event-list", opts); + }); + + withMessageBase( + event + .command("create") + .description("Create a scheduled event") + .requiredOption("--guild-id ", "Guild id") + .requiredOption("--event-name ", "Event name") + .requiredOption("--start-time ", "Event start time"), + ) + .option("--end-time ", "Event end time") + .option("--desc ", "Event description") + .option("--channel-id ", "Channel id") + .option("--location ", "Event location") + .option("--event-type ", "Event type") + .action(async (opts) => { + await runMessageAction("event-create", opts); + }); + + withMessageBase( + message + .command("timeout") + .description("Timeout a member") + .requiredOption("--guild-id ", "Guild id") + .requiredOption("--user-id ", "User id"), + ) + .option("--duration-min ", "Timeout duration minutes") + .option("--until ", "Timeout until") + .option("--reason ", "Moderation reason") + .action(async (opts) => { + await runMessageAction("timeout", opts); + }); + + withMessageBase( + message + .command("kick") + .description("Kick a member") + .requiredOption("--guild-id ", "Guild id") + .requiredOption("--user-id ", "User id"), + ) + .option("--reason ", "Moderation reason") + .action(async (opts) => { + await runMessageAction("kick", opts); + }); + + withMessageBase( + message + .command("ban") + .description("Ban a member") + .requiredOption("--guild-id ", "Guild id") + .requiredOption("--user-id ", "User id"), + ) + .option("--reason ", "Moderation reason") + .option("--delete-days ", "Ban delete message days") + .action(async (opts) => { + await runMessageAction("ban", opts); }); program