UNPKG

@gguf/claw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,363 lines (1,351 loc) 56 kB
import { At as getResolvedLoggerSettings, Dt as theme, Q as isRecord, _ as defaultRuntime, ht as danger, w as DEFAULT_CHAT_CHANNEL, xt as setVerbose } from "./entry.js"; import { x as loadAuthProfileStore } from "./auth-profiles-DFa1zzNy.js"; import { t as formatCliCommand } from "./command-format-D3syQOZg.js"; import { c as normalizeAccountId, t as DEFAULT_ACCOUNT_ID } from "./session-key-BGiG_JcT.js"; import "./exec-CBKBIMpA.js"; import { c as resolveAgentWorkspaceDir, l as resolveDefaultAgentId } from "./agent-scope-RzK9Zcks.js"; import "./github-copilot-token-DuFIqfeC.js"; import "./model-DY9t1aT6.js"; import "./pi-model-discovery-Do3xMEtM.js"; import "./frontmatter-D-YR-Ghi.js"; import "./skills-DJmGZazd.js"; import "./manifest-registry-DS2iK5AZ.js"; import { i as loadConfig, l as writeConfigFile, o as readConfigFileSnapshot } from "./config-B2kL1ciP.js"; import "./client-CDjZdZtI.js"; import { n as callGateway } from "./call-DXhJGwEy.js"; import "./message-channel-CVHJDItx.js"; import "./pairing-token-Byh6drgn.js"; import { Xt as formatUsageReportLines, Yt as loadProviderUsageSummary, _ as deleteTelegramUpdateOffset } from "./subagent-registry-C7Edpn23.js"; import "./sessions-BD5dyLxb.js"; import "./tokens-D5RzuaYP.js"; import { o as resolveTelegramAccount } from "./normalize-Db7Xtx2v.js"; import "./accounts-BpzwDfBB.js"; import "./bindings-KqaGKS1E.js"; import "./logging-CFvkxgcX.js"; import { i as createSlackWebClient } from "./send-BFiRlO6V.js"; import { n as listChannelPlugins, r as normalizeChannelId, t as getChannelPlugin } from "./plugins-RqhjLCb6.js"; import { Q as fetchChannelPermissionsDiscord, nt as parseDiscordTarget } from "./send-CWDtU8Gi.js"; import "./with-timeout-nVWy7PWz.js"; import "./deliver-CeVE1Jh-.js"; import "./diagnostic-774btyou.js"; import "./diagnostic-session-state-HO94DMou.js"; import "./accounts-B3D4iWUP.js"; import "./send-DCCBUsNn.js"; import "./image-ops-lDlFpoR2.js"; import "./pi-embedded-helpers-NVEtaJwl.js"; import "./sandbox-CO-R8v6J.js"; import "./common-DcSh1hZE.js"; import "./chrome-52ZF7gmE.js"; import "./tailscale-BzRVNhMW.js"; import "./auth-Dq2pFnjj.js"; import "./server-context-C1f7cijA.js"; import "./routes-BJmh1Ify.js"; import "./redact-C2s6sr73.js"; import "./errors-CFEPAPWc.js"; import "./fs-safe-3YwsxSr5.js"; import "./paths-CXKNzNjU.js"; import "./ssrf-Cv2DiHsm.js"; import "./store-e6fIrP17.js"; import "./ports-59j-bA53.js"; import "./trash-DVLJ0k6q.js"; import "./dock-BZuwgj1O.js"; import "./accounts-Jg3_1Y-r.js"; import "./paths-CXpciDEv.js"; import "./thinking-CJPPUYWd.js"; import "./models-config-5zJ39mUb.js"; import "./reply-prefix-DJOqzjBt.js"; import "./memory-cli-C3uas9sI.js"; import "./manager-D3ZmOwqt.js"; import "./gemini-auth-CdgAkz2K.js"; import "./sqlite-1-TCahE6.js"; import "./retry-BcjnIuo-.js"; import "./chunk-Z2NYDchs.js"; import "./markdown-tables-GCHx-nGT.js"; import "./fetch-guard-B5BAqVyD.js"; import "./local-roots-SkDT1Wv8.js"; import "./ir-DAwVi0a7.js"; import "./render-e7fENCYH.js"; import "./commands-registry-CRmMPJQ9.js"; import "./image-QGUQCdGh.js"; import "./tool-display-Cs1uRaRV.js"; import { n as formatTimeAgo } from "./format-relative-TyajjYxu.js"; import "./runner-D81xREgc.js"; import "./model-catalog-DLoDxnxL.js"; import "./session-utils-C3Oi9cXA.js"; import "./skill-commands-D2rmj6w8.js"; import "./workspace-dirs-BXftTDSV.js"; import "./pairing-store-CruPwgBw.js"; import "./fetch-vg2oFVIH.js"; import "./exec-approvals-BZA6z4HM.js"; import "./nodes-screen-x7YKy8Ay.js"; import "./session-cost-usage-CpxG9Mup.js"; import "./pi-tools.policy-DeRwGLEO.js"; import "./control-service-DGleRjGJ.js"; import "./stagger-CArN2YVJ.js"; import { n as resolveMessageChannelSelection } from "./channel-selection-DoXlsBhe.js"; import "./send-C3RIrBTr.js"; import "./outbound-attachment-CdFWGHgJ.js"; import "./delivery-queue-DKdWlR61.js"; import "./send-0Nz3O9Jc.js"; import "./resolve-route-BONxzeyg.js"; import "./channel-activity-BYGpAtHP.js"; import "./tables-BrqD0SUa.js"; import "./proxy-DL3MD6-P.js"; import { t as formatDocsLink } from "./links-CW8Bx7rK.js"; import { n as runCommandWithRuntime } from "./cli-utils-CCaEbxAz.js"; import { t as formatHelpExamples } from "./help-format-B0pWGnZs.js"; import { n as withProgress } from "./progress-BAHiAaDW.js"; import "./replies-CuUAAFF2.js"; import "./onboard-helpers-CEx2tGVB.js"; import "./prompt-style-DwCXob2h.js"; import "./pairing-labels-DtxjElSq.js"; import { t as hasExplicitOptions } from "./command-options-CRqZtG5w.js"; import { r as listChannelPluginCatalogEntries } from "./catalog-vmrRUSxJ.js"; import "./note-D3Xn5qjj.js"; import { t as createClackPrompter } from "./clack-prompter-DOOcFy2t.js"; import { t as resolveChannelDefaultAccountId } from "./helpers-UPrYYVP7.js"; import "./plugin-auto-enable-DR5UZ2Ky.js"; import "./install-safe-path-CnPOnUC0.js"; import "./npm-registry-spec-Dx1jSlNK.js"; import "./skill-scanner-BlmeBzA8.js"; import "./installs-Bfs-7l-S.js"; import { a as reloadOnboardingPluginRegistry, i as ensureOnboardingPluginInstalled, r as setupChannels } from "./onboard-channels-CBn2fwj1.js"; import { t as requireValidConfigSnapshot } from "./config-validation-CBZ_NgG2.js"; import { t as buildChannelAccountSnapshot } from "./status-hXJX3_FZ.js"; import { t as parseLogLine } from "./parse-log-line-mmAgu0Es.js"; import { t as collectChannelStatusIssues } from "./channels-status-issues-DgVhydeW.js"; import "./plugin-registry-CXQ59uvq.js"; import { t as formatCliChannelOptions } from "./channel-options-RMlqt6L8.js"; import fs from "node:fs/promises"; //#region src/commands/channels/add-mutators.ts function applyAccountName(params) { const accountId = normalizeAccountId(params.accountId); const apply = getChannelPlugin(params.channel)?.setup?.applyAccountName; return apply ? apply({ cfg: params.cfg, accountId, name: params.name }) : params.cfg; } function applyChannelAccountConfig(params) { const accountId = normalizeAccountId(params.accountId); const apply = getChannelPlugin(params.channel)?.setup?.applyAccountConfig; if (!apply) return params.cfg; return apply({ cfg: params.cfg, accountId, input: params.input }); } //#endregion //#region src/commands/channels/shared.ts async function requireValidConfig(runtime = defaultRuntime) { return await requireValidConfigSnapshot(runtime); } function formatAccountLabel(params) { const base = params.accountId || DEFAULT_ACCOUNT_ID; if (params.name?.trim()) return `${base} (${params.name.trim()})`; return base; } const channelLabel = (channel) => { return getChannelPlugin(channel)?.meta.label ?? channel; }; function formatChannelAccountLabel(params) { const channelText = channelLabel(params.channel); const accountText = formatAccountLabel({ accountId: params.accountId, name: params.name }); return `${params.channelStyle ? params.channelStyle(channelText) : channelText} ${params.accountStyle ? params.accountStyle(accountText) : accountText}`; } function shouldUseWizard(params) { return params?.hasFlags === false; } //#endregion //#region src/commands/channels/add.ts function parseList(value) { if (!value?.trim()) return; const parsed = value.split(/[\n,;]+/g).map((entry) => entry.trim()).filter(Boolean); return parsed.length > 0 ? parsed : void 0; } function resolveCatalogChannelEntry(raw, cfg) { const trimmed = raw.trim().toLowerCase(); if (!trimmed) return; return listChannelPluginCatalogEntries({ workspaceDir: cfg ? resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg)) : void 0 }).find((entry) => { if (entry.id.toLowerCase() === trimmed) return true; return (entry.meta.aliases ?? []).some((alias) => alias.trim().toLowerCase() === trimmed); }); } async function channelsAddCommand(opts, runtime = defaultRuntime, params) { const cfg = await requireValidConfig(runtime); if (!cfg) return; let nextConfig = cfg; if (shouldUseWizard(params)) { const prompter = createClackPrompter(); let selection = []; const accountIds = {}; await prompter.intro("Channel setup"); let nextConfig = await setupChannels(cfg, runtime, prompter, { allowDisable: false, allowSignalInstall: true, promptAccountIds: true, onSelection: (value) => { selection = value; }, onAccountId: (channel, accountId) => { accountIds[channel] = accountId; } }); if (selection.length === 0) { await prompter.outro("No channels selected."); return; } if (await prompter.confirm({ message: "Add display names for these accounts? (optional)", initialValue: false })) for (const channel of selection) { const accountId = accountIds[channel] ?? DEFAULT_ACCOUNT_ID; const plugin = getChannelPlugin(channel); const account = plugin?.config.resolveAccount(nextConfig, accountId); const existingName = (plugin?.config.describeAccount?.(account, nextConfig))?.name ?? account?.name; const name = await prompter.text({ message: `${channel} account name (${accountId})`, initialValue: existingName }); if (name?.trim()) nextConfig = applyAccountName({ cfg: nextConfig, channel, accountId, name }); } await writeConfigFile(nextConfig); await prompter.outro("Channels updated."); return; } const rawChannel = String(opts.channel ?? ""); let channel = normalizeChannelId(rawChannel); let catalogEntry = channel ? void 0 : resolveCatalogChannelEntry(rawChannel, nextConfig); if (!channel && catalogEntry) { const prompter = createClackPrompter(); const workspaceDir = resolveAgentWorkspaceDir(nextConfig, resolveDefaultAgentId(nextConfig)); const result = await ensureOnboardingPluginInstalled({ cfg: nextConfig, entry: catalogEntry, prompter, runtime, workspaceDir }); nextConfig = result.cfg; if (!result.installed) return; reloadOnboardingPluginRegistry({ cfg: nextConfig, runtime, workspaceDir }); channel = normalizeChannelId(catalogEntry.id) ?? catalogEntry.id; } if (!channel) { const hint = catalogEntry ? `Plugin ${catalogEntry.meta.label} could not be loaded after install.` : `Unknown channel: ${String(opts.channel ?? "")}`; runtime.error(hint); runtime.exit(1); return; } const plugin = getChannelPlugin(channel); if (!plugin?.setup?.applyAccountConfig) { runtime.error(`Channel ${channel} does not support add.`); runtime.exit(1); return; } const accountId = plugin.setup.resolveAccountId?.({ cfg: nextConfig, accountId: opts.account }) ?? normalizeAccountId(opts.account); const useEnv = opts.useEnv === true; const initialSyncLimit = typeof opts.initialSyncLimit === "number" ? opts.initialSyncLimit : typeof opts.initialSyncLimit === "string" && opts.initialSyncLimit.trim() ? Number.parseInt(opts.initialSyncLimit, 10) : void 0; const groupChannels = parseList(opts.groupChannels); const dmAllowlist = parseList(opts.dmAllowlist); const input = { name: opts.name, token: opts.token, tokenFile: opts.tokenFile, botToken: opts.botToken, appToken: opts.appToken, signalNumber: opts.signalNumber, cliPath: opts.cliPath, dbPath: opts.dbPath, service: opts.service, region: opts.region, authDir: opts.authDir, httpUrl: opts.httpUrl, httpHost: opts.httpHost, httpPort: opts.httpPort, webhookPath: opts.webhookPath, webhookUrl: opts.webhookUrl, audienceType: opts.audienceType, audience: opts.audience, homeserver: opts.homeserver, userId: opts.userId, accessToken: opts.accessToken, password: opts.password, deviceName: opts.deviceName, initialSyncLimit, useEnv, ship: opts.ship, url: opts.url, code: opts.code, groupChannels, dmAllowlist, autoDiscoverChannels: opts.autoDiscoverChannels }; const validationError = plugin.setup.validateInput?.({ cfg: nextConfig, accountId, input }); if (validationError) { runtime.error(validationError); runtime.exit(1); return; } const previousTelegramToken = channel === "telegram" ? resolveTelegramAccount({ cfg: nextConfig, accountId }).token.trim() : ""; nextConfig = applyChannelAccountConfig({ cfg: nextConfig, channel, accountId, input }); if (channel === "telegram") { if (previousTelegramToken !== resolveTelegramAccount({ cfg: nextConfig, accountId }).token.trim()) await deleteTelegramUpdateOffset({ accountId }); } await writeConfigFile(nextConfig); runtime.log(`Added ${channelLabel(channel)} account "${accountId}".`); } //#endregion //#region src/slack/scopes.ts function collectScopes(value, into) { if (!value) return; if (Array.isArray(value)) { for (const entry of value) if (typeof entry === "string" && entry.trim()) into.push(entry.trim()); return; } if (typeof value === "string") { const raw = value.trim(); if (!raw) return; const parts = raw.split(/[,\s]+/).map((part) => part.trim()); for (const part of parts) if (part) into.push(part); return; } if (!isRecord(value)) return; for (const entry of Object.values(value)) if (Array.isArray(entry) || typeof entry === "string") collectScopes(entry, into); } function normalizeScopes(scopes) { return Array.from(new Set(scopes.map((scope) => scope.trim()).filter(Boolean))).toSorted(); } function extractScopes(payload) { if (!isRecord(payload)) return []; const scopes = []; collectScopes(payload.scopes, scopes); collectScopes(payload.scope, scopes); if (isRecord(payload.info)) { collectScopes(payload.info.scopes, scopes); collectScopes(payload.info.scope, scopes); collectScopes(payload.info.user_scopes, scopes); collectScopes(payload.info.bot_scopes, scopes); } return normalizeScopes(scopes); } function readError(payload) { if (!isRecord(payload)) return; const error = payload.error; return typeof error === "string" && error.trim() ? error.trim() : void 0; } async function callSlack(client, method) { try { const result = await client.apiCall(method); return isRecord(result) ? result : null; } catch (err) { return { ok: false, error: err instanceof Error ? err.message : String(err) }; } } async function fetchSlackScopes(token, timeoutMs) { const client = createSlackWebClient(token, { timeout: timeoutMs }); const attempts = ["auth.scopes", "apps.permissions.info"]; const errors = []; for (const method of attempts) { const result = await callSlack(client, method); const scopes = extractScopes(result); if (scopes.length > 0) return { ok: true, scopes, source: method }; const error = readError(result); if (error) errors.push(`${method}: ${error}`); } return { ok: false, error: errors.length > 0 ? errors.join(" | ") : "no scopes returned" }; } //#endregion //#region src/commands/channels/capabilities.ts const REQUIRED_DISCORD_PERMISSIONS = ["ViewChannel", "SendMessages"]; const TEAMS_GRAPH_PERMISSION_HINTS = { "ChannelMessage.Read.All": "channel history", "Chat.Read.All": "chat history", "Channel.ReadBasic.All": "channel list", "Team.ReadBasic.All": "team list", "TeamsActivity.Read.All": "teams activity", "Sites.Read.All": "files (SharePoint)", "Files.Read.All": "files (OneDrive)" }; function normalizeTimeout(raw, fallback = 1e4) { const value = typeof raw === "string" ? Number(raw) : Number(raw); if (!Number.isFinite(value) || value <= 0) return fallback; return value; } function formatSupport(capabilities) { if (!capabilities) return "unknown"; const bits = []; if (capabilities.chatTypes?.length) bits.push(`chatTypes=${capabilities.chatTypes.join(",")}`); if (capabilities.polls) bits.push("polls"); if (capabilities.reactions) bits.push("reactions"); if (capabilities.edit) bits.push("edit"); if (capabilities.unsend) bits.push("unsend"); if (capabilities.reply) bits.push("reply"); if (capabilities.effects) bits.push("effects"); if (capabilities.groupManagement) bits.push("groupManagement"); if (capabilities.threads) bits.push("threads"); if (capabilities.media) bits.push("media"); if (capabilities.nativeCommands) bits.push("nativeCommands"); if (capabilities.blockStreaming) bits.push("blockStreaming"); return bits.length ? bits.join(" ") : "none"; } function summarizeDiscordTarget(raw) { if (!raw) return; const target = parseDiscordTarget(raw, { defaultKind: "channel" }); if (!target) return { raw }; if (target.kind === "channel") return { raw, normalized: target.normalized, kind: "channel", channelId: target.id }; if (target.kind === "user") return { raw, normalized: target.normalized, kind: "user" }; return { raw, normalized: target.normalized }; } function formatDiscordIntents(intents) { if (!intents) return "unknown"; return [ `messageContent=${intents.messageContent ?? "unknown"}`, `guildMembers=${intents.guildMembers ?? "unknown"}`, `presence=${intents.presence ?? "unknown"}` ].join(" "); } function formatProbeLines(channelId, probe) { const lines = []; if (!probe || typeof probe !== "object") return lines; const probeObj = probe; if (channelId === "discord") { const bot = probeObj.bot; if (bot?.username) { const botId = bot.id ? ` (${bot.id})` : ""; lines.push(`Bot: ${theme.accent(`@${bot.username}`)}${botId}`); } const app = probeObj.application; if (app?.intents) lines.push(`Intents: ${formatDiscordIntents(app.intents)}`); } if (channelId === "telegram") { const bot = probeObj.bot; if (bot?.username) { const botId = bot.id ? ` (${bot.id})` : ""; lines.push(`Bot: ${theme.accent(`@${bot.username}`)}${botId}`); } const flags = []; const canJoinGroups = bot?.canJoinGroups; const canReadAll = bot?.canReadAllGroupMessages; const inlineQueries = bot?.supportsInlineQueries; if (typeof canJoinGroups === "boolean") flags.push(`joinGroups=${canJoinGroups}`); if (typeof canReadAll === "boolean") flags.push(`readAllGroupMessages=${canReadAll}`); if (typeof inlineQueries === "boolean") flags.push(`inlineQueries=${inlineQueries}`); if (flags.length > 0) lines.push(`Flags: ${flags.join(" ")}`); const webhook = probeObj.webhook; if (webhook?.url !== void 0) lines.push(`Webhook: ${webhook.url || "none"}`); } if (channelId === "slack") { const bot = probeObj.bot; const team = probeObj.team; if (bot?.name) lines.push(`Bot: ${theme.accent(`@${bot.name}`)}`); if (team?.name || team?.id) { const id = team?.id ? ` (${team.id})` : ""; lines.push(`Team: ${team?.name ?? "unknown"}${id}`); } } if (channelId === "signal") { const version = probeObj.version; if (version) lines.push(`Signal daemon: ${version}`); } if (channelId === "msteams") { const appId = typeof probeObj.appId === "string" ? probeObj.appId.trim() : ""; if (appId) lines.push(`App: ${theme.accent(appId)}`); const graph = probeObj.graph; if (graph) { const roles = Array.isArray(graph.roles) ? graph.roles.map((role) => String(role).trim()).filter(Boolean) : []; const scopes = typeof graph.scopes === "string" ? graph.scopes.split(/\s+/).map((scope) => scope.trim()).filter(Boolean) : Array.isArray(graph.scopes) ? graph.scopes.map((scope) => String(scope).trim()).filter(Boolean) : []; if (graph.ok === false) lines.push(`Graph: ${theme.error(graph.error ?? "failed")}`); else if (roles.length > 0 || scopes.length > 0) { const formatPermission = (permission) => { const hint = TEAMS_GRAPH_PERMISSION_HINTS[permission]; return hint ? `${permission} (${hint})` : permission; }; if (roles.length > 0) lines.push(`Graph roles: ${roles.map(formatPermission).join(", ")}`); if (scopes.length > 0) lines.push(`Graph scopes: ${scopes.map(formatPermission).join(", ")}`); } else if (graph.ok === true) lines.push("Graph: ok"); } } const ok = typeof probeObj.ok === "boolean" ? probeObj.ok : void 0; if (ok === true && lines.length === 0) lines.push("Probe: ok"); if (ok === false) { const error = typeof probeObj.error === "string" && probeObj.error ? ` (${probeObj.error})` : ""; lines.push(`Probe: ${theme.error(`failed${error}`)}`); } return lines; } async function buildDiscordPermissions(params) { const target = summarizeDiscordTarget(params.target?.trim()); if (!target) return {}; if (target.kind !== "channel" || !target.channelId) return { target, report: { error: "Target looks like a DM user; pass channel:<id> to audit channel permissions." } }; const token = params.account.token?.trim(); if (!token) return { target, report: { channelId: target.channelId, error: "Discord bot token missing for permission audit." } }; try { const perms = await fetchChannelPermissionsDiscord(target.channelId, { token, accountId: params.account.accountId ?? void 0 }); const missing = REQUIRED_DISCORD_PERMISSIONS.filter((permission) => !perms.permissions.includes(permission)); return { target, report: { channelId: perms.channelId, guildId: perms.guildId, isDm: perms.isDm, channelType: perms.channelType, permissions: perms.permissions, missingRequired: missing.length ? missing : [], raw: perms.raw } }; } catch (err) { return { target, report: { channelId: target.channelId, error: err instanceof Error ? err.message : String(err) } }; } } async function resolveChannelReports(params) { const { plugin, cfg, timeoutMs } = params; const accountIds = params.accountOverride ? [params.accountOverride] : (() => { const ids = plugin.config.listAccountIds(cfg); return ids.length > 0 ? ids : [resolveChannelDefaultAccountId({ plugin, cfg, accountIds: ids })]; })(); const reports = []; const listedActions = plugin.actions?.listActions?.({ cfg }) ?? []; const actions = Array.from(new Set([ "send", "broadcast", ...listedActions.map((action) => String(action)) ])); for (const accountId of accountIds) { const resolvedAccount = plugin.config.resolveAccount(cfg, accountId); const configured = plugin.config.isConfigured ? await plugin.config.isConfigured(resolvedAccount, cfg) : Boolean(resolvedAccount); const enabled = plugin.config.isEnabled ? plugin.config.isEnabled(resolvedAccount, cfg) : resolvedAccount.enabled !== false; let probe; if (configured && enabled && plugin.status?.probeAccount) try { probe = await plugin.status.probeAccount({ account: resolvedAccount, timeoutMs, cfg }); } catch (err) { probe = { ok: false, error: err instanceof Error ? err.message : String(err) }; } let slackScopes; if (plugin.id === "slack" && configured && enabled) { const botToken = resolvedAccount.botToken?.trim(); const userToken = resolvedAccount.config?.userToken?.trim(); const scopeReports = []; if (botToken) scopeReports.push({ tokenType: "bot", result: await fetchSlackScopes(botToken, timeoutMs) }); else scopeReports.push({ tokenType: "bot", result: { ok: false, error: "Slack bot token missing." } }); if (userToken) scopeReports.push({ tokenType: "user", result: await fetchSlackScopes(userToken, timeoutMs) }); slackScopes = scopeReports; } let discordTarget; let discordPermissions; if (plugin.id === "discord" && params.target) { const perms = await buildDiscordPermissions({ account: resolvedAccount, target: params.target }); discordTarget = perms.target; discordPermissions = perms.report; } reports.push({ channel: plugin.id, accountId, accountName: typeof resolvedAccount.name === "string" ? resolvedAccount.name?.trim() || void 0 : void 0, configured, enabled, support: plugin.capabilities, probe, target: discordTarget, channelPermissions: discordPermissions, actions, slackScopes }); } return reports; } async function channelsCapabilitiesCommand(opts, runtime = defaultRuntime) { const cfg = await requireValidConfig(runtime); if (!cfg) return; const timeoutMs = normalizeTimeout(opts.timeout, 1e4); const rawChannel = typeof opts.channel === "string" ? opts.channel.trim().toLowerCase() : ""; const rawTarget = typeof opts.target === "string" ? opts.target.trim() : ""; if (opts.account && (!rawChannel || rawChannel === "all")) { runtime.error(danger("--account requires a specific --channel.")); runtime.exit(1); return; } if (rawTarget && rawChannel !== "discord") { runtime.error(danger("--target requires --channel discord.")); runtime.exit(1); return; } const plugins = listChannelPlugins(); const selected = !rawChannel || rawChannel === "all" ? plugins : (() => { const plugin = getChannelPlugin(rawChannel); if (!plugin) return null; return [plugin]; })(); if (!selected || selected.length === 0) { runtime.error(danger(`Unknown channel "${rawChannel}".`)); runtime.exit(1); return; } const reports = []; for (const plugin of selected) { const accountOverride = opts.account?.trim() || void 0; reports.push(...await resolveChannelReports({ plugin, cfg, timeoutMs, accountOverride, target: rawTarget && plugin.id === "discord" ? rawTarget : void 0 })); } if (opts.json) { runtime.log(JSON.stringify({ channels: reports }, null, 2)); return; } const lines = []; for (const report of reports) { const label = formatChannelAccountLabel({ channel: report.channel, accountId: report.accountId, name: report.accountName, channelStyle: theme.accent, accountStyle: theme.heading }); lines.push(theme.heading(label)); lines.push(`Support: ${formatSupport(report.support)}`); if (report.actions && report.actions.length > 0) lines.push(`Actions: ${report.actions.join(", ")}`); if (report.configured === false || report.enabled === false) { const configuredLabel = report.configured === false ? "not configured" : "configured"; const enabledLabel = report.enabled === false ? "disabled" : "enabled"; lines.push(`Status: ${configuredLabel}, ${enabledLabel}`); } const probeLines = formatProbeLines(report.channel, report.probe); if (probeLines.length > 0) lines.push(...probeLines); else if (report.configured && report.enabled) lines.push(theme.muted("Probe: unavailable")); if (report.channel === "slack" && report.slackScopes) for (const entry of report.slackScopes) { const source = entry.result.source ? ` (${entry.result.source})` : ""; const label = entry.tokenType === "user" ? "User scopes" : "Bot scopes"; if (entry.result.ok && entry.result.scopes?.length) lines.push(`${label}${source}: ${entry.result.scopes.join(", ")}`); else if (entry.result.error) lines.push(`${label}: ${theme.error(entry.result.error)}`); } if (report.channel === "discord" && report.channelPermissions) { const perms = report.channelPermissions; if (perms.error) lines.push(`Permissions: ${theme.error(perms.error)}`); else { const list = perms.permissions?.length ? perms.permissions.join(", ") : "none"; const label = perms.channelId ? ` (${perms.channelId})` : ""; lines.push(`Permissions${label}: ${list}`); if (perms.missingRequired && perms.missingRequired.length > 0) lines.push(`${theme.warn("Missing required:")} ${perms.missingRequired.join(", ")}`); else lines.push(theme.success("Missing required: none")); } } else if (report.channel === "discord" && rawTarget && !report.channelPermissions) lines.push(theme.muted("Permissions: skipped (no target).")); lines.push(""); } runtime.log(lines.join("\n").trimEnd()); } //#endregion //#region src/commands/channels/list.ts const colorValue = (value) => { if (value === "none") return theme.error(value); if (value === "env") return theme.accent(value); return theme.success(value); }; function formatEnabled(value) { return value === false ? theme.error("disabled") : theme.success("enabled"); } function formatConfigured(value) { return value ? theme.success("configured") : theme.warn("not configured"); } function formatTokenSource(source) { return `token=${colorValue(source || "none")}`; } function formatSource(label, source) { return `${label}=${colorValue(source || "none")}`; } function formatLinked(value) { return value ? theme.success("linked") : theme.warn("not linked"); } function shouldShowConfigured(channel) { return channel.meta.showConfigured !== false; } function formatAccountLine(params) { const { channel, snapshot } = params; const label = formatChannelAccountLabel({ channel: channel.id, accountId: snapshot.accountId, name: snapshot.name, channelStyle: theme.accent, accountStyle: theme.heading }); const bits = []; if (snapshot.linked !== void 0) bits.push(formatLinked(snapshot.linked)); if (shouldShowConfigured(channel) && typeof snapshot.configured === "boolean") bits.push(formatConfigured(snapshot.configured)); if (snapshot.tokenSource) bits.push(formatTokenSource(snapshot.tokenSource)); if (snapshot.botTokenSource) bits.push(formatSource("bot", snapshot.botTokenSource)); if (snapshot.appTokenSource) bits.push(formatSource("app", snapshot.appTokenSource)); if (snapshot.baseUrl) bits.push(`base=${theme.muted(snapshot.baseUrl)}`); if (typeof snapshot.enabled === "boolean") bits.push(formatEnabled(snapshot.enabled)); return `- ${label}: ${bits.join(", ")}`; } async function loadUsageWithProgress(runtime) { try { return await withProgress({ label: "Fetching usage snapshot…", indeterminate: true, enabled: true }, async () => await loadProviderUsageSummary()); } catch (err) { runtime.error(String(err)); return null; } } async function channelsListCommand(opts, runtime = defaultRuntime) { const cfg = await requireValidConfig(runtime); if (!cfg) return; const includeUsage = opts.usage !== false; const plugins = listChannelPlugins(); const authStore = loadAuthProfileStore(); const authProfiles = Object.entries(authStore.profiles).map(([profileId, profile]) => ({ id: profileId, provider: profile.provider, type: profile.type, isExternal: false })); if (opts.json) { const usage = includeUsage ? await loadProviderUsageSummary() : void 0; const chat = {}; for (const plugin of plugins) chat[plugin.id] = plugin.config.listAccountIds(cfg); const payload = { chat, auth: authProfiles, ...usage ? { usage } : {} }; runtime.log(JSON.stringify(payload, null, 2)); return; } const lines = []; lines.push(theme.heading("Chat channels:")); for (const plugin of plugins) { const accounts = plugin.config.listAccountIds(cfg); if (!accounts || accounts.length === 0) continue; for (const accountId of accounts) { const snapshot = await buildChannelAccountSnapshot({ plugin, cfg, accountId }); lines.push(formatAccountLine({ channel: plugin, snapshot })); } } lines.push(""); lines.push(theme.heading("Auth providers (OAuth + API keys):")); if (authProfiles.length === 0) lines.push(theme.muted("- none")); else for (const profile of authProfiles) { const external = profile.isExternal ? theme.muted(" (synced)") : ""; lines.push(`- ${theme.accent(profile.id)} (${theme.success(profile.type)}${external})`); } runtime.log(lines.join("\n")); if (includeUsage) { runtime.log(""); const usage = await loadUsageWithProgress(runtime); if (usage) { const usageLines = formatUsageReportLines(usage); if (usageLines.length > 0) { usageLines[0] = theme.accent(usageLines[0]); runtime.log(usageLines.join("\n")); } } } runtime.log(""); runtime.log(`Docs: ${formatDocsLink("/gateway/configuration", "gateway/configuration")}`); } //#endregion //#region src/commands/channels/logs.ts const DEFAULT_LIMIT = 200; const MAX_BYTES = 1e6; const getChannelSet = () => new Set([...listChannelPlugins().map((plugin) => plugin.id), "all"]); function parseChannelFilter(raw) { const trimmed = raw?.trim().toLowerCase(); if (!trimmed) return "all"; return getChannelSet().has(trimmed) ? trimmed : "all"; } function matchesChannel(line, channel) { if (channel === "all") return true; const needle = `gateway/channels/${channel}`; if (line.subsystem?.includes(needle)) return true; if (line.module?.includes(channel)) return true; return false; } async function readTailLines(file, limit) { const stat = await fs.stat(file).catch(() => null); if (!stat) return []; const size = stat.size; const start = Math.max(0, size - MAX_BYTES); const handle = await fs.open(file, "r"); try { const length = Math.max(0, size - start); if (length === 0) return []; const buffer = Buffer.alloc(length); const readResult = await handle.read(buffer, 0, length, start); let lines = buffer.toString("utf8", 0, readResult.bytesRead).split("\n"); if (start > 0) lines = lines.slice(1); if (lines.length && lines[lines.length - 1] === "") lines = lines.slice(0, -1); if (lines.length > limit) lines = lines.slice(lines.length - limit); return lines; } finally { await handle.close(); } } async function channelsLogsCommand(opts, runtime = defaultRuntime) { const channel = parseChannelFilter(opts.channel); const limitRaw = typeof opts.lines === "string" ? Number(opts.lines) : opts.lines; const limit = typeof limitRaw === "number" && Number.isFinite(limitRaw) && limitRaw > 0 ? Math.floor(limitRaw) : DEFAULT_LIMIT; const file = getResolvedLoggerSettings().file; const filtered = (await readTailLines(file, limit * 4)).map(parseLogLine).filter((line) => Boolean(line)).filter((line) => matchesChannel(line, channel)); const lines = filtered.slice(Math.max(0, filtered.length - limit)); if (opts.json) { runtime.log(JSON.stringify({ file, channel, lines }, null, 2)); return; } runtime.log(theme.info(`Log file: ${file}`)); if (channel !== "all") runtime.log(theme.info(`Channel: ${channel}`)); if (lines.length === 0) { runtime.log(theme.muted("No matching log lines.")); return; } for (const line of lines) { const ts = line.time ? `${line.time} ` : ""; const level = line.level ? `${line.level.toLowerCase()} ` : ""; runtime.log(`${ts}${level}${line.message}`.trim()); } } //#endregion //#region src/commands/channels/remove.ts function listAccountIds(cfg, channel) { const plugin = getChannelPlugin(channel); if (!plugin) return []; return plugin.config.listAccountIds(cfg); } async function channelsRemoveCommand(opts, runtime = defaultRuntime, params) { const cfg = await requireValidConfig(runtime); if (!cfg) return; const useWizard = shouldUseWizard(params); const prompter = useWizard ? createClackPrompter() : null; let channel = normalizeChannelId(opts.channel); let accountId = normalizeAccountId(opts.account); const deleteConfig = Boolean(opts.delete); if (useWizard && prompter) { await prompter.intro("Remove channel account"); const selectedChannel = await prompter.select({ message: "Channel", options: listChannelPlugins().map((plugin) => ({ value: plugin.id, label: plugin.meta.label })) }); channel = selectedChannel; accountId = await (async () => { const ids = listAccountIds(cfg, selectedChannel); return normalizeAccountId(await prompter.select({ message: "Account", options: ids.map((id) => ({ value: id, label: id === DEFAULT_ACCOUNT_ID ? "default (primary)" : id })), initialValue: ids[0] ?? DEFAULT_ACCOUNT_ID })); })(); if (!await prompter.confirm({ message: `Disable ${channelLabel(selectedChannel)} account "${accountId}"? (keeps config)`, initialValue: true })) { await prompter.outro("Cancelled."); return; } } else { if (!channel) { runtime.error("Channel is required. Use --channel <name>."); runtime.exit(1); return; } if (!deleteConfig) { if (!await createClackPrompter().confirm({ message: `Disable ${channelLabel(channel)} account "${accountId}"? (keeps config)`, initialValue: true })) return; } } const plugin = getChannelPlugin(channel); if (!plugin) { runtime.error(`Unknown channel: ${channel}`); runtime.exit(1); return; } const resolvedAccountId = normalizeAccountId(accountId) ?? resolveChannelDefaultAccountId({ plugin, cfg }); const accountKey = resolvedAccountId || DEFAULT_ACCOUNT_ID; let next = { ...cfg }; if (deleteConfig) { if (!plugin.config.deleteAccount) { runtime.error(`Channel ${channel} does not support delete.`); runtime.exit(1); return; } next = plugin.config.deleteAccount({ cfg: next, accountId: resolvedAccountId }); if (channel === "telegram") await deleteTelegramUpdateOffset({ accountId: resolvedAccountId }); } else { if (!plugin.config.setAccountEnabled) { runtime.error(`Channel ${channel} does not support disable.`); runtime.exit(1); return; } next = plugin.config.setAccountEnabled({ cfg: next, accountId: resolvedAccountId, enabled: false }); } await writeConfigFile(next); if (useWizard && prompter) await prompter.outro(deleteConfig ? `Deleted ${channelLabel(channel)} account "${accountKey}".` : `Disabled ${channelLabel(channel)} account "${accountKey}".`); else runtime.log(deleteConfig ? `Deleted ${channelLabel(channel)} account "${accountKey}".` : `Disabled ${channelLabel(channel)} account "${accountKey}".`); } //#endregion //#region src/commands/channels/resolve.ts function resolvePreferredKind(kind) { if (!kind || kind === "auto") return; if (kind === "user") return "user"; return "group"; } function detectAutoKind(input) { const trimmed = input.trim(); if (!trimmed) return "group"; if (trimmed.startsWith("@")) return "user"; if (/^<@!?/.test(trimmed)) return "user"; if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) return "user"; if (/^(user|discord|slack|matrix|msteams|teams|zalo|zalouser|googlechat|google-chat|gchat):/i.test(trimmed)) return "user"; return "group"; } function formatResolveResult(result) { if (!result.resolved || !result.id) return `${result.input} -> unresolved`; const name = result.name ? ` (${result.name})` : ""; const note = result.note ? ` [${result.note}]` : ""; return `${result.input} -> ${result.id}${name}${note}`; } async function channelsResolveCommand(opts, runtime) { const cfg = loadConfig(); const entries = (opts.entries ?? []).map((entry) => entry.trim()).filter(Boolean); if (entries.length === 0) throw new Error("At least one entry is required."); const selection = await resolveMessageChannelSelection({ cfg, channel: opts.channel ?? null }); const plugin = getChannelPlugin(selection.channel); if (!plugin?.resolver?.resolveTargets) throw new Error(`Channel ${selection.channel} does not support resolve.`); const preferredKind = resolvePreferredKind(opts.kind); let results = []; if (preferredKind) results = (await plugin.resolver.resolveTargets({ cfg, accountId: opts.account ?? null, inputs: entries, kind: preferredKind, runtime })).map((entry) => ({ input: entry.input, resolved: entry.resolved, id: entry.id, name: entry.name, note: entry.note })); else { const byKind = /* @__PURE__ */ new Map(); for (const entry of entries) { const kind = detectAutoKind(entry); byKind.set(kind, [...byKind.get(kind) ?? [], entry]); } const resolved = []; for (const [kind, inputs] of byKind.entries()) { const batch = await plugin.resolver.resolveTargets({ cfg, accountId: opts.account ?? null, inputs, kind, runtime }); resolved.push(...batch); } const byInput = new Map(resolved.map((entry) => [entry.input, entry])); results = entries.map((input) => { const entry = byInput.get(input); return { input, resolved: entry?.resolved ?? false, id: entry?.id, name: entry?.name, note: entry?.note }; }); } if (opts.json) { runtime.log(JSON.stringify(results, null, 2)); return; } for (const result of results) if (result.resolved && result.id) runtime.log(formatResolveResult(result)); else runtime.error(danger(`${result.input} -> unresolved${result.error ? ` (${result.error})` : result.note ? ` (${result.note})` : ""}`)); } //#endregion //#region src/commands/channels/status.ts function appendEnabledConfiguredLinkedBits(bits, account) { if (typeof account.enabled === "boolean") bits.push(account.enabled ? "enabled" : "disabled"); if (typeof account.configured === "boolean") bits.push(account.configured ? "configured" : "not configured"); if (typeof account.linked === "boolean") bits.push(account.linked ? "linked" : "not linked"); } function appendModeBit(bits, account) { if (typeof account.mode === "string" && account.mode.length > 0) bits.push(`mode:${account.mode}`); } function appendTokenSourceBits(bits, account) { if (typeof account.tokenSource === "string" && account.tokenSource) bits.push(`token:${account.tokenSource}`); if (typeof account.botTokenSource === "string" && account.botTokenSource) bits.push(`bot:${account.botTokenSource}`); if (typeof account.appTokenSource === "string" && account.appTokenSource) bits.push(`app:${account.appTokenSource}`); } function appendBaseUrlBit(bits, account) { if (typeof account.baseUrl === "string" && account.baseUrl) bits.push(`url:${account.baseUrl}`); } function buildChannelAccountLine(provider, account, bits) { return `- ${formatChannelAccountLabel({ channel: provider, accountId: typeof account.accountId === "string" ? account.accountId : "default", name: (typeof account.name === "string" ? account.name.trim() : "") || void 0 })}: ${bits.join(", ")}`; } function formatGatewayChannelsStatusLines(payload) { const lines = []; lines.push(theme.success("Gateway reachable.")); const accountLines = (provider, accounts) => accounts.map((account) => { const bits = []; appendEnabledConfiguredLinkedBits(bits, account); if (typeof account.running === "boolean") bits.push(account.running ? "running" : "stopped"); if (typeof account.connected === "boolean") bits.push(account.connected ? "connected" : "disconnected"); const inboundAt = typeof account.lastInboundAt === "number" && Number.isFinite(account.lastInboundAt) ? account.lastInboundAt : null; const outboundAt = typeof account.lastOutboundAt === "number" && Number.isFinite(account.lastOutboundAt) ? account.lastOutboundAt : null; if (inboundAt) bits.push(`in:${formatTimeAgo(Date.now() - inboundAt)}`); if (outboundAt) bits.push(`out:${formatTimeAgo(Date.now() - outboundAt)}`); appendModeBit(bits, account); const botUsername = (() => { const bot = account.bot; const probeBot = account.probe?.bot; const raw = bot?.username ?? probeBot?.username ?? ""; if (typeof raw !== "string") return ""; const trimmed = raw.trim(); if (!trimmed) return ""; return trimmed.startsWith("@") ? trimmed : `@${trimmed}`; })(); if (botUsername) bits.push(`bot:${botUsername}`); if (typeof account.dmPolicy === "string" && account.dmPolicy.length > 0) bits.push(`dm:${account.dmPolicy}`); if (Array.isArray(account.allowFrom) && account.allowFrom.length > 0) bits.push(`allow:${account.allowFrom.slice(0, 2).join(",")}`); appendTokenSourceBits(bits, account); const messageContent = account.application?.intents?.messageContent; if (typeof messageContent === "string" && messageContent.length > 0 && messageContent !== "enabled") bits.push(`intents:content=${messageContent}`); if (account.allowUnmentionedGroups === true) bits.push("groups:unmentioned"); appendBaseUrlBit(bits, account); const probe = account.probe; if (probe && typeof probe.ok === "boolean") bits.push(probe.ok ? "works" : "probe failed"); const audit = account.audit; if (audit && typeof audit.ok === "boolean") bits.push(audit.ok ? "audit ok" : "audit failed"); if (typeof account.lastError === "string" && account.lastError) bits.push(`error:${account.lastError}`); return buildChannelAccountLine(provider, account, bits); }); const plugins = listChannelPlugins(); const accountsByChannel = payload.channelAccounts; const accountPayloads = {}; for (const plugin of plugins) { const raw = accountsByChannel?.[plugin.id]; if (Array.isArray(raw)) accountPayloads[plugin.id] = raw; } for (const plugin of plugins) { const accounts = accountPayloads[plugin.id]; if (accounts && accounts.length > 0) lines.push(...accountLines(plugin.id, accounts)); } lines.push(""); const issues = collectChannelStatusIssues(payload); if (issues.length > 0) { lines.push(theme.warn("Warnings:")); for (const issue of issues) lines.push(`- ${issue.channel} ${issue.accountId}: ${issue.message}${issue.fix ? ` (${issue.fix})` : ""}`); lines.push(`- Run: ${formatCliCommand("openclaw doctor")}`); lines.push(""); } lines.push(`Tip: ${formatDocsLink("/cli#status", "status --deep")} adds gateway health probes to status output (requires a reachable gateway).`); return lines; } async function formatConfigChannelsStatusLines(cfg, meta) { const lines = []; lines.push(theme.warn("Gateway not reachable; showing config-only status.")); if (meta.path) lines.push(`Config: ${meta.path}`); if (meta.mode) lines.push(`Mode: ${meta.mode}`); if (meta.path || meta.mode) lines.push(""); const accountLines = (provider, accounts) => accounts.map((account) => { const bits = []; appendEnabledConfiguredLinkedBits(bits, account); appendModeBit(bits, account); appendTokenSourceBits(bits, account); appendBaseUrlBit(bits, account); return buildChannelAccountLine(provider, account, bits); }); const plugins = listChannelPlugins(); for (const plugin of plugins) { const accountIds = plugin.config.listAccountIds(cfg); if (!accountIds.length) continue; const snapshots = []; for (const accountId of accountIds) { const snapshot = await buildChannelAccountSnapshot({ plugin, cfg, accountId }); snapshots.push(snapshot); } if (snapshots.length > 0) lines.push(...accountLines(plugin.id, snapshots)); } lines.push(""); lines.push(`Tip: ${formatDocsLink("/cli#status", "status --deep")} adds gateway health probes to status output (requires a reachable gateway).`); return lines; } async function channelsStatusCommand(opts, runtime = defaultRuntime) { const timeoutMs = Number(opts.timeout ?? 1e4); const statusLabel = opts.probe ? "Checking channel status (probe)…" : "Checking channel status…"; if (opts.json !== true && !process.stderr.isTTY) runtime.log(statusLabel); try { const payload = await withProgress({ label: statusLabel, indeterminate: true, enabled: opts.json !== true }, async () => await callGateway({ method: "channels.status", params: { probe: Boolean(opts.probe), timeoutMs }, timeoutMs })); if (opts.json) { runtime.log(JSON.stringify(payload, null, 2)); return; } runtime.log(formatGatewayChannelsStatusLines(payload).join("\n")); } catch (err) { runtime.error(`Gateway not reachable: ${String(err)}`); const cfg = await requireValidConfig(runtime); if (!cfg) return; const snapshot = await readConfigFileSnapshot(); const mode = cfg.gateway?.mode === "remote" ? "remote" : "local"; runtime.log((await formatConfigChannelsStatusLines(cfg, { path: snapshot.path, mode })).join("\n")); } } //#endregion //#region src/cli/channel-auth.ts async function runChannelLogin(opts, runtime = defaultRuntime) { const channelInput = opts.channel ?? DEFAULT_CHAT_CHANNEL; const channelId = normalizeChannelId(channelInput); if (!channelId) throw new Error(`Unsupported channel: ${channelInput}`); const plugin = getChannelPlugin(channelId); if (!plugin?.auth?.login) throw new Error(`Channel ${channelId} does not support login`); setVerbose(Boolean(opts.verbose)); const cfg = loadConfig(); const accountId = opts.account?.trim() || resolveChannelDefaultAccountId({ plugin, cfg }); await plugin.auth.login({ cfg, accountId, runtime, verbose: Boolean(opts.verbose), channelInput }); } async function runChannelLogout(opts, runtime = defaultRuntime) { const channelInput = opts.channel ?? DEFAULT_CHAT_CHANNEL; const channelId = normalizeChannelId(channelInput); if (!channelId) throw new Error(`Unsupported channel: ${channelInput}`); const plugin = getChannelPlugin(channelId); if (!plugin?.gateway?.logoutAccount) throw new Error(`Channel ${channelId} does not support logout`); const cfg = loadConfig(); const accountId = opts.account?.trim() || resolveChannelDefaultAccountId({ plugin, cfg }); const account = plugin.config.resolveAccount(cfg, accountId); await plugin.gateway.logoutAccount({ cfg, accountId, account, runtime }); } //#endregion //#region src/cli/channels-cli.ts const optionNamesAdd = [ "channel", "account", "name", "token", "tokenFile", "botToken", "appToken", "signalNumber", "cliPath", "dbPath", "service", "region", "authDir", "httpUrl", "httpHost", "httpPort", "webhookPath", "webhookUrl", "audienceType", "audience", "useEnv", "homeserver", "userId", "accessToken", "password", "deviceName", "initialSyncLimit", "ship", "url", "code", "groupChannels", "dmAllowlist", "autoDiscoverChannels" ]; const optionNamesRemove = [ "channel", "account", "delete" ]; function runChannelsCommand(action) { return runCommandWithRuntime(defaultRuntime, action); } function runChannelsCommandWithDanger(action, label) { return runCommandWithRuntime(defaultRuntime, action, (err) => { defaultRuntime.error(danger(`${label}: ${String(err)}`)); defaultRuntime.exit(1); }); } function registerChannelsCli(program) { const channelNames = formatCliChannelOptions(); const channels = program.command("channels").description("Manage connected chat channels and accounts").addHelpText("after", () => `\n${theme.heading("Examples:")}\n${formatHelpExamples([ ["openclaw channels list", "List configured channels and auth profiles."], ["openclaw channels status --probe", "Run channel status checks and probes."], ["openclaw channels add --channel telegram --token <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 configured channels + auth profiles").option("--no-usage", "Skip model provider usage/quota