UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,021 lines (1,020 loc) 40.8 kB
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString, s as normalizeOptionalLowercaseString } from "./string-coerce-mnp54Vah.js"; import { b as parseStrictPositiveInteger, y as parseStrictNonNegativeInteger } from "./number-coercion-CJQ8TR--.js"; import "./parse-finite-number-Z7n6tXLk.js"; import { i as formatErrorMessage } from "./errors-BXgSefBE.js"; import { t as formatCliCommand } from "./command-format-CKGmlpAQ.js"; import { r as theme } from "./theme-vjDs9tao.js"; import { t as createLazyImportLoader } from "./lazy-promise-BONnzNfb.js"; import { l as normalizeStringEntries } from "./string-normalization-WNUDCpXX.js"; import { n as defaultRuntime, r as writeRuntimeJson } from "./runtime-B4lgFmsS.js"; import { a as getResolvedLoggerSettings } from "./logger-Brhy0cvC.js"; import "./agent-scope-MrLta7Pq.js"; import { n as normalizeAccountId } from "./account-id-Df9e41E6.js"; import { c as resolveDefaultAgentId, o as resolveAgentWorkspaceDir } from "./agent-scope-config-CgCYpZfK.js"; import { t as danger } from "./globals-GTrXU4s9.js"; import { i as getRuntimeConfig, u as readConfigFileSnapshot } from "./io-Gi7-pyU-.js"; import { a as getBundledChannelSetupPlugin } from "./bundled-DcYUmgzC.js"; import { i as replaceConfigFile } from "./config-C9RxTsn1.js"; import { c as formatUnknownChannelMessage, l as formatUnsupportedChannelActionMessage } from "./error-format-COoNML-C.js"; import { i as GATEWAY_CLIENT_NAMES, r as GATEWAY_CLIENT_MODES } from "./client-info-CcqJJIan.js"; import { o as callGateway } from "./call-B5-GYOlf.js"; import { o as normalizeChannelId } from "./registry-9YoS2BJP.js"; import { a as normalizeChannelId$1, n as getLoadedChannelPlugin, t as getChannelPlugin } from "./registry-MkxNn1Ue.js"; import "./plugins-t2ejWcVy.js"; import "./message-channel-BiOeMu0l.js"; import "./logging-CvNAh8ZC.js"; import { t as listManifestChannelContributionIds } from "./manifest-contribution-ids-SDxxvWhJ.js"; import { l as resolveMessageActionDiscoveryForPlugin, r as createMessageActionDiscoveryContext } from "./message-action-discovery-BhmC0Myk.js"; import { r as resolveMessageChannelSelection } from "./channel-selection-MKj8pvsX.js"; import { i as resolveChannelDefaultAccountId, r as parseOptionalDelimitedEntries } from "./helpers-BLGguEiN.js"; import { n as listReadOnlyChannelPluginsForConfig } from "./read-only-7UA2_pQ4.js"; import { o as getChannelsCommandSecretTargetIds } from "./command-secret-targets-DY8QOUoE.js"; import { c as moveSingleAccountChannelSectionToDefaultAccount } from "./setup-helpers-DO5nSeNy.js"; import { t as WizardCancelledError } from "./prompts-E0FayNjJ.js"; import { t as createClackPrompter } from "./clack-prompter-fc9v8HJv.js"; import { t as commitConfigWithPendingPluginInstalls } from "./plugins-install-record-commit--Zbbdxlm.js"; import { t as refreshPluginRegistryAfterConfigMutation } from "./plugins-registry-refresh-D83-QnpV.js"; import { t as resolveCommandConfigWithSecrets } from "./command-config-resolution-Ce9AmmGx.js"; import { n as parseTimeoutMsWithFallback } from "./parse-timeout-DZMZaF9B.js"; import { t as resolveInstallableChannelPlugin } from "./channel-plugin-resolution-BCjPdSkx.js"; import { n as resolveLogFile } from "./log-tail-472n_nQU.js"; import { t as parseLogLine } from "./parse-log-line-BSO0Z-Rb.js"; import { t as describeBinding } from "./agents.binding-format-C3S9Mq5U.js"; import { t as applyAgentBindings } from "./agents.bindings-Ih6TOgX-.js"; import { t as requireValidConfigFileSnapshot } from "./config-validation-Bz2qAeof.js"; import { c as shouldUseWizard, o as formatChannelAccountLabel, s as requireValidConfig } from "./shared-vtv0TMAo.js"; import { t as channelsListCommand } from "./list-C42d9gRr.js"; import { n as formatGatewayChannelsStatusLines, t as channelsStatusCommand } from "./status-BIpVw_QG.js"; import fs from "node:fs/promises"; //#region src/commands/channels/add-mutators.ts /** Apply a display name to a channel account when the plugin supports account naming. */ function applyAccountName(params) { const accountId = normalizeAccountId(params.accountId); const apply = (params.plugin ?? getChannelPlugin(params.channel))?.setup?.applyAccountName; return apply ? apply({ cfg: params.cfg, accountId, name: params.name }) : params.cfg; } /** Delegate account config mutation to the channel plugin setup contract. */ function applyChannelAccountConfig(params) { const accountId = normalizeAccountId(params.accountId); const apply = (params.plugin ?? getChannelPlugin(params.channel))?.setup?.applyAccountConfig; if (!apply) return params.cfg; return apply({ cfg: params.cfg, accountId, input: params.input }); } //#endregion //#region src/commands/channels/runtime-label.ts /** Resolve a display label from loaded, setup-only, or bundled channel plugin metadata. */ const channelLabel = (channel) => { return (getLoadedChannelPlugin(channel) ?? getBundledChannelSetupPlugin(channel) ?? getChannelPlugin(channel))?.meta.label ?? channel; }; //#endregion //#region src/commands/channels/add.ts const channelSetupPluginInstallLoader = createLazyImportLoader(() => import("./plugin-install-2p4B0Ns4.js")); const onboardChannelsLoader = createLazyImportLoader(() => import("./onboard-channels-BQbbJqOI.js")); function loadChannelSetupPluginInstall() { return channelSetupPluginInstallLoader.load(); } function loadOnboardChannels() { return onboardChannelsLoader.load(); } const CHANNEL_ADD_CONTROL_OPTION_KEYS = new Set(["channel", "account"]); const NEXTCLOUD_TALK_CLI_ALIASES = new Set([ "nextcloud-talk", "nc-talk", "nc" ]); async function resolveCatalogChannelEntry(raw, cfg) { const trimmed = normalizeOptionalLowercaseString(raw); if (!trimmed) return; return (cfg ? await import("./trusted-catalog-BMRfMlSe.js").then(({ listTrustedChannelPluginCatalogEntries }) => listTrustedChannelPluginCatalogEntries({ cfg, workspaceDir: resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg)) })) : await import("./catalog-BmJHJqAv.js").then(({ listRawChannelPluginCatalogEntries }) => listRawChannelPluginCatalogEntries({ excludeWorkspace: true }))).find((entry) => { if (normalizeOptionalLowercaseString(entry.id) === trimmed) return true; return (entry.meta.aliases ?? []).some((alias) => normalizeOptionalLowercaseString(alias) === trimmed); }); } function parseOptionalInt(value, flag) { if (value === void 0 || value === null || value === "") return; const parsed = parseStrictNonNegativeInteger(value); if (parsed === void 0) throw new Error(`${flag} must be a non-negative integer.`); return parsed; } function parseOptionalDelimitedInput(value) { if (Array.isArray(value)) return value.filter((entry) => typeof entry === "string"); return parseOptionalDelimitedEntries(typeof value === "string" ? value : void 0); } function readOptionalString(value) { return typeof value === "string" && value.length > 0 ? value : void 0; } function buildChannelSetupInput(opts) { const input = {}; for (const [key, value] of Object.entries(opts)) { if (CHANNEL_ADD_CONTROL_OPTION_KEYS.has(key) || value === void 0) continue; input[key] = value; } const rawChannel = readOptionalString(opts.channel)?.trim().toLowerCase(); if (rawChannel && NEXTCLOUD_TALK_CLI_ALIASES.has(rawChannel)) { input.baseUrl ??= readOptionalString(input.url); input.secret ??= readOptionalString(input.token) ?? readOptionalString(input.password); input.secretFile ??= readOptionalString(input.tokenFile); } input.initialSyncLimit = parseOptionalInt(opts.initialSyncLimit, "--initial-sync-limit"); input.groupChannels = parseOptionalDelimitedInput(opts.groupChannels); input.dmAllowlist = parseOptionalDelimitedInput(opts.dmAllowlist); return input; } /** Add or configure a channel account, using the wizard when no concrete flags are supplied. */ async function channelsAddCommand(opts, runtime = defaultRuntime, params) { try { return await channelsAddCommandImpl(opts, runtime, params); } catch (err) { if (err instanceof WizardCancelledError) { runtime.exit(1); return; } throw err; } } async function channelsAddCommandImpl(opts, runtime, params) { const configSnapshot = await requireValidConfigFileSnapshot(runtime); if (!configSnapshot) return; const cfg = configSnapshot.sourceConfig ?? configSnapshot.config; const baseHash = configSnapshot.hash; let nextConfig = cfg; let pluginRegistrySourceChanged = false; if (shouldUseWizard(params)) { const [{ buildAgentSummaries }, onboardChannels] = await Promise.all([import("./agents.config-DIfO9Scn.js"), loadOnboardChannels()]); const prompter = createClackPrompter(); const postWriteHooks = onboardChannels.createChannelOnboardingPostWriteHookCollector(); let selection = []; const accountIds = {}; const resolvedPlugins = /* @__PURE__ */ new Map(); await prompter.intro("Channel setup"); let nextConfigLocal = await onboardChannels.setupChannels(cfg, runtime, prompter, { allowDisable: false, allowSignalInstall: true, onPostWriteHook: (hook) => { postWriteHooks.collect(hook); }, promptAccountIds: true, deferStatusUntilSelection: true, skipStatusNote: true, onSelection: (value) => { selection = value; }, onAccountId: (channel, accountId) => { accountIds[channel] = accountId; }, onResolvedPlugin: (channel, plugin) => { resolvedPlugins.set(channel, plugin); } }); if (selection.length === 0) { await prompter.outro("No channel changes made."); return; } if (await prompter.confirm({ message: "Name these channel accounts now? (optional)", initialValue: false })) for (const channel of selection) { const accountId = accountIds[channel] ?? "default"; const plugin = resolvedPlugins.get(channel) ?? getLoadedChannelPlugin(channel); const account = plugin?.config.resolveAccount(nextConfigLocal, accountId); const existingName = (plugin?.config.describeAccount?.(account, nextConfigLocal))?.name ?? account?.name; const name = await prompter.text({ message: `${channel} display name for account "${accountId}"`, initialValue: existingName }); if (name?.trim()) nextConfigLocal = applyAccountName({ cfg: nextConfigLocal, channel, accountId, name, plugin }); } const bindTargets = selection.map((channel) => ({ channel, accountId: accountIds[channel]?.trim() })).filter((value) => Boolean(value.accountId)); if (bindTargets.length > 0) { if (await prompter.confirm({ message: "Route these channel accounts to agents now?", initialValue: true })) { const agentSummaries = buildAgentSummaries(nextConfigLocal); const defaultAgentId = resolveDefaultAgentId(nextConfigLocal); for (const target of bindTargets) { const targetAgentId = await prompter.select({ message: `Send ${target.channel}/${target.accountId} messages to agent`, options: agentSummaries.map((agent) => ({ value: agent.id, label: agent.isDefault ? `${agent.id} (default)` : agent.id })), initialValue: defaultAgentId }); const bindingResult = applyAgentBindings(nextConfigLocal, [{ agentId: targetAgentId, match: { channel: target.channel, accountId: target.accountId } }]); nextConfigLocal = bindingResult.config; if (bindingResult.added.length > 0 || bindingResult.updated.length > 0) await prompter.note([...bindingResult.added.map((binding) => `Added: ${describeBinding(binding)}`), ...bindingResult.updated.map((binding) => `Updated: ${describeBinding(binding)}`)].join("\n"), "Routing bindings"); if (bindingResult.conflicts.length > 0) await prompter.note(["Skipped bindings already claimed by another agent:", ...bindingResult.conflicts.map((conflict) => `- ${describeBinding(conflict.binding)} (agent=${conflict.existingAgentId})`)].join("\n"), "Routing bindings"); } } } const committed = await commitConfigWithPendingPluginInstalls({ nextConfig: nextConfigLocal, ...baseHash !== void 0 ? { baseHash } : {} }); const writtenConfig = committed.config; if (committed.movedInstallRecords) await refreshPluginRegistryAfterConfigMutation({ config: writtenConfig, reason: "source-changed", installRecords: committed.installRecords, logger: { warn: (message) => runtime.log(message) } }); await onboardChannels.runCollectedChannelOnboardingPostWriteHooks({ hooks: postWriteHooks.drain(), cfg: writtenConfig, runtime }); await prompter.outro("Channels updated."); return; } const rawChannel = opts.channel ?? ""; let channel = normalizeChannelId$1(rawChannel); let catalogEntry = await resolveCatalogChannelEntry(rawChannel, nextConfig); const resolveWorkspaceDir = () => resolveAgentWorkspaceDir(nextConfig, resolveDefaultAgentId(nextConfig)); const loadScopedPlugin = async (channelId, pluginId) => { const existing = getLoadedChannelPlugin(channelId); if (existing?.setup?.applyAccountConfig) return existing; const { loadChannelSetupPluginRegistrySnapshotForChannel } = await loadChannelSetupPluginInstall(); const snapshot = loadChannelSetupPluginRegistrySnapshotForChannel({ cfg: nextConfig, runtime, channel: channelId, ...pluginId ? { pluginId } : {}, workspaceDir: resolveWorkspaceDir(), forceSetupOnlyChannelPlugins: true }); return snapshot.channelSetups.find((entry) => entry.plugin.id === channelId)?.plugin ?? getBundledChannelSetupPlugin(channelId) ?? snapshot.channels.find((entry) => entry.plugin.id === channelId)?.plugin ?? existing; }; if (catalogEntry) { const workspaceDir = resolveWorkspaceDir(); const { isCatalogChannelInstalled } = await import("./discovery-C2G3T65s.js"); const registeredPlugin = channel ? getLoadedChannelPlugin(channel) : void 0; const bundledSetupPlugin = channel ? getBundledChannelSetupPlugin(channel) : void 0; if (!registeredPlugin && !bundledSetupPlugin && !isCatalogChannelInstalled({ cfg: nextConfig, entry: catalogEntry, workspaceDir })) { const { ensureChannelSetupPluginInstalled } = await loadChannelSetupPluginInstall(); const prompter = createClackPrompter(); const result = await ensureChannelSetupPluginInstalled({ cfg: nextConfig, entry: catalogEntry, prompter, runtime, workspaceDir, promptInstall: false }); nextConfig = result.cfg; if (!result.installed) return; pluginRegistrySourceChanged = true; catalogEntry = { ...catalogEntry, ...result.pluginId ? { pluginId: result.pluginId } : {} }; } channel ??= normalizeChannelId$1(catalogEntry.id) ?? catalogEntry.id; } if (!channel) { const hint = catalogEntry ? `Plugin ${catalogEntry.meta.label} could not be loaded after install. Run openclaw doctor --fix, then retry openclaw channels add.` : formatUnknownChannelMessage({ channel: rawChannel }); runtime.error(hint); runtime.exit(1); return; } const plugin = await loadScopedPlugin(channel, catalogEntry?.pluginId); if (!plugin?.setup?.applyAccountConfig) { runtime.error(`${formatUnsupportedChannelActionMessage({ channel, action: "non-interactive add" })} Run ${formatCliCommand("openclaw channels add")} with no flags for guided setup.`); runtime.exit(1); return; } const input = buildChannelSetupInput(opts); const accountId = plugin.setup.resolveAccountId?.({ cfg: nextConfig, accountId: opts.account, input }) ?? normalizeAccountId(opts.account); const validationError = plugin.setup.validateInput?.({ cfg: nextConfig, accountId, input }); if (validationError) { runtime.error(validationError); runtime.exit(1); return; } const prevConfig = nextConfig; if (accountId !== "default") nextConfig = moveSingleAccountChannelSectionToDefaultAccount({ cfg: nextConfig, channelKey: channel }); nextConfig = applyChannelAccountConfig({ cfg: nextConfig, channel, accountId, input, plugin }); await plugin.lifecycle?.onAccountConfigChanged?.({ prevCfg: prevConfig, nextCfg: nextConfig, accountId, runtime }); const committed = await commitConfigWithPendingPluginInstalls({ nextConfig, ...baseHash !== void 0 ? { baseHash } : {} }); const writtenConfig = committed.config; if (committed.movedInstallRecords || pluginRegistrySourceChanged) await refreshPluginRegistryAfterConfigMutation({ config: writtenConfig, reason: "source-changed", ...committed.movedInstallRecords ? { installRecords: committed.installRecords } : {}, logger: { warn: (message) => runtime.log(message) } }); runtime.log(`Added ${plugin.meta.label ?? channelLabel(channel)} account "${accountId}".`); const afterAccountConfigWritten = plugin.setup?.afterAccountConfigWritten; if (afterAccountConfigWritten) { const { runCollectedChannelOnboardingPostWriteHooks } = await loadOnboardChannels(); await runCollectedChannelOnboardingPostWriteHooks({ hooks: [{ channel, accountId, run: async ({ cfg: writtenCfg, runtime: hookRuntime }) => await afterAccountConfigWritten({ previousCfg: cfg, cfg: writtenCfg, accountId, input, runtime: hookRuntime }) }], cfg: writtenConfig, runtime }); } } //#endregion //#region src/commands/channels/capabilities.ts 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 formatGenericProbeLines(probe) { if (!probe || typeof probe !== "object") return []; const probeObj = probe; const ok = typeof probeObj.ok === "boolean" ? probeObj.ok : void 0; if (ok === true) return [{ text: "Probe: ok" }]; if (ok === false) return [{ text: `Probe: failed${typeof probeObj.error === "string" && probeObj.error ? ` (${probeObj.error})` : ""}`, tone: "error" }]; return []; } function renderDisplayLine(line) { switch (line.tone) { case "muted": return theme.muted(line.text); case "success": return theme.success(line.text); case "warn": return theme.warn(line.text); case "error": return theme.error(line.text); default: return line.text; } } 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 = []; 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: formatErrorMessage(err) }; } const diagnostics = configured && enabled ? await plugin.status?.buildCapabilitiesDiagnostics?.({ account: resolvedAccount, timeoutMs, cfg, probe, target: params.target }) : void 0; const discoveredActions = resolveMessageActionDiscoveryForPlugin({ pluginId: plugin.id, actions: plugin.actions, context: createMessageActionDiscoveryContext({ cfg, accountId }), includeActions: true }).actions; const actions = Array.from(new Set([ "send", "broadcast", ...discoveredActions.map((action) => action) ])); reports.push({ plugin, channel: plugin.id, accountId, accountName: typeof resolvedAccount.name === "string" ? normalizeOptionalString(resolvedAccount.name) : void 0, configured, enabled, support: plugin.capabilities, probe, actions, diagnostics }); } return reports; } /** Print or serialize configured channel capabilities, actions, and optional health probe details. */ async function channelsCapabilitiesCommand(opts, runtime = defaultRuntime) { const sourceSnapshotPromise = readConfigFileSnapshot().catch(() => null); const loadedCfg = await requireValidConfig(runtime); if (!loadedCfg) return; let cfg = loadedCfg; const timeoutMs = parseTimeoutMsWithFallback(opts.timeout, 1e4); const rawChannel = normalizeLowercaseStringOrEmpty(opts.channel); const rawTarget = normalizeOptionalString(opts.target) ?? ""; if (opts.account && (!rawChannel || rawChannel === "all")) { runtime.error(danger(`--account requires a specific --channel. Run ${formatCliCommand("openclaw channels list")} to choose one.`)); runtime.exit(1); return; } if (rawTarget && (!rawChannel || rawChannel === "all")) { runtime.error(danger(`--target requires a specific --channel. Run ${formatCliCommand("openclaw channels list")} to choose one.`)); runtime.exit(1); return; } const plugins = listReadOnlyChannelPluginsForConfig(cfg, { includeSetupFallbackPlugins: true }); const selected = !rawChannel || rawChannel === "all" ? plugins : await (async () => { const resolved = await resolveInstallableChannelPlugin({ cfg, runtime, rawChannel, allowInstall: true }); if (resolved.configChanged) { cfg = resolved.cfg; if (Boolean(cfg.plugins?.installs && Object.keys(cfg.plugins.installs).length > 0)) { const committed = await commitConfigWithPendingPluginInstalls({ nextConfig: cfg, baseHash: (await sourceSnapshotPromise)?.hash }); cfg = committed.config; await refreshPluginRegistryAfterConfigMutation({ config: cfg, reason: "source-changed", installRecords: committed.installRecords, logger: { warn: (message) => runtime.log(message) } }); } else { await replaceConfigFile({ nextConfig: cfg, baseHash: (await sourceSnapshotPromise)?.hash }); if (resolved.pluginInstalled) await refreshPluginRegistryAfterConfigMutation({ config: cfg, reason: "source-changed", logger: { warn: (message) => runtime.log(message) } }); } } return resolved.plugin ? [resolved.plugin] : null; })(); if (!selected || selected.length === 0) { if (!rawChannel || rawChannel === "all") { if (opts.json) { writeRuntimeJson(runtime, { channels: [] }); return; } runtime.log(theme.muted(`No configured channel capabilities found. Run ${formatCliCommand("openclaw channels list --all")} to see available channels.`)); return; } runtime.error(danger(formatUnknownChannelMessage({ channel: rawChannel }))); runtime.exit(1); return; } const reports = []; for (const plugin of selected) { const accountOverride = normalizeOptionalString(opts.account); reports.push(...await resolveChannelReports({ plugin, cfg, timeoutMs, accountOverride, target: rawTarget || void 0 })); } if (opts.json) { writeRuntimeJson(runtime, { channels: reports }); return; } const lines = []; for (const report of reports) { const label = formatChannelAccountLabel({ channel: report.channel, accountId: report.accountId, name: report.accountName, channelLabel: report.plugin.meta.label ?? report.channel, 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 = report.plugin.status?.formatCapabilitiesProbe?.({ probe: report.probe }) ?? formatGenericProbeLines(report.probe); if (probeLines.length > 0) lines.push(...probeLines.map(renderDisplayLine)); else if (report.configured && report.enabled) lines.push(theme.muted("Probe: unavailable")); if (report.diagnostics?.lines?.length) lines.push(...report.diagnostics.lines.map(renderDisplayLine)); lines.push(""); } runtime.log(lines.join("\n").trimEnd()); } //#endregion //#region src/commands/channels/logs.ts const DEFAULT_LIMIT = 200; const MAX_BYTES = 1e6; function listManifestChannelIds() { return new Set(listManifestChannelContributionIds({ includeDisabled: true, env: process.env })); } function parseChannelFilter(raw) { const trimmed = normalizeLowercaseStringOrEmpty(raw); if (!trimmed || trimmed === "all") return "all"; const bundled = normalizeChannelId(trimmed); if (bundled) return bundled; return listManifestChannelIds().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; } function parseLinesOption(value) { if (value === void 0 || value === null || value === "") return DEFAULT_LIMIT; const parsed = parseStrictPositiveInteger(value); if (parsed === void 0) throw new Error("--lines must be a positive integer."); return parsed; } 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 { let prefix = ""; if (start > 0) { const prefixBuf = Buffer.alloc(1); const prefixRead = await handle.read(prefixBuf, 0, 1, start - 1); prefix = prefixBuf.toString("utf8", 0, prefixRead.bytesRead); } 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 && prefix !== "\n") 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(); } } /** Print or serialize recent log lines matching one channel subsystem/module. */ async function channelsLogsCommand(opts, runtime = defaultRuntime) { const channel = parseChannelFilter(opts.channel); const limit = parseLinesOption(opts.lines); const file = await resolveLogFile(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) { writeRuntimeJson(runtime, { file, channel, lines }); 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 ? `${normalizeLowercaseStringOrEmpty(line.level)} ` : ""; runtime.log(`${ts}${level}${line.message}`.trim()); } } //#endregion //#region src/commands/channels/remove.ts function listAccountIds(cfg, channel, pluginInput) { let plugin = pluginInput; plugin ??= getChannelPlugin(channel); if (!plugin) return []; return plugin.config.listAccountIds(cfg); } async function stopGatewayRuntimeBeforeRemove(params) { if (!params.plugin.gateway?.startAccount && !params.plugin.gateway?.logoutAccount) return; try { await callGateway({ config: params.cfg, method: "channels.stop", params: { channel: params.channel, accountId: params.accountId }, mode: GATEWAY_CLIENT_MODES.BACKEND, clientName: GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT, deviceIdentity: null }); } catch (error) { params.runtime.log(`Could not stop running ${channelLabel(params.channel)} account "${params.accountId}" before removing it: ${formatErrorMessage(error)}`); } } /** Disable or delete a channel account, stopping gateway runtime state before mutation. */ async function channelsRemoveCommand(opts, runtime = defaultRuntime, params) { const configSnapshot = await requireValidConfigFileSnapshot(runtime); if (!configSnapshot) return; const baseHash = configSnapshot.hash; let cfg = configSnapshot.sourceConfig ?? configSnapshot.config; const useWizard = shouldUseWizard(params); const prompter = useWizard ? createClackPrompter() : null; const rawChannel = normalizeOptionalString(opts.channel) ?? ""; let lookupChannel = rawChannel; let channel = normalizeChannelId$1(rawChannel); let accountId = normalizeAccountId(opts.account); const deleteConfig = Boolean(opts.delete); if (useWizard && prompter) { await prompter.intro("Remove channel account"); const readOnlyPlugins = listReadOnlyChannelPluginsForConfig(cfg, { includeSetupFallbackPlugins: true }); const selectedChannel = await prompter.select({ message: "Channel", options: readOnlyPlugins.map((plugin) => ({ value: plugin.id, label: plugin.meta.label })) }); channel = selectedChannel; lookupChannel = selectedChannel; accountId = await (async () => { const readOnlyPlugin = readOnlyPlugins.find((plugin) => plugin.id === selectedChannel); const ids = listAccountIds(cfg, selectedChannel, readOnlyPlugin); return normalizeAccountId(await prompter.select({ message: "Account", options: ids.map((id) => ({ value: id, label: id === "default" ? "default (primary)" : id })), initialValue: ids[0] ?? "default" })); })(); if (!await prompter.confirm({ message: `Disable ${channelLabel(selectedChannel)} account "${accountId}"? (keeps config)`, initialValue: true })) { await prompter.outro("Cancelled."); return; } } else { if (!rawChannel) { runtime.error(`Missing channel. Use ${formatCliCommand("openclaw channels remove --channel <name>")} or run ${formatCliCommand("openclaw channels status")} to inspect configured channels.`); runtime.exit(1); return; } if (!deleteConfig) { const confirm = createClackPrompter(); const channelPromptLabel = channel ? channelLabel(channel) : rawChannel; if (!await confirm.confirm({ message: `Disable ${channelPromptLabel} account "${accountId}"? (keeps config)`, initialValue: true })) return; } } const resolvedPluginState = Boolean(lookupChannel || channel) ? await (async () => { const { resolveInstallableChannelPlugin } = await import("./channel-plugin-resolution-B536_dwo.js"); return await resolveInstallableChannelPlugin({ cfg, runtime, rawChannel: lookupChannel, allowInstall: false }); })() : null; if (resolvedPluginState?.configChanged) cfg = resolvedPluginState.cfg; const resolvedChannel = resolvedPluginState?.channelId ?? channel; if (!resolvedChannel) { runtime.error(formatUnknownChannelMessage({ channel: rawChannel })); runtime.exit(1); return; } channel = resolvedChannel; const plugin = resolvedPluginState?.plugin ?? getChannelPlugin(resolvedChannel); if (!plugin) { if (resolvedPluginState?.catalogEntry) { runtime.error(`Channel plugin "${resolvedPluginState.catalogEntry.id}" is not installed. Run ${formatCliCommand(`openclaw channels add --channel ${resolvedPluginState.catalogEntry.id}`)} first.`); runtime.exit(1); return; } runtime.error(formatUnknownChannelMessage({ channel: resolvedChannel })); runtime.exit(1); return; } const resolvedChannelId = resolvedChannel; const resolvedAccountId = normalizeAccountId(accountId) ?? resolveChannelDefaultAccountId({ plugin, cfg }); const accountKey = resolvedAccountId || "default"; await stopGatewayRuntimeBeforeRemove({ cfg, channel: resolvedChannelId, accountId: accountKey, plugin, runtime }); let next = { ...cfg }; const prevCfg = cfg; if (deleteConfig) { if (!plugin.config.deleteAccount) { runtime.error(`${formatUnsupportedChannelActionMessage({ channel, action: "delete" })} Use ${formatCliCommand("openclaw channels remove --channel " + channel)} to disable it without deleting config.`); runtime.exit(1); return; } next = plugin.config.deleteAccount({ cfg: next, accountId: resolvedAccountId }); await plugin.lifecycle?.onAccountRemoved?.({ prevCfg, accountId: resolvedAccountId, runtime }); } else { if (!plugin.config.setAccountEnabled) { runtime.error(`${formatUnsupportedChannelActionMessage({ channel, action: "disable" })} Use ${formatCliCommand("openclaw channels remove --channel " + channel + " --delete")} only if you want to remove config.`); runtime.exit(1); return; } next = plugin.config.setAccountEnabled({ cfg: next, accountId: resolvedAccountId, enabled: false }); await plugin.lifecycle?.onAccountConfigChanged?.({ prevCfg, nextCfg: next, accountId: resolvedAccountId, runtime }); } if (Boolean(next.plugins?.installs && Object.keys(next.plugins.installs).length > 0)) { const committed = await commitConfigWithPendingPluginInstalls({ nextConfig: next, ...baseHash !== void 0 ? { baseHash } : {} }); next = committed.config; await refreshPluginRegistryAfterConfigMutation({ config: next, reason: "source-changed", installRecords: committed.installRecords, logger: { warn: (message) => runtime.log(message) } }); } else { await replaceConfigFile({ nextConfig: next, ...baseHash !== void 0 ? { baseHash } : {} }); if (resolvedPluginState?.pluginInstalled) await refreshPluginRegistryAfterConfigMutation({ config: next, reason: "source-changed", logger: { warn: (message) => runtime.log(message) } }); } if (useWizard && prompter) await prompter.outro(deleteConfig ? `Deleted ${channelLabel(resolvedChannelId)} account "${accountKey}".` : `Disabled ${channelLabel(resolvedChannelId)} account "${accountKey}".`); else runtime.log(deleteConfig ? `Deleted ${channelLabel(resolvedChannelId)} account "${accountKey}".` : `Disabled ${channelLabel(resolvedChannelId)} 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:/i.test(trimmed)) return "user"; return "group"; } function detectAutoKindForPlugin(input, plugin) { const generic = detectAutoKind(input); if (generic === "user" || !plugin) return generic; const lowered = normalizeLowercaseStringOrEmpty(input.trim()); const prefixes = [plugin.id, ...plugin.meta?.aliases ?? []].map((entry) => normalizeOptionalLowercaseString(entry)).filter((entry) => Boolean(entry)); for (const prefix of prefixes) { if (!lowered.startsWith(`${prefix}:`)) continue; const remainder = lowered.slice(prefix.length + 1); if (remainder.startsWith("group:") || remainder.startsWith("channel:") || remainder.startsWith("room:") || remainder.startsWith("conversation:") || remainder.startsWith("spaces/") || remainder.startsWith("channels/")) return "group"; return "user"; } return generic; } 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}`; } /** Resolve user/group/channel labels into plugin-specific stable target ids. */ async function channelsResolveCommand(opts, runtime) { const sourceSnapshotPromise = readConfigFileSnapshot().catch(() => null); let { effectiveConfig: cfg } = await resolveCommandConfigWithSecrets({ config: getRuntimeConfig(), commandName: "channels resolve", targetIds: getChannelsCommandSecretTargetIds(), mode: "read_only_operational", runtime, autoEnable: true }); const entries = normalizeStringEntries(opts.entries); if (entries.length === 0) throw new Error(`At least one entry is required. Example: ${formatCliCommand("openclaw channels resolve --channel discord <name-or-id>")}.`); const explicitChannel = opts.channel?.trim(); const resolvedExplicit = explicitChannel ? await resolveInstallableChannelPlugin({ cfg, runtime, rawChannel: explicitChannel, allowInstall: false, supports: (plugin) => Boolean(plugin.resolver?.resolveTargets) }) : null; if (explicitChannel && resolvedExplicit?.catalogEntry && !resolvedExplicit.plugin) throw new Error(`Channel plugin "${resolvedExplicit.catalogEntry.id}" is not installed. Run ${formatCliCommand(`openclaw channels add --channel ${resolvedExplicit.catalogEntry.id}`)} first.`); if (resolvedExplicit?.configChanged) { cfg = resolvedExplicit.cfg; if (Boolean(cfg.plugins?.installs && Object.keys(cfg.plugins.installs).length > 0)) { const committed = await commitConfigWithPendingPluginInstalls({ nextConfig: cfg, baseHash: (await sourceSnapshotPromise)?.hash }); cfg = committed.config; await refreshPluginRegistryAfterConfigMutation({ config: cfg, reason: "source-changed", installRecords: committed.installRecords, logger: { warn: (message) => runtime.log(message) } }); } else { await replaceConfigFile({ nextConfig: cfg, baseHash: (await sourceSnapshotPromise)?.hash }); if (resolvedExplicit.pluginInstalled) await refreshPluginRegistryAfterConfigMutation({ config: cfg, reason: "source-changed", logger: { warn: (message) => runtime.log(message) } }); } } const selection = explicitChannel ? { channel: resolvedExplicit?.channelId } : await resolveMessageChannelSelection({ cfg, channel: opts.channel ?? null }); const plugin = (explicitChannel ? resolvedExplicit?.plugin : void 0) ?? (selection.channel ? getChannelPlugin(selection.channel) : void 0); if (!plugin?.resolver?.resolveTargets) { const channelText = selection.channel ?? explicitChannel ?? ""; throw new Error(formatUnsupportedChannelActionMessage({ channel: channelText, action: "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 = detectAutoKindForPlugin(entry, plugin); 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) { writeRuntimeJson(runtime, results); 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 export { channelsAddCommand, channelsCapabilitiesCommand, channelsListCommand, channelsLogsCommand, channelsRemoveCommand, channelsResolveCommand, channelsStatusCommand, formatGatewayChannelsStatusLines };