openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
384 lines (383 loc) • 15.8 kB
JavaScript
import { C as parseStrictNonNegativeInteger } from "./number-coercion-CLj0HTDM.js";
import { c as isRecord } from "./record-coerce-DItp3I4t.js";
import { c as normalizeOptionalLowercaseString } from "./string-coerce-CIXf7egm.js";
import { r as defaultRuntime } from "./runtime-CF2WjnNZ.js";
import { n as createLazyPromise } from "./lazy-promise-DGqyc4Y4.js";
import { n as ok, t as err } from "./result-BQGgYouL.js";
import { t as formatCliCommand } from "./command-format-C7YfyMTd.js";
import "./session-key-BnWWjqNc.js";
import { n as normalizeAccountId } from "./account-id-CETVCrTz.js";
import { n as resolveChannelSetupExecutionAdapter } from "./setup-contract-RbJSZiOH.js";
import { i as getBundledChannelSetupPlugin } from "./bundled-0Ib5Dos0.js";
import { c as formatUnknownChannelMessage, l as formatUnsupportedChannelActionMessage } from "./error-format-Cae7EwnT.js";
import { r as isTerminalInteractive } from "./terminal-interactivity-DXUXAq5U.js";
import { a as normalizeChannelId, n as getLoadedChannelPlugin, t as getChannelPlugin } from "./registry-Cz6cv4VC.js";
import "./plugins-Cozj1Enf.js";
import { r as parseOptionalDelimitedEntries } from "./helpers-VcK_WJ24.js";
import { n as WizardCancelledError } from "./prompts-DLsO8MlU.js";
import { s as moveSingleAccountChannelSectionToDefaultAccount } from "./setup-helpers-DpWE8O4k.js";
import { t as createClackPrompter } from "./clack-prompter-BSpWH_ak.js";
import { t as commitConfigWithPendingPluginInstalls } from "./install-record-commit-C4pioTsf.js";
import { n as refreshPluginRegistryAfterConfigMutation } from "./registry-refresh-CvO7IUdQ.js";
import { n as resolveChannelSetupCliOptionMetadata } from "./cli-add-options-BI7Q9lu2.js";
import { t as resolveChannelSetupOwner } from "./owner-nhq5CyLg.js";
import { t as assertAccountSelectorForMutation } from "./account-selector-83VY47P_.js";
import { n as requireValidConfigFileSnapshot } from "./config-validation-C1Wo5r0Q.js";
import { t as normalizeExternalChannelSetupConfig } from "./config-compatibility-oR4lHgqu.js";
import { l as shouldUseWizard } from "./shared-BZUXWiDY.js";
//#region src/channels/plugins/account-config-mutation.ts
function resolveMissingSetupEnvMessage(plugin, input) {
if (!plugin.setupContract || !isRecord(input) || input.useEnv !== true) return;
const useEnvField = plugin.setupContract.metadata.fields.find((field) => field.kind === "boolean" && field.key === "useEnv");
if (!useEnvField?.envVars?.length) return;
const { envVars, envVarMode } = useEnvField;
const missing = envVars.filter((name) => !process.env[name]?.trim());
if (envVarMode === "any" ? missing.length < envVars.length : !missing.length) return;
return envVarMode === "any" ? `Set one of these environment variables before using --use-env: ${missing.join(", ")}.` : `Set these environment variables before using --use-env: ${missing.join(", ")}.`;
}
async function prepareChannelAccountConfiguration(params) {
const setup = resolveChannelSetupExecutionAdapter(params.plugin);
if (!setup?.applyAccountConfig) return err({ kind: "unsupported" });
const rawInput = params.resolveInput();
let input;
if (params.plugin.setupContract) {
const parsed = params.plugin.setupContract.parseInput(rawInput);
if (!parsed.ok) return err({
kind: "invalid-input",
message: parsed.error
});
input = parsed.value;
} else input = rawInput;
const accountId = setup.resolveAccountId?.({
cfg: params.cfg,
accountId: params.requestedAccountId,
input
}) ?? normalizeAccountId(params.requestedAccountId);
if (setup.prepareAccountConfigInput) {
await params.beforePersistentEffect?.();
input = await setup.prepareAccountConfigInput({
cfg: params.cfg,
accountId,
input,
runtime: params.runtime
});
}
const validationError = setup.validateInput?.({
cfg: params.cfg,
accountId,
input
});
if (validationError) return err({
kind: "invalid-input",
message: validationError
});
const missingEnvMessage = resolveMissingSetupEnvMessage(params.plugin, input);
if (missingEnvMessage) return err({
kind: "invalid-input",
message: missingEnvMessage
});
return ok({
plugin: params.plugin,
setup,
applyAccountConfig: setup.applyAccountConfig,
accountId,
input
});
}
async function applyPreparedChannelAccountConfiguration(params) {
const { accountId, applyAccountConfig, input, plugin, setup } = params.prepared;
const configAccountId = normalizeAccountId(accountId);
let nextConfig = params.cfg;
if (accountId !== "default") nextConfig = moveSingleAccountChannelSectionToDefaultAccount({
cfg: nextConfig,
channelKey: params.channel,
setupSurface: setup
});
nextConfig = applyAccountConfig({
cfg: nextConfig,
accountId: configAccountId,
input
});
if (plugin.lifecycle?.onAccountConfigChanged) {
await params.beforePersistentEffect?.();
await plugin.lifecycle.onAccountConfigChanged({
prevCfg: params.cfg,
nextCfg: nextConfig,
accountId,
runtime: params.runtime
});
}
return {
nextConfig,
accountId,
input,
...setup.afterAccountConfigWritten ? { afterAccountConfigWritten: setup.afterAccountConfigWritten } : {}
};
}
function prepareChannelAccountRemoval(params) {
const accountId = normalizeAccountId(params.accountId);
return {
plugin: params.plugin,
action: params.action,
accountId,
accountKey: accountId || "default",
shouldStopRuntime: Boolean(params.plugin.gateway?.startAccount || params.plugin.gateway?.logoutAccount)
};
}
async function applyPreparedChannelAccountRemoval(params) {
const { accountId, action, plugin } = params.prepared;
if (action === "delete") {
if (!plugin.config.deleteAccount) return err({
kind: "unsupported-action",
action
});
const nextConfig = plugin.config.deleteAccount({
cfg: { ...params.cfg },
accountId
});
await plugin.lifecycle?.onAccountRemoved?.({
prevCfg: params.cfg,
accountId,
runtime: params.runtime
});
return ok({ nextConfig });
}
if (!plugin.config.setAccountEnabled) return err({
kind: "unsupported-action",
action
});
const nextConfig = plugin.config.setAccountEnabled({
cfg: { ...params.cfg },
accountId,
enabled: false
});
await plugin.lifecycle?.onAccountConfigChanged?.({
prevCfg: params.cfg,
nextCfg: nextConfig,
accountId,
runtime: params.runtime
});
return ok({ nextConfig });
}
//#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 loadChannelSetupPluginInstall = createLazyPromise(() => import("./plugin-install-0YeHETcW.js"));
const loadOnboardChannels = createLazyPromise(() => import("./onboard-channels-wf3-q-wO.js"));
const CHANNEL_ADD_CONTROL_OPTION_KEYS = /* @__PURE__ */ new Set([
"agent",
"channel",
"account"
]);
async function resolveCatalogChannelEntry(raw, cfg, resolveWorkspaceDir) {
const trimmed = normalizeOptionalLowercaseString(raw);
if (!trimmed) return;
return (await import("./trusted-catalog-gRbM8cDN.js").then(({ listTrustedChannelPluginCatalogEntries }) => listTrustedChannelPluginCatalogEntries({
cfg,
workspaceDir: resolveWorkspaceDir()
}))).find((entry) => {
if (normalizeOptionalLowercaseString(entry.id) === trimmed) return true;
return (entry.meta.aliases ?? []).some((alias) => normalizeOptionalLowercaseString(alias) === trimmed);
});
}
function buildChannelSetupInput(opts) {
const input = {};
const { valueMetadataByAttributeName } = resolveChannelSetupCliOptionMetadata(opts.channel);
for (const [key, value] of Object.entries(opts)) {
if (CHANNEL_ADD_CONTROL_OPTION_KEYS.has(key) || value === void 0) continue;
const metadata = valueMetadataByAttributeName.get(key);
if (metadata?.valueType !== "int") {
input[key] = metadata?.valueType === "list" ? Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : parseOptionalDelimitedEntries(typeof value === "string" ? value : void 0) : value;
continue;
}
if (value === null || value === "") {
input[key] = void 0;
continue;
}
const parsed = parseStrictNonNegativeInteger(value);
if (parsed === void 0) throw new Error(`${metadata.longFlag} must be a non-negative integer.`);
input[key] = parsed;
}
return input;
}
function buildChannelOwnedSetupInput(opts) {
return Object.fromEntries(Object.entries(opts).filter(([key, value]) => !CHANNEL_ADD_CONTROL_OPTION_KEYS.has(key) && value !== void 0));
}
/** 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) {
assertAccountSelectorForMutation(opts.account);
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 { resolveInitialWizardChannelTarget, runChannelsAddWizardFlow } = await import("./add-wizard-WuC27TqQ.js");
const workspaceDir = opts.agent === void 0 ? void 0 : resolveChannelSetupOwner(cfg, opts.agent).workspaceDir;
const target = await resolveInitialWizardChannelTarget(opts.channel, cfg, workspaceDir);
if (target.kind === "unresolved") {
runtime.error(target.message);
runtime.exit(1);
return;
}
if (!isTerminalInteractive()) {
runtime.error("Interactive channel setup requires a TTY. Use `openclaw channels add --channel <id> --use-env` or pass the channel's credential flags for non-interactive setup.");
runtime.exit(1);
return;
}
await runChannelsAddWizardFlow({
cfg,
...baseHash !== void 0 ? { baseHash } : {},
runtime,
prompter: createClackPrompter(),
...workspaceDir ? { workspaceDir } : {},
...target.kind === "resolved" ? { initialChannel: target.channel } : {},
...params?.beforePersistentEffect ? { beforePersistentEffect: params.beforePersistentEffect } : {}
});
return;
}
const rawChannel = opts.channel ?? "";
let channel = normalizeChannelId(rawChannel);
let preparedWorkspaceDir;
const resolveWorkspaceDir = () => preparedWorkspaceDir ??= resolveChannelSetupOwner(cfg, opts.agent).workspaceDir;
let catalogEntry = await resolveCatalogChannelEntry(rawChannel, nextConfig, resolveWorkspaceDir);
const loadScopedPlugin = async (channelId, pluginId) => {
const existing = getLoadedChannelPlugin(channelId);
if (existing?.setupContract?.applyAccountConfig || 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-DehDhH5q.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,
...params?.beforePersistentEffect ? { beforePersistentEffect: params.beforePersistentEffect } : {}
});
nextConfig = result.cfg;
if (!result.installed) return;
pluginRegistrySourceChanged = true;
catalogEntry = {
...catalogEntry,
...result.pluginId ? { pluginId: result.pluginId } : {}
};
}
channel ??= normalizeChannelId(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) {
runtime.error(`${formatUnsupportedChannelActionMessage({
channel,
action: "non-interactive add"
})} Run ${formatCliCommand("openclaw channels add")} with no flags for guided setup.`);
runtime.exit(1);
return;
}
const prepared = await prepareChannelAccountConfiguration({
cfg: nextConfig,
plugin,
requestedAccountId: opts.account,
resolveInput: () => plugin.setupContract ? buildChannelOwnedSetupInput(opts) : buildChannelSetupInput(opts),
runtime,
...params?.beforePersistentEffect ? { beforePersistentEffect: params.beforePersistentEffect } : {}
});
if (!prepared.ok) {
runtime.error(prepared.error.kind === "unsupported" ? `${formatUnsupportedChannelActionMessage({
channel,
action: "non-interactive add"
})} Run ${formatCliCommand("openclaw channels add")} with no flags for guided setup.` : prepared.error.message);
runtime.exit(1);
return;
}
const applied = await applyPreparedChannelAccountConfiguration({
cfg: nextConfig,
channel,
prepared: prepared.value,
runtime,
...params?.beforePersistentEffect ? { beforePersistentEffect: params.beforePersistentEffect } : {}
});
nextConfig = normalizeExternalChannelSetupConfig({
cfg: applied.nextConfig,
channel
});
await params?.beforePersistentEffect?.();
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 "${applied.accountId}".`);
const afterAccountConfigWritten = applied.afterAccountConfigWritten;
if (afterAccountConfigWritten) {
const { runCollectedChannelOnboardingPostWriteHooks } = await loadOnboardChannels();
await runCollectedChannelOnboardingPostWriteHooks({
hooks: [{
channel,
accountId: applied.accountId,
run: async ({ cfg: writtenCfg, runtime: hookRuntime }) => await afterAccountConfigWritten({
previousCfg: cfg,
cfg: writtenCfg,
accountId: applied.accountId,
input: applied.input,
runtime: hookRuntime
})
}],
cfg: writtenConfig,
runtime,
...params?.beforePersistentEffect ? { beforePersistentEffect: params.beforePersistentEffect } : {}
});
}
}
//#endregion
export { prepareChannelAccountRemoval as i, channelLabel as n, applyPreparedChannelAccountRemoval as r, channelsAddCommand as t };