// Commander registration for channel discovery, setup, status, auth, and diagnostics commands. import { Option, type Command } from "commander"; import { formatDocsLink } from "../../packages/terminal-core/src/links.js"; import { theme } from "../../packages/terminal-core/src/theme.js"; import { parseAccountSelector } from "../commands/channels/account-selector.js"; import { danger } from "../globals.js"; import { formatErrorMessage } from "../infra/errors.js"; import { defaultRuntime } from "../runtime.js"; import { createLazyPromise } from "../shared/lazy-promise.js"; import { resolveCliArgvInvocation } from "./argv-invocation.js"; import { runChannelLogin, runChannelLogout } from "./channel-auth.js"; import { formatCliChannelOptions } from "./channel-options.js"; import { getChannelSetupOptionSwitches, loadChannelSetupCliOptions, resolveChannelsAddChannelFromArgv, resolveChannelsAddOptions, type ChannelSetupCliOption, } from "./channels-cli-add-args.js"; import { runCommandWithRuntime } from "./cli-utils.js"; import { hasExplicitOptions, inheritOptionFromParent } from "./command-options.js"; import { formatHelpExamples } from "./help-format.js"; import { applyParentDefaultHelpAction } from "./program/parent-default-help.js"; import { normalizeWindowsArgv } from "./windows-argv.js"; const optionNamesRemove = ["channel", "account", "delete"] as const; const CHANNEL_ADD_SELECTION_OPTION_NAMES = new Set(["agent", "channel"]); type RegisterChannelsCliOptions = { includeSetupOptions?: boolean; }; type AddChannelSetupOptionsParams = { channelId?: string; includeAll?: boolean; }; type ChannelSetupOptionMode = "none" | "modern" | "legacy"; type ChannelSetupOptionRegistration = { mode: ChannelSetupOptionMode; legacyIntDefaultAttributeNames: ReadonlySet; }; const LEGACY_CHANNEL_SETUP_OPTIONS: readonly ChannelSetupCliOption[] = [ { flags: "--token ", description: "Channel token or credential payload" }, { flags: "--token-file ", description: "Read channel token or credential payload from file", }, { flags: "--secret ", description: "Channel shared secret" }, { flags: "--bot-token ", description: "Bot token" }, { flags: "--app-token ", description: "App token" }, { flags: "--password ", description: "Channel password or login secret" }, { flags: "--cli-path ", description: "Channel CLI path" }, { flags: "--url ", description: "Channel setup URL" }, { flags: "--base-url ", description: "Channel base URL" }, { flags: "--http-url ", description: "Channel HTTP service URL" }, { flags: "--auth-dir ", description: "Channel auth directory override" }, { flags: "--use-env", description: "Use env-backed credentials when supported", defaultValue: false, }, ]; const loadChannelsCommands = createLazyPromise(() => import("../commands/channels.js")); function runChannelsCommand(action: () => Promise) { return runCommandWithRuntime(defaultRuntime, action); } function runChannelsCommandWithDanger(action: () => Promise, label: string) { return runCommandWithRuntime(defaultRuntime, action, (err) => { defaultRuntime.error(danger(`${label}: ${formatErrorMessage(err)}`)); defaultRuntime.exit(1); }); } function getOptionNames(command: Command): string[] { return command.options.map((option) => option.attributeName()); } function resolveStringOption(command: Command, name: string): string | undefined { const source = command.getOptionValueSource(name); const localValue = command.getOptionValue(name); const value = source && source !== "default" ? localValue : (inheritOptionFromParent(command, name) ?? localValue); return typeof value === "string" ? value : undefined; } function addChannelSetupOption( command: Command, option: ChannelSetupCliOption, seenFlags: Set, ): void { const prepared = command.createOption(option.flags, option.description); const optionSwitches = getChannelSetupOptionSwitches(prepared); if (optionSwitches.some((flag) => seenFlags.has(flag))) { return; } optionSwitches.forEach((flag) => seenFlags.add(flag)); command.addOption(prepared.makeOptionMandatory(false).default(option.defaultValue)); if (option.negatedFlags) { const negated = command.createOption(option.negatedFlags, option.description); const negatedSwitches = getChannelSetupOptionSwitches(negated); if (!negatedSwitches.some((flag) => seenFlags.has(flag))) { negatedSwitches.forEach((flag) => seenFlags.add(flag)); command.addOption(negated.makeOptionMandatory(false).default(undefined)); } } } function shouldRegisterChannelSetupOptions( argv: string[] = process.argv, options: RegisterChannelsCliOptions = {}, ): boolean { // Channel-specific setup flags are expensive to load and only needed on `channels add`. if (options.includeSetupOptions) { return true; } const { commandPath } = resolveCliArgvInvocation(normalizeWindowsArgv(argv)); return commandPath[0] === "channels" && commandPath[1] === "add"; } async function addChannelSetupOptions( command: Command, params: AddChannelSetupOptionsParams = {}, ): Promise { const { resolveChannelSetupCliOptionMetadata } = await loadChannelSetupCliOptions(); const selected = params.channelId?.trim().toLowerCase(); const { options, selectedChannel, valueMetadataByAttributeName } = resolveChannelSetupCliOptionMetadata(selected, { includeAll: params.includeAll, }); const mode: ChannelSetupOptionMode = selected ? selectedChannel?.setup ? "modern" : "legacy" : "none"; const seenFlags = new Set(command.options.flatMap(getChannelSetupOptionSwitches)); for (const option of options) { addChannelSetupOption(command, option, seenFlags); } if ( params.includeAll || (mode === "legacy" && (selectedChannel === undefined || selectedChannel.setup === undefined)) ) { for (const option of LEGACY_CHANNEL_SETUP_OPTIONS) { addChannelSetupOption(command, option, seenFlags); } } const legacyIntDefaultAttributeNames = new Set(); for (const [attributeName, metadata] of valueMetadataByAttributeName) { if (metadata.valueType === "int") { legacyIntDefaultAttributeNames.add(attributeName); } } return { mode, legacyIntDefaultAttributeNames }; } export async function registerChannelsCli( program: Command, argv: string[] = process.argv, options: RegisterChannelsCliOptions = {}, ) { const channelNames = formatCliChannelOptions(); const channels = program .command("channels") .description("Manage connected chat channels and accounts") .option("--agent ", "Agent owner for channel commands that require workspace context") .addHelpText( "after", () => `\n${theme.heading("Examples:")}\n${formatHelpExamples([ ["openclaw channels list", "List configured channels."], ["openclaw channels list --all", "Show configured, bundled, and installable channels."], ["openclaw channels add", "Open guided channel setup."], ["openclaw channels status --probe", "Run channel status checks and probes."], [ "openclaw channels add --channel telegram --token ", "Add or update a channel account non-interactively.", ], ["openclaw channels login --channel whatsapp", "Link a WhatsApp Web account."], ])}\n\n${theme.muted("Docs:")} ${formatDocsLink( "/cli/channels", "docs.openclaw.ai/cli/channels", )}\n`, ); channels .command("list") .description("List chat channels (configured by default; pass --all for installable catalog)") .option("--all", "Include bundled and installable catalog channels", false) .option("--json", "Output JSON", false) .action(async (opts) => { await runChannelsCommand(async () => { const { channelsListCommand } = await import("../commands/channels/list.js"); await channelsListCommand(opts, defaultRuntime); }); }); channels .command("status") .description("Show channel status (use openclaw status --deep for a full connection check)") .option("--channel ", `Only show one channel (${formatCliChannelOptions(["all"])})`) .option("--probe", "Probe channel credentials", false) .option("--timeout ", "Timeout in ms", "10000") .option("--json", "Output JSON", false) .action(async (opts) => { await runChannelsCommand(async () => { const { channelsStatusCommand } = await import("../commands/channels/status.js"); await channelsStatusCommand(opts, defaultRuntime); }); }); channels .command("capabilities") .description("Show provider capabilities (intents/scopes + supported features)") .option("--agent ", "Agent owner for channel discovery") .option("--channel ", `Channel (${formatCliChannelOptions(["all"])})`) .option("--account ", "Account id (only with --channel)", parseAccountSelector) .option("--target ", "Channel target for permission audit (Discord channel:)") .option("--timeout ", "Timeout in ms", "10000") .option("--json", "Output JSON", false) .action(async (opts, command) => { await runChannelsCommand(async () => { const { channelsCapabilitiesCommand } = await loadChannelsCommands(); await channelsCapabilitiesCommand( { ...opts, agent: resolveStringOption(command, "agent") }, defaultRuntime, ); }); }); channels .command("resolve") .description("Resolve channel/user names to IDs") .argument("", "Entries to resolve (names or ids)") .option("--channel ", `Channel (${channelNames})`) .option("--account ", "Account id (accountId)", parseAccountSelector) .option("--agent ", "Agent owner for channel resolution") .addOption( new Option("--kind ", "Target kind (auto|user|group|channel)") .choices(["auto", "user", "group", "channel"]) .default("auto"), ) .option("--json", "Output JSON", false) .action(async (entries, opts, command) => { await runChannelsCommand(async () => { const { channelsResolveCommand } = await loadChannelsCommands(); await channelsResolveCommand( { agent: resolveStringOption(command, "agent"), channel: opts.channel as string | undefined, account: opts.account as string | undefined, kind: opts.kind as "auto" | "user" | "group" | "channel", json: Boolean(opts.json), entries: Array.isArray(entries) ? entries : [String(entries)], }, defaultRuntime, ); }); }); channels .command("logs") .description("Show recent channel logs from the gateway log file") .option("--channel ", `Channel (${formatCliChannelOptions(["all"])}; default: all)`) .option("--lines ", "Number of lines (default: 200)", "200") .option("--json", "Output JSON", false) .action(async (opts) => { await runChannelsCommand(async () => { const { channelsLogsCommand } = await loadChannelsCommands(); await channelsLogsCommand(opts, defaultRuntime); }); }); const deadLetters = channels .command("dead-letters") .description("Inspect and resubmit failed inbound channel events") .option("--account ", "Account id", "default"); deadLetters .command("list") .description("List failed inbound events for one channel account") .requiredOption("--channel ", "Channel id") .option("--account ", "Account id", "default") .option("--limit ", "Maximum entries", "100") .option("--json", "Output JSON", false) .action(async (opts, command) => { await runChannelsCommand(async () => { const { channelsDeadLettersListCommand } = await import("../commands/channels/dead-letters.js"); await channelsDeadLettersListCommand( { ...opts, account: resolveStringOption(command, "account") }, defaultRuntime, ); }); }); deadLetters .command("resubmit") .description("Re-enqueue one failed inbound event") .argument("", "Ingress event id") .requiredOption("--channel ", "Channel id") .option("--account ", "Account id", "default") .option("--json", "Output JSON", false) .action(async (eventId, opts, command) => { await runChannelsCommand(async () => { const { channelsDeadLettersResubmitCommand } = await import("../commands/channels/dead-letters.js"); await channelsDeadLettersResubmitCommand( eventId, { ...opts, account: resolveStringOption(command, "account") }, defaultRuntime, ); }); }); applyParentDefaultHelpAction(deadLetters); const addCommand = channels .command("add") .description("Add or update a channel account") .argument("[channel]", "Channel id") .addHelpText( "after", () => `\n${theme.heading("Examples:")}\n${formatHelpExamples([ ["openclaw channels add", "Open guided setup for available chat channels."], [ "openclaw channels add --channel telegram --token ", "Add or update Telegram non-interactively.", ], ["openclaw channels list --all", "Find channel ids before using --channel."], ])}\n`, ) .option("--channel ", `Channel (${channelNames})`) .option("--agent ", "Agent owner for channel setup") .option("--account ", "Account id (default when omitted)") .option("--name ", "Display name for this account"); let channelSetupRegistration: ChannelSetupOptionRegistration = { mode: "none", legacyIntDefaultAttributeNames: new Set(), }; const selectedChannelId = await resolveChannelsAddChannelFromArgv(argv); if ( shouldRegisterChannelSetupOptions(argv, options) && (selectedChannelId !== undefined || options.includeSetupOptions) ) { channelSetupRegistration = await addChannelSetupOptions(addCommand, { channelId: selectedChannelId, includeAll: options.includeSetupOptions, }); } addCommand.action(async (channelArg: string | undefined, opts, command) => { await runChannelsCommand(async () => { const { channelsAddCommand } = await loadChannelsCommands(); const hasFlags = hasExplicitOptions( command, getOptionNames(command).filter((name) => !CHANNEL_ADD_SELECTION_OPTION_NAMES.has(name)), ); await channelsAddCommand( { ...resolveChannelsAddOptions( channelArg, opts, command, channelSetupRegistration.mode === "modern" ? undefined : { preserveLegacyDefaults: true, dropEmptyLegacyDefaultsForAttributeNames: channelSetupRegistration.legacyIntDefaultAttributeNames, }, ), agent: resolveStringOption(command, "agent"), }, defaultRuntime, { hasFlags, }, ); }); }); channels .command("remove") .description("Disable or delete a channel account") .option("--agent ", "Agent owner for channel discovery") .option("--channel ", `Channel (${channelNames})`) .option("--account ", "Account id (default when omitted)") .option("--delete", "Delete config entries (no prompt)", false) .action(async (opts, command) => { await runChannelsCommand(async () => { const { channelsRemoveCommand } = await loadChannelsCommands(); const hasFlags = hasExplicitOptions(command, optionNamesRemove); await channelsRemoveCommand( { ...opts, agent: resolveStringOption(command, "agent") }, defaultRuntime, { hasFlags }, ); }); }); for (const mode of ["login", "logout"] as const) { const auth = channels .command(mode) .description( mode === "login" ? "Link a channel account (if supported)" : "Log out of a channel session (if supported)", ) .option("--agent ", "Agent owner for channel discovery") .option("--channel ", "Channel alias (auto when only one is configured)") .option("--account ", "Account id (accountId)"); if (mode === "login") { auth.option("--verbose", "Verbose connection logs", false); } auth.action(async (opts, command) => { await runChannelsCommandWithDanger( () => (mode === "login" ? runChannelLogin : runChannelLogout)( { agent: resolveStringOption(command, "agent"), channel: opts.channel as string | undefined, account: opts.account as string | undefined, ...(mode === "login" ? { verbose: Boolean(opts.verbose) } : {}), }, defaultRuntime, ), `Channel ${mode} failed`, ); }); } applyParentDefaultHelpAction(channels); }