import fs from "node:fs"; import path from "node:path"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js"; import type { ChannelPluginCatalogEntry } from "../../channels/plugins/catalog.js"; import type { ClawdbotConfig } from "../../config/config.js"; import { createSubsystemLogger } from "../../logging.js"; import { loadClawdbotPlugins } from "../../plugins/loader.js"; import { installPluginFromNpmSpec } from "../../plugins/install.js"; import type { RuntimeEnv } from "../../runtime.js"; import type { WizardPrompter } from "../../wizard/prompts.js"; type InstallChoice = "npm" | "local" | "skip"; type InstallResult = { cfg: ClawdbotConfig; installed: boolean; }; function resolveLocalPath(entry: ChannelPluginCatalogEntry, workspaceDir?: string): string | null { const raw = entry.install.localPath?.trim(); if (!raw) return null; const candidates = 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 ensurePluginEnabled(cfg: ClawdbotConfig, pluginId: string): ClawdbotConfig { const entries = { ...cfg.plugins?.entries, [pluginId]: { ...(cfg.plugins?.entries?.[pluginId] as Record | undefined), enabled: true, }, }; const next: ClawdbotConfig = { ...cfg, plugins: { ...cfg.plugins, ...(cfg.plugins?.enabled === false ? { enabled: true } : {}), entries, }, }; return ensurePluginAllowlist(next, pluginId); } function ensurePluginAllowlist(cfg: ClawdbotConfig, pluginId: string): ClawdbotConfig { const allow = cfg.plugins?.allow; if (!allow || allow.includes(pluginId)) return cfg; return { ...cfg, plugins: { ...cfg.plugins, allow: [...allow, pluginId], }, }; } function addPluginLoadPath(cfg: ClawdbotConfig, pluginPath: string): ClawdbotConfig { 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: { entry: ChannelPluginCatalogEntry; localPath?: string | null; prompter: WizardPrompter; }): Promise { const { entry, localPath, prompter } = params; const localOptions: Array<{ value: InstallChoice; label: string; hint?: string }> = localPath ? [ { value: "local", label: "Use local plugin path", hint: localPath, }, ] : []; const options: Array<{ value: InstallChoice; label: string; hint?: string }> = [ { value: "npm", label: `Download from npm (${entry.install.npmSpec})` }, ...localOptions, { value: "skip", label: "Skip for now" }, ]; const initialValue: InstallChoice = localPath ? "local" : "npm"; return await prompter.select({ message: `Install ${entry.meta.label} plugin?`, options, initialValue, }); } export async function ensureOnboardingPluginInstalled(params: { cfg: ClawdbotConfig; entry: ChannelPluginCatalogEntry; prompter: WizardPrompter; runtime: RuntimeEnv; workspaceDir?: string; }): Promise { const { entry, prompter, runtime, workspaceDir } = params; let next = params.cfg; const localPath = resolveLocalPath(entry, workspaceDir); const choice = await promptInstallChoice({ entry, localPath, prompter, }); if (choice === "skip") { return { cfg: next, installed: false }; } if (choice === "local" && localPath) { next = addPluginLoadPath(next, localPath); next = ensurePluginEnabled(next, entry.id); 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 = ensurePluginEnabled(next, result.pluginId); return { cfg: next, installed: true }; } await prompter.note( `Failed to install ${entry.install.npmSpec}: ${result.error}`, "Plugin install", ); if (localPath) { const fallback = await prompter.confirm({ message: `Use local plugin path instead? (${localPath})`, initialValue: true, }); if (fallback) { next = addPluginLoadPath(next, localPath); next = ensurePluginEnabled(next, entry.id); return { cfg: next, installed: true }; } } runtime.error?.(`Plugin install failed: ${result.error}`); return { cfg: next, installed: false }; } export function reloadOnboardingPluginRegistry(params: { cfg: ClawdbotConfig; runtime: RuntimeEnv; workspaceDir?: string; }): void { const workspaceDir = params.workspaceDir ?? resolveAgentWorkspaceDir(params.cfg, resolveDefaultAgentId(params.cfg)); const log = createSubsystemLogger("plugins"); loadClawdbotPlugins({ 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), }, }); }