670 lines
22 KiB
JavaScript
670 lines
22 KiB
JavaScript
import { Z as loadOpenClawPlugins } from "./reply-5K4M-ATj.js";
|
|
import { E as DEFAULT_ACCOUNT_ID, M as normalizeAccountId, c as resolveDefaultAgentId, s as resolveAgentWorkspaceDir } from "./agent-scope-CrgUOY3f.js";
|
|
import { _ as formatChannelSelectionLine, g as formatChannelPrimerLine, t as createSubsystemLogger, y as listChatChannels } from "./subsystem-46MXi6Ip.js";
|
|
import { t as formatCliCommand } from "./command-format-BQK1OIvH.js";
|
|
import { n as listChannelPlugins, t as getChannelPlugin } from "./plugins-D1CxUobm.js";
|
|
import { t as formatDocsLink } from "./links-C9fyAH-V.js";
|
|
import { t as resolveChannelDefaultAccountId } from "./helpers-Cw9kFCkw.js";
|
|
import { i as listChannelPluginCatalogEntries, n as isChannelConfigured } from "./plugin-auto-enable-JQ63k0Ce.js";
|
|
import { n as installPluginFromNpmSpec, t as recordPluginInstall } from "./installs-W4Pc1LJz.js";
|
|
import path from "node:path";
|
|
import fs from "node:fs";
|
|
|
|
//#region src/plugins/enable.ts
|
|
function ensureAllowlisted(cfg, pluginId) {
|
|
const allow = cfg.plugins?.allow;
|
|
if (!Array.isArray(allow) || allow.includes(pluginId)) return cfg;
|
|
return {
|
|
...cfg,
|
|
plugins: {
|
|
...cfg.plugins,
|
|
allow: [...allow, pluginId]
|
|
}
|
|
};
|
|
}
|
|
function enablePluginInConfig(cfg, pluginId) {
|
|
if (cfg.plugins?.enabled === false) return {
|
|
config: cfg,
|
|
enabled: false,
|
|
reason: "plugins disabled"
|
|
};
|
|
if (cfg.plugins?.deny?.includes(pluginId)) return {
|
|
config: cfg,
|
|
enabled: false,
|
|
reason: "blocked by denylist"
|
|
};
|
|
const entries = {
|
|
...cfg.plugins?.entries,
|
|
[pluginId]: {
|
|
...cfg.plugins?.entries?.[pluginId],
|
|
enabled: true
|
|
}
|
|
};
|
|
let next = {
|
|
...cfg,
|
|
plugins: {
|
|
...cfg.plugins,
|
|
entries
|
|
}
|
|
};
|
|
next = ensureAllowlisted(next, pluginId);
|
|
return {
|
|
config: next,
|
|
enabled: true
|
|
};
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/commands/onboarding/plugin-install.ts
|
|
function hasGitWorkspace(workspaceDir) {
|
|
const candidates = /* @__PURE__ */ new Set();
|
|
candidates.add(path.join(process.cwd(), ".git"));
|
|
if (workspaceDir && workspaceDir !== process.cwd()) candidates.add(path.join(workspaceDir, ".git"));
|
|
for (const candidate of candidates) if (fs.existsSync(candidate)) return true;
|
|
return false;
|
|
}
|
|
function resolveLocalPath(entry, workspaceDir, allowLocal) {
|
|
if (!allowLocal) return null;
|
|
const raw = entry.install.localPath?.trim();
|
|
if (!raw) return null;
|
|
const candidates = /* @__PURE__ */ new Set();
|
|
candidates.add(path.resolve(process.cwd(), raw));
|
|
if (workspaceDir && workspaceDir !== process.cwd()) candidates.add(path.resolve(workspaceDir, raw));
|
|
for (const candidate of candidates) if (fs.existsSync(candidate)) return candidate;
|
|
return null;
|
|
}
|
|
function addPluginLoadPath(cfg, pluginPath) {
|
|
const existing = cfg.plugins?.load?.paths ?? [];
|
|
const merged = Array.from(new Set([...existing, pluginPath]));
|
|
return {
|
|
...cfg,
|
|
plugins: {
|
|
...cfg.plugins,
|
|
load: {
|
|
...cfg.plugins?.load,
|
|
paths: merged
|
|
}
|
|
}
|
|
};
|
|
}
|
|
async function promptInstallChoice(params) {
|
|
const { entry, localPath, prompter, defaultChoice } = params;
|
|
const localOptions = localPath ? [{
|
|
value: "local",
|
|
label: "Use local plugin path",
|
|
hint: localPath
|
|
}] : [];
|
|
const options = [
|
|
{
|
|
value: "npm",
|
|
label: `Download from npm (${entry.install.npmSpec})`
|
|
},
|
|
...localOptions,
|
|
{
|
|
value: "skip",
|
|
label: "Skip for now"
|
|
}
|
|
];
|
|
const initialValue = defaultChoice === "local" && !localPath ? "npm" : defaultChoice;
|
|
return await prompter.select({
|
|
message: `Install ${entry.meta.label} plugin?`,
|
|
options,
|
|
initialValue
|
|
});
|
|
}
|
|
function resolveInstallDefaultChoice(params) {
|
|
const { cfg, entry, localPath } = params;
|
|
const updateChannel = cfg.update?.channel;
|
|
if (updateChannel === "dev") return localPath ? "local" : "npm";
|
|
if (updateChannel === "stable" || updateChannel === "beta") return "npm";
|
|
const entryDefault = entry.install.defaultChoice;
|
|
if (entryDefault === "local") return localPath ? "local" : "npm";
|
|
if (entryDefault === "npm") return "npm";
|
|
return localPath ? "local" : "npm";
|
|
}
|
|
async function ensureOnboardingPluginInstalled(params) {
|
|
const { entry, prompter, runtime, workspaceDir } = params;
|
|
let next = params.cfg;
|
|
const localPath = resolveLocalPath(entry, workspaceDir, hasGitWorkspace(workspaceDir));
|
|
const choice = await promptInstallChoice({
|
|
entry,
|
|
localPath,
|
|
defaultChoice: resolveInstallDefaultChoice({
|
|
cfg: next,
|
|
entry,
|
|
localPath
|
|
}),
|
|
prompter
|
|
});
|
|
if (choice === "skip") return {
|
|
cfg: next,
|
|
installed: false
|
|
};
|
|
if (choice === "local" && localPath) {
|
|
next = addPluginLoadPath(next, localPath);
|
|
next = enablePluginInConfig(next, entry.id).config;
|
|
return {
|
|
cfg: next,
|
|
installed: true
|
|
};
|
|
}
|
|
const result = await installPluginFromNpmSpec({
|
|
spec: entry.install.npmSpec,
|
|
logger: {
|
|
info: (msg) => runtime.log?.(msg),
|
|
warn: (msg) => runtime.log?.(msg)
|
|
}
|
|
});
|
|
if (result.ok) {
|
|
next = enablePluginInConfig(next, result.pluginId).config;
|
|
next = recordPluginInstall(next, {
|
|
pluginId: result.pluginId,
|
|
source: "npm",
|
|
spec: entry.install.npmSpec,
|
|
installPath: result.targetDir,
|
|
version: result.version
|
|
});
|
|
return {
|
|
cfg: next,
|
|
installed: true
|
|
};
|
|
}
|
|
await prompter.note(`Failed to install ${entry.install.npmSpec}: ${result.error}`, "Plugin install");
|
|
if (localPath) {
|
|
if (await prompter.confirm({
|
|
message: `Use local plugin path instead? (${localPath})`,
|
|
initialValue: true
|
|
})) {
|
|
next = addPluginLoadPath(next, localPath);
|
|
next = enablePluginInConfig(next, entry.id).config;
|
|
return {
|
|
cfg: next,
|
|
installed: true
|
|
};
|
|
}
|
|
}
|
|
runtime.error?.(`Plugin install failed: ${result.error}`);
|
|
return {
|
|
cfg: next,
|
|
installed: false
|
|
};
|
|
}
|
|
function reloadOnboardingPluginRegistry(params) {
|
|
const workspaceDir = params.workspaceDir ?? resolveAgentWorkspaceDir(params.cfg, resolveDefaultAgentId(params.cfg));
|
|
const log = createSubsystemLogger("plugins");
|
|
loadOpenClawPlugins({
|
|
config: params.cfg,
|
|
workspaceDir,
|
|
cache: false,
|
|
logger: {
|
|
info: (msg) => log.info(msg),
|
|
warn: (msg) => log.warn(msg),
|
|
error: (msg) => log.error(msg),
|
|
debug: (msg) => log.debug(msg)
|
|
}
|
|
});
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/commands/onboarding/registry.ts
|
|
const CHANNEL_ONBOARDING_ADAPTERS = () => new Map(listChannelPlugins().map((plugin) => plugin.onboarding ? [plugin.id, plugin.onboarding] : null).filter((entry) => Boolean(entry)));
|
|
function getChannelOnboardingAdapter(channel) {
|
|
return CHANNEL_ONBOARDING_ADAPTERS().get(channel);
|
|
}
|
|
function listChannelOnboardingAdapters() {
|
|
return Array.from(CHANNEL_ONBOARDING_ADAPTERS().values());
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/commands/onboard-channels.ts
|
|
function formatAccountLabel(accountId) {
|
|
return accountId === DEFAULT_ACCOUNT_ID ? "default (primary)" : accountId;
|
|
}
|
|
async function promptConfiguredAction(params) {
|
|
const { prompter, label, supportsDisable, supportsDelete } = params;
|
|
const updateOption = {
|
|
value: "update",
|
|
label: "Modify settings"
|
|
};
|
|
const disableOption = {
|
|
value: "disable",
|
|
label: "Disable (keeps config)"
|
|
};
|
|
const deleteOption = {
|
|
value: "delete",
|
|
label: "Delete config"
|
|
};
|
|
const skipOption = {
|
|
value: "skip",
|
|
label: "Skip (leave as-is)"
|
|
};
|
|
const options = [
|
|
updateOption,
|
|
...supportsDisable ? [disableOption] : [],
|
|
...supportsDelete ? [deleteOption] : [],
|
|
skipOption
|
|
];
|
|
return await prompter.select({
|
|
message: `${label} already configured. What do you want to do?`,
|
|
options,
|
|
initialValue: "update"
|
|
});
|
|
}
|
|
async function promptRemovalAccountId(params) {
|
|
const { cfg, prompter, label, channel } = params;
|
|
const plugin = getChannelPlugin(channel);
|
|
if (!plugin) return DEFAULT_ACCOUNT_ID;
|
|
const accountIds = plugin.config.listAccountIds(cfg).filter(Boolean);
|
|
const defaultAccountId = resolveChannelDefaultAccountId({
|
|
plugin,
|
|
cfg,
|
|
accountIds
|
|
});
|
|
if (accountIds.length <= 1) return defaultAccountId;
|
|
return normalizeAccountId(await prompter.select({
|
|
message: `${label} account`,
|
|
options: accountIds.map((accountId) => ({
|
|
value: accountId,
|
|
label: formatAccountLabel(accountId)
|
|
})),
|
|
initialValue: defaultAccountId
|
|
})) ?? defaultAccountId;
|
|
}
|
|
async function collectChannelStatus(params) {
|
|
const installedPlugins = listChannelPlugins();
|
|
const installedIds = new Set(installedPlugins.map((plugin) => plugin.id));
|
|
const catalogEntries = listChannelPluginCatalogEntries({ workspaceDir: resolveAgentWorkspaceDir(params.cfg, resolveDefaultAgentId(params.cfg)) }).filter((entry) => !installedIds.has(entry.id));
|
|
const statusEntries = await Promise.all(listChannelOnboardingAdapters().map((adapter) => adapter.getStatus({
|
|
cfg: params.cfg,
|
|
options: params.options,
|
|
accountOverrides: params.accountOverrides
|
|
})));
|
|
const statusByChannel = new Map(statusEntries.map((entry) => [entry.channel, entry]));
|
|
const fallbackStatuses = listChatChannels().filter((meta) => !statusByChannel.has(meta.id)).map((meta) => {
|
|
const configured = isChannelConfigured(params.cfg, meta.id);
|
|
const statusLabel = configured ? "configured (plugin disabled)" : "not configured";
|
|
return {
|
|
channel: meta.id,
|
|
configured,
|
|
statusLines: [`${meta.label}: ${statusLabel}`],
|
|
selectionHint: configured ? "configured · plugin disabled" : "not configured",
|
|
quickstartScore: 0
|
|
};
|
|
});
|
|
const catalogStatuses = catalogEntries.map((entry) => ({
|
|
channel: entry.id,
|
|
configured: false,
|
|
statusLines: [`${entry.meta.label}: install plugin to enable`],
|
|
selectionHint: "plugin · install",
|
|
quickstartScore: 0
|
|
}));
|
|
const combinedStatuses = [
|
|
...statusEntries,
|
|
...fallbackStatuses,
|
|
...catalogStatuses
|
|
];
|
|
return {
|
|
installedPlugins,
|
|
catalogEntries,
|
|
statusByChannel: new Map(combinedStatuses.map((entry) => [entry.channel, entry])),
|
|
statusLines: combinedStatuses.flatMap((entry) => entry.statusLines)
|
|
};
|
|
}
|
|
async function noteChannelStatus(params) {
|
|
const { statusLines } = await collectChannelStatus({
|
|
cfg: params.cfg,
|
|
options: params.options,
|
|
accountOverrides: params.accountOverrides ?? {}
|
|
});
|
|
if (statusLines.length > 0) await params.prompter.note(statusLines.join("\n"), "Channel status");
|
|
}
|
|
async function noteChannelPrimer(prompter, channels) {
|
|
const channelLines = channels.map((channel) => formatChannelPrimerLine({
|
|
id: channel.id,
|
|
label: channel.label,
|
|
selectionLabel: channel.label,
|
|
docsPath: "/",
|
|
blurb: channel.blurb
|
|
}));
|
|
await prompter.note([
|
|
"DM security: default is pairing; unknown DMs get a pairing code.",
|
|
`Approve with: ${formatCliCommand("openclaw pairing approve <channel> <code>")}`,
|
|
"Public DMs require dmPolicy=\"open\" + allowFrom=[\"*\"].",
|
|
"Multi-user DMs: set session.dmScope=\"per-channel-peer\" (or \"per-account-channel-peer\" for multi-account channels) to isolate sessions.",
|
|
`Docs: ${formatDocsLink("/start/pairing", "start/pairing")}`,
|
|
"",
|
|
...channelLines
|
|
].join("\n"), "How channels work");
|
|
}
|
|
function resolveQuickstartDefault(statusByChannel) {
|
|
let best = null;
|
|
for (const [channel, status] of statusByChannel) {
|
|
if (status.quickstartScore == null) continue;
|
|
if (!best || status.quickstartScore > best.score) best = {
|
|
channel,
|
|
score: status.quickstartScore
|
|
};
|
|
}
|
|
return best?.channel;
|
|
}
|
|
async function maybeConfigureDmPolicies(params) {
|
|
const { selection, prompter, accountIdsByChannel } = params;
|
|
const dmPolicies = selection.map((channel) => getChannelOnboardingAdapter(channel)?.dmPolicy).filter(Boolean);
|
|
if (dmPolicies.length === 0) return params.cfg;
|
|
if (!await prompter.confirm({
|
|
message: "Configure DM access policies now? (default: pairing)",
|
|
initialValue: false
|
|
})) return params.cfg;
|
|
let cfg = params.cfg;
|
|
const selectPolicy = async (policy) => {
|
|
await prompter.note([
|
|
"Default: pairing (unknown DMs get a pairing code).",
|
|
`Approve: ${formatCliCommand(`openclaw pairing approve ${policy.channel} <code>`)}`,
|
|
`Allowlist DMs: ${policy.policyKey}="allowlist" + ${policy.allowFromKey} entries.`,
|
|
`Public DMs: ${policy.policyKey}="open" + ${policy.allowFromKey} includes "*".`,
|
|
"Multi-user DMs: set session.dmScope=\"per-channel-peer\" (or \"per-account-channel-peer\" for multi-account channels) to isolate sessions.",
|
|
`Docs: ${formatDocsLink("/start/pairing", "start/pairing")}`
|
|
].join("\n"), `${policy.label} DM access`);
|
|
return await prompter.select({
|
|
message: `${policy.label} DM policy`,
|
|
options: [
|
|
{
|
|
value: "pairing",
|
|
label: "Pairing (recommended)"
|
|
},
|
|
{
|
|
value: "allowlist",
|
|
label: "Allowlist (specific users only)"
|
|
},
|
|
{
|
|
value: "open",
|
|
label: "Open (public inbound DMs)"
|
|
},
|
|
{
|
|
value: "disabled",
|
|
label: "Disabled (ignore DMs)"
|
|
}
|
|
]
|
|
});
|
|
};
|
|
for (const policy of dmPolicies) {
|
|
const current = policy.getCurrent(cfg);
|
|
const nextPolicy = await selectPolicy(policy);
|
|
if (nextPolicy !== current) cfg = policy.setPolicy(cfg, nextPolicy);
|
|
if (nextPolicy === "allowlist" && policy.promptAllowFrom) cfg = await policy.promptAllowFrom({
|
|
cfg,
|
|
prompter,
|
|
accountId: accountIdsByChannel?.get(policy.channel)
|
|
});
|
|
}
|
|
return cfg;
|
|
}
|
|
async function setupChannels(cfg, runtime, prompter, options) {
|
|
let next = cfg;
|
|
const forceAllowFromChannels = new Set(options?.forceAllowFromChannels ?? []);
|
|
const accountOverrides = { ...options?.accountIds };
|
|
if (options?.whatsappAccountId?.trim()) accountOverrides.whatsapp = options.whatsappAccountId.trim();
|
|
const { installedPlugins, catalogEntries, statusByChannel, statusLines } = await collectChannelStatus({
|
|
cfg: next,
|
|
options,
|
|
accountOverrides
|
|
});
|
|
if (!options?.skipStatusNote && statusLines.length > 0) await prompter.note(statusLines.join("\n"), "Channel status");
|
|
if (!(options?.skipConfirm ? true : await prompter.confirm({
|
|
message: "Configure chat channels now?",
|
|
initialValue: true
|
|
}))) return cfg;
|
|
const corePrimer = listChatChannels().map((meta) => ({
|
|
id: meta.id,
|
|
label: meta.label,
|
|
blurb: meta.blurb
|
|
}));
|
|
const coreIds = new Set(corePrimer.map((entry) => entry.id));
|
|
await noteChannelPrimer(prompter, [
|
|
...corePrimer,
|
|
...installedPlugins.filter((plugin) => !coreIds.has(plugin.id)).map((plugin) => ({
|
|
id: plugin.id,
|
|
label: plugin.meta.label,
|
|
blurb: plugin.meta.blurb
|
|
})),
|
|
...catalogEntries.filter((entry) => !coreIds.has(entry.id)).map((entry) => ({
|
|
id: entry.id,
|
|
label: entry.meta.label,
|
|
blurb: entry.meta.blurb
|
|
}))
|
|
]);
|
|
const quickstartDefault = options?.initialSelection?.[0] ?? resolveQuickstartDefault(statusByChannel);
|
|
const shouldPromptAccountIds = options?.promptAccountIds === true;
|
|
const accountIdsByChannel = /* @__PURE__ */ new Map();
|
|
const recordAccount = (channel, accountId) => {
|
|
options?.onAccountId?.(channel, accountId);
|
|
getChannelOnboardingAdapter(channel)?.onAccountRecorded?.(accountId, options);
|
|
accountIdsByChannel.set(channel, accountId);
|
|
};
|
|
const selection = [];
|
|
const addSelection = (channel) => {
|
|
if (!selection.includes(channel)) selection.push(channel);
|
|
};
|
|
const resolveDisabledHint = (channel) => {
|
|
const plugin = getChannelPlugin(channel);
|
|
if (!plugin) {
|
|
if (next.plugins?.entries?.[channel]?.enabled === false) return "plugin disabled";
|
|
if (next.plugins?.enabled === false) return "plugins disabled";
|
|
return;
|
|
}
|
|
const accountId = resolveChannelDefaultAccountId({
|
|
plugin,
|
|
cfg: next
|
|
});
|
|
const account = plugin.config.resolveAccount(next, accountId);
|
|
let enabled;
|
|
if (plugin.config.isEnabled) enabled = plugin.config.isEnabled(account, next);
|
|
else if (typeof account?.enabled === "boolean") enabled = account.enabled;
|
|
else if (typeof next.channels?.[channel]?.enabled === "boolean") enabled = next.channels[channel]?.enabled;
|
|
return enabled === false ? "disabled" : void 0;
|
|
};
|
|
const buildSelectionOptions = (entries) => entries.map((entry) => {
|
|
const status = statusByChannel.get(entry.id);
|
|
const disabledHint = resolveDisabledHint(entry.id);
|
|
const hint = [status?.selectionHint, disabledHint].filter(Boolean).join(" · ") || void 0;
|
|
return {
|
|
value: entry.meta.id,
|
|
label: entry.meta.selectionLabel ?? entry.meta.label,
|
|
...hint ? { hint } : {}
|
|
};
|
|
});
|
|
const getChannelEntries = () => {
|
|
const core = listChatChannels();
|
|
const installed = listChannelPlugins();
|
|
const installedIds = new Set(installed.map((plugin) => plugin.id));
|
|
const catalog = listChannelPluginCatalogEntries({ workspaceDir: resolveAgentWorkspaceDir(next, resolveDefaultAgentId(next)) }).filter((entry) => !installedIds.has(entry.id));
|
|
const metaById = /* @__PURE__ */ new Map();
|
|
for (const meta of core) metaById.set(meta.id, meta);
|
|
for (const plugin of installed) metaById.set(plugin.id, plugin.meta);
|
|
for (const entry of catalog) if (!metaById.has(entry.id)) metaById.set(entry.id, entry.meta);
|
|
return {
|
|
entries: Array.from(metaById, ([id, meta]) => ({
|
|
id,
|
|
meta
|
|
})),
|
|
catalog,
|
|
catalogById: new Map(catalog.map((entry) => [entry.id, entry]))
|
|
};
|
|
};
|
|
const refreshStatus = async (channel) => {
|
|
const adapter = getChannelOnboardingAdapter(channel);
|
|
if (!adapter) return;
|
|
const status = await adapter.getStatus({
|
|
cfg: next,
|
|
options,
|
|
accountOverrides
|
|
});
|
|
statusByChannel.set(channel, status);
|
|
};
|
|
const ensureBundledPluginEnabled = async (channel) => {
|
|
if (getChannelPlugin(channel)) return true;
|
|
const result = enablePluginInConfig(next, channel);
|
|
next = result.config;
|
|
if (!result.enabled) {
|
|
await prompter.note(`Cannot enable ${channel}: ${result.reason ?? "plugin disabled"}.`, "Channel setup");
|
|
return false;
|
|
}
|
|
const workspaceDir = resolveAgentWorkspaceDir(next, resolveDefaultAgentId(next));
|
|
reloadOnboardingPluginRegistry({
|
|
cfg: next,
|
|
runtime,
|
|
workspaceDir
|
|
});
|
|
if (!getChannelPlugin(channel)) {
|
|
await prompter.note(`${channel} plugin not available.`, "Channel setup");
|
|
return false;
|
|
}
|
|
await refreshStatus(channel);
|
|
return true;
|
|
};
|
|
const configureChannel = async (channel) => {
|
|
const adapter = getChannelOnboardingAdapter(channel);
|
|
if (!adapter) {
|
|
await prompter.note(`${channel} does not support onboarding yet.`, "Channel setup");
|
|
return;
|
|
}
|
|
const result = await adapter.configure({
|
|
cfg: next,
|
|
runtime,
|
|
prompter,
|
|
options,
|
|
accountOverrides,
|
|
shouldPromptAccountIds,
|
|
forceAllowFrom: forceAllowFromChannels.has(channel)
|
|
});
|
|
next = result.cfg;
|
|
if (result.accountId) recordAccount(channel, result.accountId);
|
|
addSelection(channel);
|
|
await refreshStatus(channel);
|
|
};
|
|
const handleConfiguredChannel = async (channel, label) => {
|
|
const plugin = getChannelPlugin(channel);
|
|
const adapter = getChannelOnboardingAdapter(channel);
|
|
const supportsDisable = Boolean(options?.allowDisable && (plugin?.config.setAccountEnabled || adapter?.disable));
|
|
const supportsDelete = Boolean(options?.allowDisable && plugin?.config.deleteAccount);
|
|
const action = await promptConfiguredAction({
|
|
prompter,
|
|
label,
|
|
supportsDisable,
|
|
supportsDelete
|
|
});
|
|
if (action === "skip") return;
|
|
if (action === "update") {
|
|
await configureChannel(channel);
|
|
return;
|
|
}
|
|
if (!options?.allowDisable) return;
|
|
if (action === "delete" && !supportsDelete) {
|
|
await prompter.note(`${label} does not support deleting config entries.`, "Remove channel");
|
|
return;
|
|
}
|
|
const resolvedAccountId = normalizeAccountId((action === "delete" ? Boolean(plugin?.config.deleteAccount) : Boolean(plugin?.config.setAccountEnabled)) ? await promptRemovalAccountId({
|
|
cfg: next,
|
|
prompter,
|
|
label,
|
|
channel
|
|
}) : DEFAULT_ACCOUNT_ID) ?? (plugin ? resolveChannelDefaultAccountId({
|
|
plugin,
|
|
cfg: next
|
|
}) : DEFAULT_ACCOUNT_ID);
|
|
const accountLabel = formatAccountLabel(resolvedAccountId);
|
|
if (action === "delete") {
|
|
if (!await prompter.confirm({
|
|
message: `Delete ${label} account "${accountLabel}"?`,
|
|
initialValue: false
|
|
})) return;
|
|
if (plugin?.config.deleteAccount) next = plugin.config.deleteAccount({
|
|
cfg: next,
|
|
accountId: resolvedAccountId
|
|
});
|
|
await refreshStatus(channel);
|
|
return;
|
|
}
|
|
if (plugin?.config.setAccountEnabled) next = plugin.config.setAccountEnabled({
|
|
cfg: next,
|
|
accountId: resolvedAccountId,
|
|
enabled: false
|
|
});
|
|
else if (adapter?.disable) next = adapter.disable(next);
|
|
await refreshStatus(channel);
|
|
};
|
|
const handleChannelChoice = async (channel) => {
|
|
const { catalogById } = getChannelEntries();
|
|
const catalogEntry = catalogById.get(channel);
|
|
if (catalogEntry) {
|
|
const workspaceDir = resolveAgentWorkspaceDir(next, resolveDefaultAgentId(next));
|
|
const result = await ensureOnboardingPluginInstalled({
|
|
cfg: next,
|
|
entry: catalogEntry,
|
|
prompter,
|
|
runtime,
|
|
workspaceDir
|
|
});
|
|
next = result.cfg;
|
|
if (!result.installed) return;
|
|
reloadOnboardingPluginRegistry({
|
|
cfg: next,
|
|
runtime,
|
|
workspaceDir
|
|
});
|
|
await refreshStatus(channel);
|
|
} else if (!await ensureBundledPluginEnabled(channel)) return;
|
|
const label = getChannelPlugin(channel)?.meta.label ?? catalogEntry?.meta.label ?? channel;
|
|
if (statusByChannel.get(channel)?.configured ?? false) {
|
|
await handleConfiguredChannel(channel, label);
|
|
return;
|
|
}
|
|
await configureChannel(channel);
|
|
};
|
|
if (options?.quickstartDefaults) {
|
|
const { entries } = getChannelEntries();
|
|
const choice = await prompter.select({
|
|
message: "Select channel (QuickStart)",
|
|
options: [...buildSelectionOptions(entries), {
|
|
value: "__skip__",
|
|
label: "Skip for now",
|
|
hint: `You can add channels later via \`${formatCliCommand("openclaw channels add")}\``
|
|
}],
|
|
initialValue: quickstartDefault
|
|
});
|
|
if (choice !== "__skip__") await handleChannelChoice(choice);
|
|
} else {
|
|
const doneValue = "__done__";
|
|
const initialValue = options?.initialSelection?.[0] ?? quickstartDefault;
|
|
while (true) {
|
|
const { entries } = getChannelEntries();
|
|
const choice = await prompter.select({
|
|
message: "Select a channel",
|
|
options: [...buildSelectionOptions(entries), {
|
|
value: doneValue,
|
|
label: "Finished",
|
|
hint: selection.length > 0 ? "Done" : "Skip for now"
|
|
}],
|
|
initialValue
|
|
});
|
|
if (choice === doneValue) break;
|
|
await handleChannelChoice(choice);
|
|
}
|
|
}
|
|
options?.onSelection?.(selection);
|
|
const selectionNotes = /* @__PURE__ */ new Map();
|
|
const { entries: selectionEntries } = getChannelEntries();
|
|
for (const entry of selectionEntries) selectionNotes.set(entry.id, formatChannelSelectionLine(entry.meta, formatDocsLink));
|
|
const selectedLines = selection.map((channel) => selectionNotes.get(channel)).filter((line) => Boolean(line));
|
|
if (selectedLines.length > 0) await prompter.note(selectedLines.join("\n"), "Selected channels");
|
|
if (!options?.skipDmPolicyPrompt) next = await maybeConfigureDmPolicies({
|
|
cfg: next,
|
|
selection,
|
|
prompter,
|
|
accountIdsByChannel
|
|
});
|
|
return next;
|
|
}
|
|
|
|
//#endregion
|
|
export { enablePluginInConfig as a, reloadOnboardingPluginRegistry as i, setupChannels as n, ensureOnboardingPluginInstalled as r, noteChannelStatus as t }; |