UNPKG

@gguf/claw

Version:

WhatsApp gateway CLI (Baileys web) with Pi RPC agent

671 lines (666 loc) 22.9 kB
import { rt as loadOpenClawPlugins } from "./reply-B8pOiUNN.js"; import { s as normalizeAccountId, t as DEFAULT_ACCOUNT_ID } from "./session-key-Dm2EOhrH.js"; import { _ as formatChannelSelectionLine, g as formatChannelPrimerLine, t as createSubsystemLogger, y as listChatChannels } from "./subsystem-CAq3uyo7.js"; import { c as resolveDefaultAgentId, s as resolveAgentWorkspaceDir } from "./agent-scope-CMs5Y7l-.js"; import { t as formatCliCommand } from "./command-format-ChfKqObn.js"; import { n as listChannelPlugins, t as getChannelPlugin } from "./plugins-BYIWo0Cp.js"; import { t as formatDocsLink } from "./links-B5pRdmo1.js"; import { t as resolveChannelDefaultAccountId } from "./helpers-BIc7L8EF.js"; import { i as listChannelPluginCatalogEntries, n as isChannelConfigured } from "./plugin-auto-enable-Ci7TBlH2.js"; import { n as installPluginFromNpmSpec, t as recordPluginInstall } from "./installs-BhEjOqPy.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 };