openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
678 lines (677 loc) • 27.1 kB
JavaScript
import { w as parseStrictPositiveInteger } from "./number-coercion-CLj0HTDM.js";
import { c as normalizeOptionalLowercaseString, l as normalizeOptionalString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js";
import { d as normalizeStringEntries } from "./string-normalization-DsCfAx8q.js";
import { a as writeRuntimeJson, r as defaultRuntime } from "./runtime-CF2WjnNZ.js";
import { n as getRuntimeConfig } from "./io.runtime-B9iJRs3w.js";
import { t as formatCliCommand } from "./command-format-C7YfyMTd.js";
import { _ as resolveConfiguredAgentId } from "./agent-scope-config-DcbEhP0R.js";
import "./session-key-BnWWjqNc.js";
import { n as normalizeAccountId } from "./account-id-CETVCrTz.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { i as normalizeChatChannelId, n as CHAT_CHANNEL_ORDER } from "./ids-BVZRYG0I.js";
import { n as loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry-contributions-BwuDvm89.js";
import "./plugin-registry-DD_0nL_t.js";
import { i as GATEWAY_CLIENT_NAMES, r as GATEWAY_CLIENT_MODES } from "./client-info-B1bPgeKr.js";
import { r as replaceConfigFile } from "./mutate-ZNN4iFCn.js";
import "./config-Cs0XXL3x.js";
import { r as theme } from "./theme-vjDs9tao.js";
import { t as danger } from "./globals-CTaGxEqj.js";
import { c as formatUnknownChannelMessage, l as formatUnsupportedChannelActionMessage } from "./error-format-Cae7EwnT.js";
import { o as callGateway } from "./call-BWR5pPgw.js";
import "./registry-COqNvmCg.js";
import "./message-channel-BQrhwUEA.js";
import { t as ExpectedCliError } from "./failure-output-Dc5ce9_F.js";
import { n as parseTimeoutMsWithFallback } from "./parse-timeout-BhPKqfrV.js";
import { a as normalizeChannelId, t as getChannelPlugin } from "./registry-Cz6cv4VC.js";
import "./plugins-Cozj1Enf.js";
import { r as createMessageActionDiscoveryContext, u as resolveMessageActionDiscoveryForPlugin } from "./message-action-discovery-Be8PIIhL.js";
import { r as resolveMessageChannelSelection } from "./channel-selection-DGYiyd5S.js";
import { i as resolveChannelDefaultAccountId } from "./helpers-VcK_WJ24.js";
import { t as listReadOnlyChannelPluginsForConfig } from "./read-only-0zbnWxaR.js";
import { s as getChannelsCommandSecretTargetIds } from "./command-secret-targets-D0VrT_k_.js";
import { t as createClackPrompter } from "./clack-prompter-BSpWH_ak.js";
import { n as awaitWithinDeadline, t as ABSOLUTE_DEADLINE_EXPIRED } from "./absolute-deadline-D0jNXqHr.js";
import { t as commitConfigWithPendingPluginInstalls } from "./install-record-commit-C4pioTsf.js";
import { n as refreshPluginRegistryAfterConfigMutation } from "./registry-refresh-CvO7IUdQ.js";
import { t as resolveCommandConfigWithSecrets } from "./command-config-resolution-CMrTrLUm.js";
import { t as resolveInstallableChannelPlugin } from "./channel-plugin-resolution-DtQTXXc0.js";
import { t as assertAccountSelectorForMutation } from "./account-selector-83VY47P_.js";
import { n as requireValidConfigFileSnapshot } from "./config-validation-C1Wo5r0Q.js";
import { n as readConfiguredParsedLogTail } from "./log-tail-Nn_FQ1QS.js";
import { i as prepareChannelAccountRemoval, n as channelLabel, r as applyPreparedChannelAccountRemoval, t as channelsAddCommand } from "./add-A4MboA85.js";
import { l as shouldUseWizard, s as formatChannelAccountLabel } from "./shared-BZUXWiDY.js";
import { t as channelsListCommand } from "./list-Bs0y488y.js";
import { t as channelsStatusCommand } from "./status-Cp6V4WJ3.js";
//#region src/commands/channels/plugin-config-persistence.ts
async function persistChannelPluginConfig(params) {
const cfg = params.cfg;
if (cfg.plugins?.installs && Object.keys(cfg.plugins.installs).length > 0) {
const committed = await commitConfigWithPendingPluginInstalls({
nextConfig: cfg,
baseHash: params.baseHash
});
await refreshPluginRegistryAfterConfigMutation({
config: committed.config,
reason: "source-changed",
installRecords: committed.installRecords,
logger: { warn: (message) => params.runtime.log(message) }
});
return;
}
await replaceConfigFile({
nextConfig: cfg,
baseHash: params.baseHash
});
if (params.pluginInstalled) await refreshPluginRegistryAfterConfigMutation({
config: cfg,
reason: "source-changed",
logger: { warn: (message) => params.runtime.log(message) }
});
}
//#endregion
//#region src/commands/channels/capabilities.ts
const CHANNEL_CAPABILITIES_TIMEOUT_MAX_MS = 3e4;
async function runChannelCapabilitiesProbe(params) {
try {
const result = await awaitWithinDeadline(async () => params.run(), Date.now() + params.timeoutMs);
return result === ABSOLUTE_DEADLINE_EXPIRED ? {
ok: false,
timedOut: true,
error: `probe timed out after ${params.timeoutMs}ms`
} : result;
} catch (error) {
return {
ok: false,
error: formatErrorMessage(error)
};
}
}
async function runChannelCapabilitiesDiagnostics(params) {
try {
const result = await awaitWithinDeadline(async () => params.run(), Date.now() + params.timeoutMs);
return result === ABSOLUTE_DEADLINE_EXPIRED ? {
lines: [{
text: `Diagnostics: timed out after ${params.timeoutMs}ms`,
tone: "error"
}],
details: { timedOut: true }
} : result;
} catch (error) {
return { lines: [{
text: `Diagnostics: failed (${formatErrorMessage(error)})`,
tone: "error"
}] };
}
}
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) probe = await runChannelCapabilitiesProbe({
timeoutMs,
run: () => plugin.status?.probeAccount?.({
account: resolvedAccount,
timeoutMs,
cfg
})
});
const diagnostics = configured && enabled && plugin.status?.buildCapabilitiesDiagnostics ? await runChannelCapabilitiesDiagnostics({
timeoutMs,
run: () => 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(/* @__PURE__ */ 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;
}
async function resolveCapabilitiesRuntimeConfig(config, runtime) {
return (await resolveCommandConfigWithSecrets({
config,
commandName: "channels",
targetIds: getChannelsCommandSecretTargetIds(),
runtime
})).effectiveConfig;
}
/** Print or serialize configured channel capabilities, actions, and optional health probe details. */
async function channelsCapabilitiesCommand(opts, runtime = defaultRuntime) {
const configSnapshot = await requireValidConfigFileSnapshot(runtime);
if (!configSnapshot) return;
let cfg = await resolveCapabilitiesRuntimeConfig(configSnapshot.config, runtime);
const timeoutMs = Math.min(parseTimeoutMsWithFallback(opts.timeout, 1e4, { invalidType: "error" }), CHANNEL_CAPABILITIES_TIMEOUT_MAX_MS);
const rawChannel = normalizeLowercaseStringOrEmpty(opts.channel);
const rawTarget = normalizeOptionalString(opts.target) ?? "";
if ((!rawChannel || rawChannel === "all") && (opts.account || rawTarget)) {
const message = `${opts.account ? "--account" : "--target"} requires a specific --channel. Run ${formatCliCommand("openclaw channels list")} to choose one.`;
throw new ExpectedCliError({
message,
humanOutput: danger(message),
machineOutput: message
});
}
const plugins = listReadOnlyChannelPluginsForConfig(cfg, { includeSetupFallbackPlugins: true });
const selected = !rawChannel || rawChannel === "all" ? plugins : await (async () => {
const resolved = await resolveInstallableChannelPlugin({
cfg: configSnapshot.sourceConfig,
runtime,
agentId: opts.agent,
rawChannel,
allowInstall: true
});
if (resolved.configChanged) {
await persistChannelPluginConfig({
cfg: resolved.cfg,
pluginInstalled: resolved.pluginInstalled,
baseHash: configSnapshot.hash,
runtime
});
cfg = await resolveCapabilitiesRuntimeConfig(getRuntimeConfig(), runtime);
}
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;
}
const message = formatUnknownChannelMessage({ channel: rawChannel });
throw new ExpectedCliError({
message,
humanOutput: danger(message),
machineOutput: message
});
}
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 formattedProbeLines = report.plugin.status?.formatCapabilitiesProbe?.({ probe: report.probe });
const probeLines = formattedProbeLines?.length ? formattedProbeLines : 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 listManifestChannels() {
return loadPluginManifestRegistryForPluginRegistry({
includeDisabled: true,
env: process.env
}).plugins.flatMap((plugin) => plugin.channels.flatMap((rawChannel) => {
const id = normalizeLowercaseStringOrEmpty(rawChannel);
return id ? [{
id,
pluginId: plugin.id
}] : [];
}));
}
function parseChannelFilter(raw) {
if (raw === void 0) return {
channel: "all",
pluginIds: /* @__PURE__ */ new Set()
};
const trimmed = normalizeLowercaseStringOrEmpty(raw);
if (trimmed === "all") return {
channel: "all",
pluginIds: /* @__PURE__ */ new Set()
};
const manifestChannels = listManifestChannels();
const bundled = normalizeChatChannelId(trimmed);
const channel = bundled ?? trimmed;
const pluginIds = new Set(manifestChannels.filter((entry) => entry.id === channel).map((entry) => entry.pluginId));
if (bundled || pluginIds.size > 0) return {
channel,
pluginIds
};
const manifestIds = [...new Set(manifestChannels.map((entry) => entry.id))].toSorted();
const validChannels = ["all", .../* @__PURE__ */ new Set([...CHAT_CHANNEL_ORDER, ...manifestIds])];
throw new Error(`Unknown channel ${JSON.stringify(raw)}. Valid channels: ${validChannels.join(", ")}`);
}
function matchesChannelContext(value, channel) {
return [channel, `gateway/channels/${channel}`].some((root) => value === root || value?.startsWith(`${root}/`) === true);
}
function matchesChannel(line, filter) {
const { channel } = filter;
if (channel === "all") return true;
return [line.subsystem, line.module].some((value) => matchesChannelContext(value, channel)) || line.plugin !== void 0 && filter.pluginIds.has(line.plugin);
}
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;
}
/** Print or serialize recent log lines matching one channel subsystem/module. */
async function channelsLogsCommand(opts, runtime = defaultRuntime) {
const filter = parseChannelFilter(opts.channel);
const { channel } = filter;
const limit = parseLinesOption(opts.lines);
const tail = await readConfiguredParsedLogTail({
limit,
maxBytes: MAX_BYTES,
filter: (line) => matchesChannel(line, filter)
});
const { lines, truncated } = tail;
if (opts.json) {
writeRuntimeJson(runtime, {
file: tail.file,
channel,
truncated,
lines
});
return;
}
runtime.log(theme.info(`Log file: ${tail.file}`));
if (channel !== "all") runtime.log(theme.info(`Channel: ${channel}`));
if (truncated) runtime.log(theme.warn("Log tail truncated; earlier entries were omitted."));
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.shouldStopRuntime) 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) {
assertAccountSelectorForMutation(opts.account);
const configSnapshot = await requireValidConfigFileSnapshot(runtime);
if (!configSnapshot) return;
const baseHash = configSnapshot.hash;
const cfg = configSnapshot.sourceConfig;
const useWizard = shouldUseWizard(params);
const prompter = useWizard ? createClackPrompter() : null;
const rawChannel = normalizeOptionalString(opts.channel) ?? "";
let lookupChannel = rawChannel;
let channel = normalizeChannelId(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);
const choice = await prompter.select({
message: "Account",
options: ids.map((id) => ({
value: id,
label: id === "default" ? "default (primary)" : id
})),
initialValue: ids[0] ?? "default"
});
return normalizeAccountId(choice);
})();
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-C3glx95R.js");
return await resolveInstallableChannelPlugin({
cfg,
runtime,
agentId: opts.agent,
rawChannel: lookupChannel,
allowInstall: false
});
})() : null;
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 preparedRemoval = prepareChannelAccountRemoval({
plugin,
accountId,
action: deleteConfig ? "delete" : "disable"
});
await stopGatewayRuntimeBeforeRemove({
cfg,
channel: resolvedChannelId,
accountId: preparedRemoval.accountKey,
shouldStopRuntime: preparedRemoval.shouldStopRuntime,
runtime
});
const removal = await applyPreparedChannelAccountRemoval({
cfg,
prepared: preparedRemoval,
runtime
});
if (!removal.ok) {
runtime.error(removal.error.action === "delete" ? `${formatUnsupportedChannelActionMessage({
channel,
action: "delete"
})} Use ${formatCliCommand("openclaw channels remove --channel " + channel)} to disable it without deleting config.` : `${formatUnsupportedChannelActionMessage({
channel,
action: "disable"
})} Use ${formatCliCommand("openclaw channels remove --channel " + channel + " --delete")} only if you want to remove config.`);
runtime.exit(1);
return;
}
await persistChannelPluginConfig({
cfg: removal.value.nextConfig,
pluginInstalled: false,
...baseHash !== void 0 ? { baseHash } : {},
runtime
});
if (useWizard && prompter) await prompter.outro(deleteConfig ? `Deleted ${channelLabel(resolvedChannelId)} account "${preparedRemoval.accountKey}".` : `Disabled ${channelLabel(resolvedChannelId)} account "${preparedRemoval.accountKey}".`);
else runtime.log(deleteConfig ? `Deleted ${channelLabel(resolvedChannelId)} account "${preparedRemoval.accountKey}".` : `Disabled ${channelLabel(resolvedChannelId)} account "${preparedRemoval.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 trimmed = input.trim();
const lowered = normalizeLowercaseStringOrEmpty(trimmed);
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 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 loadedRaw = getRuntimeConfig();
const requestedAgent = opts.agent?.trim();
if (opts.agent !== void 0 && !requestedAgent) throw new Error("--agent must not be blank");
const agentId = requestedAgent ? resolveConfiguredAgentId(loadedRaw, requestedAgent) : void 0;
const { effectiveConfig: cfg } = await resolveCommandConfigWithSecrets({
config: loadedRaw,
commandName: "channels resolve",
targetIds: getChannelsCommandSecretTargetIds(),
agentId,
mode: "read_only_operational",
runtime,
autoEnable: true
});
const explicitChannel = opts.channel?.trim();
const resolvedExplicit = explicitChannel ? await resolveInstallableChannelPlugin({
cfg,
runtime,
agentId,
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.`);
const selection = explicitChannel ? {
channel: resolvedExplicit?.channelId,
plugin: resolvedExplicit?.plugin
} : await resolveMessageChannelSelection({
cfg,
channel: opts.channel ?? null,
agentId
});
const plugin = selection.plugin;
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 };