diff --git a/docs/cli/index.md b/docs/cli/index.md index 594965be7..4e1f708a3 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -1,5 +1,5 @@ --- -summary: "Clawdbot CLI reference for `clawdbot` commands, subcommands, and options" +summary: "CLI reference for clawdbot commands, subcommands, and options" read_when: - Adding or modifying CLI commands or options - Documenting new command surfaces @@ -7,13 +7,13 @@ read_when: # CLI reference -This page describes the current CLI behavior. If commands change, update this doc. +This page mirrors `src/cli/*` and is the source of truth for CLI behavior. +If you change the CLI code, update this doc. ## Global flags - `--dev`: isolate state under `~/.clawdbot-dev` and shift default ports. - `--profile `: isolate state under `~/.clawdbot-`. -- `--no-color`: disable ANSI colors. - `-V`, `--version`, `-v`: print version and exit. ## Output styling @@ -21,12 +21,11 @@ This page describes the current CLI behavior. If commands change, update this do - ANSI colors and progress indicators only render in TTY sessions. - OSC-8 hyperlinks render as clickable links in supported terminals; otherwise we fall back to plain URLs. - `--json` (and `--plain` where supported) disables styling for clean output. -- `--no-color` disables ANSI styling; `NO_COLOR=1` is also respected. - Long-running commands show a progress indicator (OSC 9;4 when supported). ## Color palette -Clawdbot uses a lobster palette for CLI output. +Clawdbot uses a lobster palette for CLI output. Source of truth: `src/terminal/theme.ts`. - `accent` (#FF5A2D): headings, provider labels, primary highlights. - `accentBright` (#FF7A3D): command names, emphasis. @@ -37,8 +36,6 @@ Clawdbot uses a lobster palette for CLI output. - `error` (#E23D2D): errors, failures. - `muted` (#8B7F77): de-emphasis, metadata. -Palette source of truth: `src/terminal/palette.ts` (aka “lobster seam”). - ## Command tree ``` @@ -58,7 +55,8 @@ clawdbot [--dev] [--profile ] list info check - message + send + poll agent agents list @@ -71,7 +69,6 @@ clawdbot [--dev] [--profile ] call health status - discover models list status @@ -169,10 +166,11 @@ Options: - `--workspace ` - `--non-interactive` - `--mode ` -- `--auth-choice ` +- `--auth-choice ` - `--anthropic-api-key ` - `--openai-api-key ` - `--gemini-api-key ` +- `--minimax-api-key ` - `--gateway-port ` - `--gateway-bind ` - `--gateway-auth ` @@ -208,8 +206,7 @@ Manage chat provider accounts (WhatsApp/Telegram/Discord/Slack/Signal/iMessage). Subcommands: - `providers list`: show configured chat providers and auth profiles (Claude Code + Codex CLI OAuth sync included). -- `providers status`: check gateway reachability and provider health (`--probe` to verify credentials and run small provider audits; use `status --deep` for local-only probes). -- Tip: `providers status` prints warnings with suggested fixes when it can detect common misconfigurations (then points you to `clawdbot doctor`). +- `providers status`: check gateway reachability and provider health (`--probe` to verify credentials; use `status --deep` for local-only probes). - `providers add`: wizard-style setup when no flags are passed; flags switch to non-interactive mode. - `providers remove`: disable by default; pass `--delete` to remove config entries without prompts. - `providers login`: interactive provider login (WhatsApp Web only). @@ -234,9 +231,7 @@ Common options: - `--json`: output JSON (includes usage unless `--no-usage` is set). OAuth sync sources: -- Claude Code → `anthropic:claude-cli` - - macOS: Keychain item "Claude Code-credentials" (choose "Always Allow" to avoid launchd prompts) - - Linux/Windows: `~/.claude/.credentials.json` +- `~/.claude/.credentials.json` → `anthropic:claude-cli` - `~/.codex/auth.json` → `openai-codex:codex-cli` More detail: [/concepts/oauth](/concepts/oauth) @@ -287,25 +282,37 @@ Options: ## Messaging + agent -### `message` -Unified outbound messaging + provider actions. +### `send` +Send a message through a provider. -See: [/cli/message](/cli/message) +Required: +- `--to ` +- `--message ` -Subcommands: -- `message send|poll|react|reactions|read|edit|delete|pin|unpin|pins|permissions|search|timeout|kick|ban` -- `message thread ` -- `message emoji ` -- `message sticker ` -- `message role ` -- `message channel ` -- `message member info` -- `message voice status` -- `message event ` +Options: +- `--media ` +- `--gif-playback` +- `--provider ` +- `--account ` (WhatsApp) +- `--dry-run` +- `--json` +- `--verbose` -Examples: -- `clawdbot message send --to +15555550123 --message "Hi"` -- `clawdbot message poll --provider discord --to channel:123 --poll-question "Snack?" --poll-option Pizza --poll-option Sushi` +### `poll` +Create a poll (WhatsApp or Discord). + +Required: +- `--to ` +- `--question ` +- `--option ` (repeat 2-12 times) + +Options: +- `--max-selections ` +- `--duration-hours ` (Discord) +- `--provider ` +- `--dry-run` +- `--json` +- `--verbose` ### `agent` Run one agent turn via the Gateway (or `--local` embedded). @@ -409,8 +416,6 @@ Options: - `--tailscale ` - `--tailscale-reset-on-exit` - `--allow-unconfigured` -- `--dev` -- `--reset` - `--force` (kill existing listener on port) - `--verbose` - `--ws-log ` @@ -438,17 +443,10 @@ Notes: ### `logs` Tail Gateway file logs via RPC. -Notes: -- TTY sessions render a colorized, structured view; non-TTY falls back to plain text. -- `--json` emits line-delimited JSON (one log event per line). - Examples: ```bash clawdbot logs --follow clawdbot logs --limit 200 -clawdbot logs --plain -clawdbot logs --json -clawdbot logs --no-color ``` ### `gateway ` @@ -482,9 +480,6 @@ Options: Options: - `--json` - `--plain` -- `--check` (exit 1=expired/missing, 2=expiring) - -Always includes the auth overview and OAuth expiry status for profiles in the auth store. ### `models set ` Set `agent.model.primary`. diff --git a/scripts/bench-model.ts b/scripts/bench-model.ts index 32ed20ad0..0b3a60d01 100644 --- a/scripts/bench-model.ts +++ b/scripts/bench-model.ts @@ -88,7 +88,7 @@ async function main(): Promise { const minimaxBaseUrl = process.env.MINIMAX_BASE_URL?.trim() || "https://api.minimax.io/v1"; const minimaxModelId = - process.env.MINIMAX_MODEL?.trim() || "minimax-m2.1"; + process.env.MINIMAX_MODEL?.trim() || "MiniMax-M2.1"; const minimaxModel: Model<"openai-completions"> = { id: minimaxModelId, diff --git a/src/agents/minimax.live.test.ts b/src/agents/minimax.live.test.ts index 666943876..53f033af1 100644 --- a/src/agents/minimax.live.test.ts +++ b/src/agents/minimax.live.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest"; const MINIMAX_KEY = process.env.MINIMAX_API_KEY ?? ""; const MINIMAX_BASE_URL = process.env.MINIMAX_BASE_URL?.trim() || "https://api.minimax.io/v1"; -const MINIMAX_MODEL = process.env.MINIMAX_MODEL?.trim() || "minimax-m2.1"; +const MINIMAX_MODEL = process.env.MINIMAX_MODEL?.trim() || "MiniMax-M2.1"; const LIVE = process.env.MINIMAX_LIVE_TEST === "1" || process.env.LIVE === "1"; const describeLive = LIVE && MINIMAX_KEY ? describe : describe.skip; diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index 22ff3879b..2f36654ba 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -136,6 +136,7 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null { cerebras: "CEREBRAS_API_KEY", xai: "XAI_API_KEY", openrouter: "OPENROUTER_API_KEY", + minimax: "MINIMAX_API_KEY", zai: "ZAI_API_KEY", mistral: "MISTRAL_API_KEY", }; diff --git a/src/cli/program.ts b/src/cli/program.ts index cc463f510..2117e71aa 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -8,8 +8,9 @@ import { import { configureCommand } from "../commands/configure.js"; import { doctorCommand } from "../commands/doctor.js"; import { healthCommand } from "../commands/health.js"; -import { messageCommand } from "../commands/message.js"; import { onboardCommand } from "../commands/onboard.js"; +import { pollCommand } from "../commands/poll.js"; +import { sendCommand } from "../commands/send.js"; import { sessionsCommand } from "../commands/sessions.js"; import { setupCommand } from "../commands/setup.js"; import { statusCommand } from "../commands/status.js"; @@ -25,11 +26,7 @@ import { autoMigrateLegacyState } from "../infra/state-migrations.js"; import { defaultRuntime } from "../runtime.js"; import { isRich, theme } from "../terminal/theme.js"; import { VERSION } from "../version.js"; -import { - emitCliBanner, - formatCliBannerArt, - formatCliBannerLine, -} from "./banner.js"; +import { emitCliBanner, formatCliBannerLine } from "./banner.js"; import { registerBrowserCli } from "./browser-cli.js"; import { hasExplicitOptions } from "./command-options.js"; import { registerCronCli } from "./cron-cli.js"; @@ -73,8 +70,6 @@ export function buildProgram() { "Use a named profile (isolates CLAWDBOT_STATE_DIR/CLAWDBOT_CONFIG_PATH under ~/.clawdbot-)", ); - program.option("--no-color", "Disable ANSI colors", false); - program.configureHelp({ optionTerm: (option) => theme.option(option.flags), subcommandTerm: (cmd) => theme.command(cmd.name()), @@ -102,10 +97,8 @@ export function buildProgram() { } program.addHelpText("beforeAll", () => { - const rich = isRich(); - const art = formatCliBannerArt({ richTty: rich }); - const line = formatCliBannerLine(PROGRAM_VERSION, { richTty: rich }); - return `\n${art}\n${line}\n`; + const line = formatCliBannerLine(PROGRAM_VERSION, { richTty: isRich() }); + return `\n${line}\n`; }); program.hook("preAction", async (_thisCommand, actionCommand) => { @@ -154,7 +147,7 @@ export function buildProgram() { "Link personal WhatsApp Web and show QR + connection logs.", ], [ - 'clawdbot message send --to +15555550123 --message "Hi" --json', + 'clawdbot send --to +15555550123 --message "Hi" --json', "Send via your web session and print JSON result.", ], ["clawdbot gateway --port 18789", "Run the WebSocket Gateway locally."], @@ -172,7 +165,7 @@ export function buildProgram() { "Talk directly to the agent using the Gateway; optionally send the WhatsApp reply.", ], [ - 'clawdbot message send --provider telegram --to @mychat --message "Hi"', + 'clawdbot send --provider telegram --to @mychat --message "Hi"', "Send via your Telegram bot.", ], ] as const; @@ -240,11 +233,12 @@ export function buildProgram() { .option("--mode ", "Wizard mode: local|remote") .option( "--auth-choice ", - "Auth: oauth|claude-cli|token|openai-codex|openai-api-key|codex-cli|antigravity|gemini-api-key|apiKey|minimax|skip", + "Auth: oauth|claude-cli|token|openai-codex|openai-api-key|codex-cli|antigravity|gemini-api-key|apiKey|minimax-cloud|minimax|skip", ) .option("--anthropic-api-key ", "Anthropic API key") .option("--openai-api-key ", "OpenAI API key") .option("--gemini-api-key ", "Gemini API key") + .option("--minimax-api-key ", "MiniMax API key") .option("--gateway-port ", "Gateway port") .option("--gateway-bind ", "Gateway bind: loopback|lan|tailnet|auto") .option("--gateway-auth ", "Gateway auth: off|token|password") @@ -277,12 +271,14 @@ export function buildProgram() { | "antigravity" | "gemini-api-key" | "apiKey" + | "minimax-cloud" | "minimax" | "skip" | undefined, anthropicApiKey: opts.anthropicApiKey as string | undefined, openaiApiKey: opts.openaiApiKey as string | undefined, geminiApiKey: opts.geminiApiKey as string | undefined, + minimaxApiKey: opts.minimaxApiKey as string | undefined, gatewayPort: typeof opts.gatewayPort === "string" ? Number.parseInt(opts.gatewayPort, 10) @@ -343,12 +339,6 @@ export function buildProgram() { false, ) .option("--yes", "Accept defaults without prompting", false) - .option("--repair", "Apply recommended repairs without prompting", false) - .option( - "--force", - "Apply aggressive repairs (overwrites custom service config)", - false, - ) .option( "--non-interactive", "Run without prompts (safe migrations only)", @@ -360,8 +350,6 @@ export function buildProgram() { await doctorCommand(defaultRuntime, { workspaceSuggestions: opts.workspaceSuggestions, yes: Boolean(opts.yes), - repair: Boolean(opts.repair), - force: Boolean(opts.force), nonInteractive: Boolean(opts.nonInteractive), deep: Boolean(opts.deep), }); @@ -414,472 +402,107 @@ export function buildProgram() { } }); - const message = program - .command("message") - .description("Send messages and provider actions") - .addHelpText( - "after", - ` -Examples: - 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 "✅"`, + program + .command("send") + .description( + "Send a message (WhatsApp Web, Telegram bot, Discord, Slack, Signal, iMessage)", ) - .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"), + .requiredOption( + "-t, --to ", + "Recipient: E.164 for WhatsApp/Signal, Telegram chat id/@username, Discord channel/user, or iMessage handle/chat_id", ) - .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"), - ), - ) + .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[], + "--gif-playback", + "Treat video media as GIF playback (WhatsApp only).", + false, + ) + .option( + "--provider ", + "Delivery provider: whatsapp|telegram|discord|slack|signal|imessage (default: whatsapp)", + ) + .option("--account ", "WhatsApp account id (accountId)") + .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 send --to +15555550123 --message "Hi" + clawdbot send --to +15555550123 --message "Hi" --media photo.jpg + clawdbot send --to +15555550123 --message "Hi" --dry-run # print payload only + clawdbot send --to +15555550123 --message "Hi" --json # machine-readable result`, ) .action(async (opts) => { - await runMessageAction("emoji-upload", opts); + setVerbose(Boolean(opts.verbose)); + const deps = createDefaultDeps(); + try { + await sendCommand( + { + ...opts, + account: opts.account as string | undefined, + }, + deps, + defaultRuntime, + ); + } catch (err) { + defaultRuntime.error(String(err)); + defaultRuntime.exit(1); + } }); - 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") + program + .command("poll") + .description("Create a poll via WhatsApp or Discord") + .requiredOption( + "-t, --to ", + "Recipient: WhatsApp JID/number or Discord channel/user", + ) + .requiredOption("-q, --question ", "Poll question") + .requiredOption( + "-o, --option ", + "Poll option (use multiple times, 2-12 required)", + (value: string, previous: string[]) => previous.concat([value]), + [] as string[], + ) + .option( + "-s, --max-selections ", + "How many options can be selected (default: 1)", + ) + .option( + "--duration-hours ", + "Poll duration in hours (Discord only, default: 24)", + ) + .option( + "--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 poll --to +15555550123 -q "Lunch today?" -o "Yes" -o "No" -o "Maybe" + clawdbot poll --to 123456789@g.us -q "Meeting time?" -o "10am" -o "2pm" -o "4pm" -s 2 + clawdbot poll --to channel:123456789 -q "Snack?" -o "Pizza" -o "Sushi" --provider discord + clawdbot poll --to channel:123456789 -q "Plan?" -o "A" -o "B" --provider discord --duration-hours 48`, + ) .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); + setVerbose(Boolean(opts.verbose)); + const deps = createDefaultDeps(); + try { + await pollCommand(opts, deps, defaultRuntime); + } catch (err) { + defaultRuntime.error(String(err)); + defaultRuntime.exit(1); + } }); program diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index 160f64911..c4203b5d5 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -93,6 +93,7 @@ export function buildAuthChoiceOptions(params: { options.push({ value: "gemini-api-key", label: "Google Gemini API key" }); options.push({ value: "apiKey", label: "Anthropic API key" }); // Token flow is currently Anthropic-only; use CLI for advanced providers. + options.push({ value: "minimax-cloud", label: "MiniMax M2.1 (minimax.io)" }); options.push({ value: "minimax", label: "Minimax M2.1 (LM Studio)" }); if (params.includeSkip) { options.push({ value: "skip", label: "Skip for now" }); diff --git a/src/commands/auth-choice.ts b/src/commands/auth-choice.ts index 6505f8bab..288aa5f73 100644 --- a/src/commands/auth-choice.ts +++ b/src/commands/auth-choice.ts @@ -37,9 +37,13 @@ import { import { applyAuthProfileConfig, applyMinimaxConfig, + applyMinimaxHostedConfig, + applyMinimaxHostedProviderConfig, applyMinimaxProviderConfig, + MINIMAX_HOSTED_MODEL_REF, setAnthropicApiKey, setGeminiApiKey, + setMinimaxApiKey, writeOAuthCredentials, } from "./onboard-auth.js"; import { openUrl } from "./onboard-helpers.js"; @@ -529,6 +533,24 @@ export async function applyAuthChoice(params: { provider: "anthropic", mode: "api_key", }); + } else if (params.authChoice === "minimax-cloud") { + const key = await params.prompter.text({ + message: "Enter MiniMax API key", + validate: (value) => (value?.trim() ? undefined : "Required"), + }); + await setMinimaxApiKey(String(key).trim(), params.agentDir); + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "minimax:default", + provider: "minimax", + mode: "api_key", + }); + if (params.setDefaultModel) { + nextConfig = applyMinimaxHostedConfig(nextConfig); + } else { + nextConfig = applyMinimaxHostedProviderConfig(nextConfig); + agentModelOverride = MINIMAX_HOSTED_MODEL_REF; + await noteAgentModel(MINIMAX_HOSTED_MODEL_REF); + } } else if (params.authChoice === "minimax") { if (params.setDefaultModel) { nextConfig = applyMinimaxConfig(nextConfig); diff --git a/src/commands/configure.ts b/src/commands/configure.ts index 14551cac5..29ab1dc87 100644 --- a/src/commands/configure.ts +++ b/src/commands/configure.ts @@ -69,8 +69,10 @@ import { healthCommand } from "./health.js"; import { applyAuthProfileConfig, applyMinimaxConfig, + applyMinimaxHostedConfig, setAnthropicApiKey, setGeminiApiKey, + setMinimaxApiKey, writeOAuthCredentials, } from "./onboard-auth.js"; import { @@ -357,6 +359,7 @@ async function promptAuthConfig( | "antigravity" | "gemini-api-key" | "apiKey" + | "minimax-cloud" | "minimax" | "skip"; @@ -691,6 +694,21 @@ async function promptAuthConfig( provider: "anthropic", mode: "api_key", }); + } else if (authChoice === "minimax-cloud") { + const key = guardCancel( + await text({ + message: "Enter MiniMax API key", + validate: (value) => (value?.trim() ? undefined : "Required"), + }), + runtime, + ); + await setMinimaxApiKey(String(key).trim()); + next = applyAuthProfileConfig(next, { + profileId: "minimax:default", + provider: "minimax", + mode: "api_key", + }); + next = applyMinimaxHostedConfig(next); } else if (authChoice === "minimax") { next = applyMinimaxConfig(next); } diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts index 76e976c22..71f42b9f4 100644 --- a/src/commands/onboard-auth.ts +++ b/src/commands/onboard-auth.ts @@ -3,6 +3,12 @@ import { resolveDefaultAgentDir } from "../agents/agent-scope.js"; import { upsertAuthProfile } from "../agents/auth-profiles.js"; import type { ClawdbotConfig } from "../config/config.js"; +const DEFAULT_MINIMAX_BASE_URL = "https://api.minimax.io/v1"; +export const MINIMAX_HOSTED_MODEL_ID = "MiniMax-M2.1"; +const DEFAULT_MINIMAX_CONTEXT_WINDOW = 200000; +const DEFAULT_MINIMAX_MAX_TOKENS = 8192; +export const MINIMAX_HOSTED_MODEL_REF = `minimax/${MINIMAX_HOSTED_MODEL_ID}`; + export async function writeOAuthCredentials( provider: OAuthProvider, creds: OAuthCredentials, @@ -46,6 +52,19 @@ export async function setGeminiApiKey(key: string, agentDir?: string) { }); } +export async function setMinimaxApiKey(key: string, agentDir?: string) { + // Write to the multi-agent path so gateway finds credentials on startup + upsertAuthProfile({ + profileId: "minimax:default", + credential: { + type: "api_key", + provider: "minimax", + key, + }, + agentDir: agentDir ?? resolveDefaultAgentDir(), + }); +} + export function applyAuthProfileConfig( cfg: ClawdbotConfig, params: { @@ -143,6 +162,57 @@ export function applyMinimaxProviderConfig( }; } +export function applyMinimaxHostedProviderConfig( + cfg: ClawdbotConfig, + params?: { baseUrl?: string }, +): ClawdbotConfig { + const models = { ...cfg.agent?.models }; + models[MINIMAX_HOSTED_MODEL_REF] = { + ...models[MINIMAX_HOSTED_MODEL_REF], + alias: models[MINIMAX_HOSTED_MODEL_REF]?.alias ?? "Minimax", + }; + + const providers = { ...cfg.models?.providers }; + const hostedModel = { + id: MINIMAX_HOSTED_MODEL_ID, + name: "MiniMax M2.1", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: DEFAULT_MINIMAX_CONTEXT_WINDOW, + maxTokens: DEFAULT_MINIMAX_MAX_TOKENS, + }; + const existingProvider = providers.minimax; + const existingModels = Array.isArray(existingProvider?.models) + ? existingProvider.models + : []; + const hasHostedModel = existingModels.some( + (model) => model.id === MINIMAX_HOSTED_MODEL_ID, + ); + const mergedModels = hasHostedModel + ? existingModels + : [...existingModels, hostedModel]; + providers.minimax = { + ...existingProvider, + baseUrl: params?.baseUrl?.trim() || DEFAULT_MINIMAX_BASE_URL, + apiKey: "minimax", + api: "openai-completions", + models: mergedModels.length > 0 ? mergedModels : [hostedModel], + }; + + return { + ...cfg, + agent: { + ...cfg.agent, + models, + }, + models: { + mode: cfg.models?.mode ?? "merge", + providers, + }, + }; +} + export function applyMinimaxConfig(cfg: ClawdbotConfig): ClawdbotConfig { const next = applyMinimaxProviderConfig(cfg); return { @@ -162,3 +232,26 @@ export function applyMinimaxConfig(cfg: ClawdbotConfig): ClawdbotConfig { }, }; } + +export function applyMinimaxHostedConfig( + cfg: ClawdbotConfig, + params?: { baseUrl?: string }, +): ClawdbotConfig { + const next = applyMinimaxHostedProviderConfig(cfg, params); + return { + ...next, + agent: { + ...next.agent, + model: { + ...(next.agent?.model && + "fallbacks" in (next.agent.model as Record) + ? { + fallbacks: (next.agent.model as { fallbacks?: string[] }) + .fallbacks, + } + : undefined), + primary: MINIMAX_HOSTED_MODEL_REF, + }, + }, + }; +} diff --git a/src/commands/onboard-non-interactive.ts b/src/commands/onboard-non-interactive.ts index 1563e090d..f4dceef79 100644 --- a/src/commands/onboard-non-interactive.ts +++ b/src/commands/onboard-non-interactive.ts @@ -30,8 +30,10 @@ import { healthCommand } from "./health.js"; import { applyAuthProfileConfig, applyMinimaxConfig, + applyMinimaxHostedConfig, setAnthropicApiKey, setGeminiApiKey, + setMinimaxApiKey, } from "./onboard-auth.js"; import { applyWizardMetadata, @@ -150,6 +152,20 @@ export async function runNonInteractiveOnboarding( }); process.env.OPENAI_API_KEY = key; runtime.log(`Saved OPENAI_API_KEY to ${result.path}`); + } else if (authChoice === "minimax-cloud") { + const key = opts.minimaxApiKey?.trim(); + if (!key) { + runtime.error("Missing --minimax-api-key"); + runtime.exit(1); + return; + } + await setMinimaxApiKey(key); + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "minimax:default", + provider: "minimax", + mode: "api_key", + }); + nextConfig = applyMinimaxHostedConfig(nextConfig); } else if (authChoice === "claude-cli") { const store = ensureAuthProfileStore(undefined, { allowKeychainPrompt: false, diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index 3f84dfaf4..3ebbe85a9 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -12,6 +12,7 @@ export type AuthChoice = | "antigravity" | "apiKey" | "gemini-api-key" + | "minimax-cloud" | "minimax" | "skip"; export type GatewayAuthChoice = "off" | "token" | "password"; @@ -29,6 +30,7 @@ export type OnboardOptions = { anthropicApiKey?: string; openaiApiKey?: string; geminiApiKey?: string; + minimaxApiKey?: string; gatewayPort?: number; gatewayBind?: GatewayBind; gatewayAuth?: GatewayAuthChoice;