UNPKG

@gguf/claw

Version:

Multi-channel AI gateway with extensible messaging integrations

797 lines (793 loc) 29.9 kB
import { Dt as theme, Yt as resolveStateDir, _ as defaultRuntime, ct as shortenHomeInString, lt as shortenHomePath, ot as resolveUserPath, rt as resolveConfigDir } from "./entry.js"; import "./auth-profiles-DFa1zzNy.js"; import "./exec-CBKBIMpA.js"; import "./agent-scope-RzK9Zcks.js"; import "./github-copilot-token-DuFIqfeC.js"; import "./model-DY9t1aT6.js"; import "./pi-model-discovery-Do3xMEtM.js"; import "./frontmatter-D-YR-Ghi.js"; import "./skills-DJmGZazd.js"; import { m as defaultSlotIdForKey, p as applyExclusiveSlotSelection, s as resolveBundledPluginsDir, t as clearPluginManifestRegistryCache } from "./manifest-registry-DS2iK5AZ.js"; import { i as loadConfig, l as writeConfigFile } from "./config-B2kL1ciP.js"; import "./client-CDjZdZtI.js"; import "./call-DXhJGwEy.js"; import "./message-channel-CVHJDItx.js"; import "./pairing-token-Byh6drgn.js"; import "./subagent-registry-C7Edpn23.js"; import "./sessions-BD5dyLxb.js"; import "./tokens-D5RzuaYP.js"; import "./normalize-Db7Xtx2v.js"; import "./accounts-BpzwDfBB.js"; import "./bindings-KqaGKS1E.js"; import "./logging-CFvkxgcX.js"; import "./send-BFiRlO6V.js"; import "./plugins-RqhjLCb6.js"; import "./send-CWDtU8Gi.js"; import "./with-timeout-nVWy7PWz.js"; import "./deliver-CeVE1Jh-.js"; import "./diagnostic-774btyou.js"; import "./diagnostic-session-state-HO94DMou.js"; import "./accounts-B3D4iWUP.js"; import "./send-DCCBUsNn.js"; import "./image-ops-lDlFpoR2.js"; import "./pi-embedded-helpers-NVEtaJwl.js"; import "./sandbox-CO-R8v6J.js"; import "./common-DcSh1hZE.js"; import "./chrome-52ZF7gmE.js"; import { l as promptYesNo } from "./tailscale-BzRVNhMW.js"; import "./auth-Dq2pFnjj.js"; import "./server-context-C1f7cijA.js"; import "./routes-BJmh1Ify.js"; import "./redact-C2s6sr73.js"; import "./errors-CFEPAPWc.js"; import "./fs-safe-3YwsxSr5.js"; import "./paths-CXKNzNjU.js"; import "./ssrf-Cv2DiHsm.js"; import "./store-e6fIrP17.js"; import "./ports-59j-bA53.js"; import "./trash-DVLJ0k6q.js"; import "./dock-BZuwgj1O.js"; import "./accounts-Jg3_1Y-r.js"; import "./paths-CXpciDEv.js"; import "./thinking-CJPPUYWd.js"; import "./models-config-5zJ39mUb.js"; import "./reply-prefix-DJOqzjBt.js"; import "./memory-cli-C3uas9sI.js"; import "./manager-D3ZmOwqt.js"; import "./gemini-auth-CdgAkz2K.js"; import "./sqlite-1-TCahE6.js"; import "./retry-BcjnIuo-.js"; import "./chunk-Z2NYDchs.js"; import "./markdown-tables-GCHx-nGT.js"; import "./fetch-guard-B5BAqVyD.js"; import "./local-roots-SkDT1Wv8.js"; import "./ir-DAwVi0a7.js"; import "./render-e7fENCYH.js"; import "./commands-registry-CRmMPJQ9.js"; import "./image-QGUQCdGh.js"; import "./tool-display-Cs1uRaRV.js"; import "./runner-D81xREgc.js"; import "./model-catalog-DLoDxnxL.js"; import "./session-utils-C3Oi9cXA.js"; import "./skill-commands-D2rmj6w8.js"; import "./workspace-dirs-BXftTDSV.js"; import "./pairing-store-CruPwgBw.js"; import "./fetch-vg2oFVIH.js"; import "./exec-approvals-BZA6z4HM.js"; import "./nodes-screen-x7YKy8Ay.js"; import "./session-cost-usage-CpxG9Mup.js"; import "./pi-tools.policy-DeRwGLEO.js"; import "./control-service-DGleRjGJ.js"; import "./stagger-CArN2YVJ.js"; import "./channel-selection-DoXlsBhe.js"; import "./send-C3RIrBTr.js"; import "./outbound-attachment-CdFWGHgJ.js"; import "./delivery-queue-DKdWlR61.js"; import "./send-0Nz3O9Jc.js"; import "./resolve-route-BONxzeyg.js"; import "./channel-activity-BYGpAtHP.js"; import "./tables-BrqD0SUa.js"; import "./proxy-DL3MD6-P.js"; import { t as formatDocsLink } from "./links-CW8Bx7rK.js"; import "./cli-utils-CCaEbxAz.js"; import "./help-format-B0pWGnZs.js"; import "./progress-BAHiAaDW.js"; import "./replies-CuUAAFF2.js"; import "./onboard-helpers-CEx2tGVB.js"; import "./prompt-style-DwCXob2h.js"; import "./pairing-labels-DtxjElSq.js"; import { c as resolveArchiveKind } from "./install-safe-path-CnPOnUC0.js"; import "./npm-registry-spec-Dx1jSlNK.js"; import "./skill-scanner-BlmeBzA8.js"; import { i as resolvePluginInstallDir, n as installPluginFromNpmSpec, r as installPluginFromPath, t as recordPluginInstall } from "./installs-Bfs-7l-S.js"; import { t as renderTable } from "./table-Ca0mornk.js"; import { t as buildPluginStatusReport } from "./status-BxazXPbL.js"; import { n as updateNpmInstalledPlugins } from "./update-CkgqeT5A.js"; import os from "node:os"; import path from "node:path"; import fs from "node:fs"; import fs$1 from "node:fs/promises"; //#region src/plugins/source-display.ts function tryRelative(root, filePath) { const rel = path.relative(root, filePath); if (!rel || rel === ".") return null; if (rel === "..") return null; if (rel.startsWith(`..${path.sep}`) || rel.startsWith("../") || rel.startsWith("..\\")) return null; if (path.isAbsolute(rel)) return null; return rel.replaceAll("\\", "/"); } function resolvePluginSourceRoots(params) { return { stock: resolveBundledPluginsDir(), global: path.join(resolveConfigDir(), "extensions"), workspace: params.workspaceDir ? path.join(params.workspaceDir, ".openclaw", "extensions") : void 0 }; } function formatPluginSourceForTable(plugin, roots) { const raw = plugin.source; if (plugin.origin === "bundled" && roots.stock) { const rel = tryRelative(roots.stock, raw); if (rel) return { value: `stock:${rel}`, rootKey: "stock" }; } if (plugin.origin === "workspace" && roots.workspace) { const rel = tryRelative(roots.workspace, raw); if (rel) return { value: `workspace:${rel}`, rootKey: "workspace" }; } if (plugin.origin === "global" && roots.global) { const rel = tryRelative(roots.global, raw); if (rel) return { value: `global:${rel}`, rootKey: "global" }; } return { value: shortenHomeInString(raw) }; } //#endregion //#region src/plugins/uninstall.ts function resolveUninstallDirectoryTarget(params) { if (!params.hasInstall) return null; if (params.installRecord?.source === "path") return null; let defaultPath; try { defaultPath = resolvePluginInstallDir(params.pluginId, params.extensionsDir); } catch { return null; } const configuredPath = params.installRecord?.installPath; if (!configuredPath) return defaultPath; if (path.resolve(configuredPath) === path.resolve(defaultPath)) return configuredPath; return defaultPath; } /** * Remove plugin references from config (pure config mutation). * Returns a new config with the plugin removed from entries, installs, allow, load.paths, and slots. */ function removePluginFromConfig(cfg, pluginId) { const actions = { entry: false, install: false, allowlist: false, loadPath: false, memorySlot: false }; const pluginsConfig = cfg.plugins ?? {}; let entries = pluginsConfig.entries; if (entries && pluginId in entries) { const { [pluginId]: _, ...rest } = entries; entries = Object.keys(rest).length > 0 ? rest : void 0; actions.entry = true; } let installs = pluginsConfig.installs; const installRecord = installs?.[pluginId]; if (installs && pluginId in installs) { const { [pluginId]: _, ...rest } = installs; installs = Object.keys(rest).length > 0 ? rest : void 0; actions.install = true; } let allow = pluginsConfig.allow; if (Array.isArray(allow) && allow.includes(pluginId)) { allow = allow.filter((id) => id !== pluginId); if (allow.length === 0) allow = void 0; actions.allowlist = true; } let load = pluginsConfig.load; if (installRecord?.source === "path" && installRecord.sourcePath) { const sourcePath = installRecord.sourcePath; const loadPaths = load?.paths; if (Array.isArray(loadPaths) && loadPaths.includes(sourcePath)) { const nextLoadPaths = loadPaths.filter((p) => p !== sourcePath); load = nextLoadPaths.length > 0 ? { ...load, paths: nextLoadPaths } : void 0; actions.loadPath = true; } } let slots = pluginsConfig.slots; if (slots?.memory === pluginId) { slots = { ...slots, memory: defaultSlotIdForKey("memory") }; actions.memorySlot = true; } if (slots && Object.keys(slots).length === 0) slots = void 0; const cleanedPlugins = { ...pluginsConfig, entries, installs, allow, load, slots }; if (cleanedPlugins.entries === void 0) delete cleanedPlugins.entries; if (cleanedPlugins.installs === void 0) delete cleanedPlugins.installs; if (cleanedPlugins.allow === void 0) delete cleanedPlugins.allow; if (cleanedPlugins.load === void 0) delete cleanedPlugins.load; if (cleanedPlugins.slots === void 0) delete cleanedPlugins.slots; return { config: { ...cfg, plugins: Object.keys(cleanedPlugins).length > 0 ? cleanedPlugins : void 0 }, actions }; } /** * Uninstall a plugin by removing it from config and optionally deleting installed files. * Linked plugins (source === "path") never have their source directory deleted. */ async function uninstallPlugin(params) { const { config, pluginId, deleteFiles = true, extensionsDir } = params; const hasEntry = pluginId in (config.plugins?.entries ?? {}); const hasInstall = pluginId in (config.plugins?.installs ?? {}); if (!hasEntry && !hasInstall) return { ok: false, error: `Plugin not found: ${pluginId}` }; const installRecord = config.plugins?.installs?.[pluginId]; const isLinked = installRecord?.source === "path"; const { config: newConfig, actions: configActions } = removePluginFromConfig(config, pluginId); const actions = { ...configActions, directory: false }; const warnings = []; const deleteTarget = deleteFiles && !isLinked ? resolveUninstallDirectoryTarget({ pluginId, hasInstall, installRecord, extensionsDir }) : null; if (deleteTarget) { const existed = await fs$1.access(deleteTarget).then(() => true).catch(() => false) ?? false; try { await fs$1.rm(deleteTarget, { recursive: true, force: true }); actions.directory = existed; } catch (error) { warnings.push(`Failed to remove plugin directory ${deleteTarget}: ${error instanceof Error ? error.message : String(error)}`); } } return { ok: true, config: newConfig, pluginId, actions, warnings }; } //#endregion //#region src/cli/plugins-cli.ts function resolveFileNpmSpecToLocalPath(raw) { const trimmed = raw.trim(); if (!trimmed.toLowerCase().startsWith("file:")) return null; const rest = trimmed.slice(5); if (!rest) return { ok: false, error: "unsupported file: spec: missing path" }; if (rest.startsWith("///")) return { ok: true, path: rest.slice(2) }; if (rest.startsWith("//localhost/")) return { ok: true, path: rest.slice(11) }; if (rest.startsWith("//")) return { ok: false, error: "unsupported file: URL host (expected \"file:<path>\" or \"file:///abs/path\")" }; return { ok: true, path: rest }; } function formatPluginLine(plugin, verbose = false) { const status = plugin.status === "loaded" ? theme.success("loaded") : plugin.status === "disabled" ? theme.warn("disabled") : theme.error("error"); const name = theme.command(plugin.name || plugin.id); const idSuffix = plugin.name && plugin.name !== plugin.id ? theme.muted(` (${plugin.id})`) : ""; const desc = plugin.description ? theme.muted(plugin.description.length > 60 ? `${plugin.description.slice(0, 57)}...` : plugin.description) : theme.muted("(no description)"); if (!verbose) return `${name}${idSuffix} ${status} - ${desc}`; const parts = [ `${name}${idSuffix} ${status}`, ` source: ${theme.muted(shortenHomeInString(plugin.source))}`, ` origin: ${plugin.origin}` ]; if (plugin.version) parts.push(` version: ${plugin.version}`); if (plugin.providerIds.length > 0) parts.push(` providers: ${plugin.providerIds.join(", ")}`); if (plugin.error) parts.push(theme.error(` error: ${plugin.error}`)); return parts.join("\n"); } function applySlotSelectionForPlugin(config, pluginId) { const report = buildPluginStatusReport({ config }); const plugin = report.plugins.find((entry) => entry.id === pluginId); if (!plugin) return { config, warnings: [] }; const result = applyExclusiveSlotSelection({ config, selectedId: plugin.id, selectedKind: plugin.kind, registry: report }); return { config: result.config, warnings: result.warnings }; } function createPluginInstallLogger() { return { info: (msg) => defaultRuntime.log(msg), warn: (msg) => defaultRuntime.log(theme.warn(msg)) }; } function enablePluginInConfig(config, pluginId) { return { ...config, plugins: { ...config.plugins, entries: { ...config.plugins?.entries, [pluginId]: { ...config.plugins?.entries?.[pluginId], enabled: true } } } }; } function logSlotWarnings(warnings) { if (warnings.length === 0) return; for (const warning of warnings) defaultRuntime.log(theme.warn(warning)); } function registerPluginsCli(program) { const plugins = program.command("plugins").description("Manage OpenClaw plugins and extensions").addHelpText("after", () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/plugins", "docs.openclaw.ai/cli/plugins")}\n`); plugins.command("list").description("List discovered plugins").option("--json", "Print JSON").option("--enabled", "Only show enabled plugins", false).option("--verbose", "Show detailed entries", false).action((opts) => { const report = buildPluginStatusReport(); const list = opts.enabled ? report.plugins.filter((p) => p.status === "loaded") : report.plugins; if (opts.json) { const payload = { workspaceDir: report.workspaceDir, plugins: list, diagnostics: report.diagnostics }; defaultRuntime.log(JSON.stringify(payload, null, 2)); return; } if (list.length === 0) { defaultRuntime.log(theme.muted("No plugins found.")); return; } const loaded = list.filter((p) => p.status === "loaded").length; defaultRuntime.log(`${theme.heading("Plugins")} ${theme.muted(`(${loaded}/${list.length} loaded)`)}`); if (!opts.verbose) { const tableWidth = Math.max(60, (process.stdout.columns ?? 120) - 1); const sourceRoots = resolvePluginSourceRoots({ workspaceDir: report.workspaceDir }); const usedRoots = /* @__PURE__ */ new Set(); const rows = list.map((plugin) => { const desc = plugin.description ? theme.muted(plugin.description) : ""; const formattedSource = formatPluginSourceForTable(plugin, sourceRoots); if (formattedSource.rootKey) usedRoots.add(formattedSource.rootKey); const sourceLine = desc ? `${formattedSource.value}\n${desc}` : formattedSource.value; return { Name: plugin.name || plugin.id, ID: plugin.name && plugin.name !== plugin.id ? plugin.id : "", Status: plugin.status === "loaded" ? theme.success("loaded") : plugin.status === "disabled" ? theme.warn("disabled") : theme.error("error"), Source: sourceLine, Version: plugin.version ?? "" }; }); if (usedRoots.size > 0) { defaultRuntime.log(theme.muted("Source roots:")); for (const key of [ "stock", "workspace", "global" ]) { if (!usedRoots.has(key)) continue; const dir = sourceRoots[key]; if (!dir) continue; defaultRuntime.log(` ${theme.command(`${key}:`)} ${theme.muted(dir)}`); } defaultRuntime.log(""); } defaultRuntime.log(renderTable({ width: tableWidth, columns: [ { key: "Name", header: "Name", minWidth: 14, flex: true }, { key: "ID", header: "ID", minWidth: 10, flex: true }, { key: "Status", header: "Status", minWidth: 10 }, { key: "Source", header: "Source", minWidth: 26, flex: true }, { key: "Version", header: "Version", minWidth: 8 } ], rows }).trimEnd()); return; } const lines = []; for (const plugin of list) { lines.push(formatPluginLine(plugin, true)); lines.push(""); } defaultRuntime.log(lines.join("\n").trim()); }); plugins.command("info").description("Show plugin details").argument("<id>", "Plugin id").option("--json", "Print JSON").action((id, opts) => { const plugin = buildPluginStatusReport().plugins.find((p) => p.id === id || p.name === id); if (!plugin) { defaultRuntime.error(`Plugin not found: ${id}`); process.exit(1); } const install = loadConfig().plugins?.installs?.[plugin.id]; if (opts.json) { defaultRuntime.log(JSON.stringify(plugin, null, 2)); return; } const lines = []; lines.push(theme.heading(plugin.name || plugin.id)); if (plugin.name && plugin.name !== plugin.id) lines.push(theme.muted(`id: ${plugin.id}`)); if (plugin.description) lines.push(plugin.description); lines.push(""); lines.push(`${theme.muted("Status:")} ${plugin.status}`); lines.push(`${theme.muted("Source:")} ${shortenHomeInString(plugin.source)}`); lines.push(`${theme.muted("Origin:")} ${plugin.origin}`); if (plugin.version) lines.push(`${theme.muted("Version:")} ${plugin.version}`); if (plugin.toolNames.length > 0) lines.push(`${theme.muted("Tools:")} ${plugin.toolNames.join(", ")}`); if (plugin.hookNames.length > 0) lines.push(`${theme.muted("Hooks:")} ${plugin.hookNames.join(", ")}`); if (plugin.gatewayMethods.length > 0) lines.push(`${theme.muted("Gateway methods:")} ${plugin.gatewayMethods.join(", ")}`); if (plugin.providerIds.length > 0) lines.push(`${theme.muted("Providers:")} ${plugin.providerIds.join(", ")}`); if (plugin.cliCommands.length > 0) lines.push(`${theme.muted("CLI commands:")} ${plugin.cliCommands.join(", ")}`); if (plugin.services.length > 0) lines.push(`${theme.muted("Services:")} ${plugin.services.join(", ")}`); if (plugin.error) lines.push(`${theme.error("Error:")} ${plugin.error}`); if (install) { lines.push(""); lines.push(`${theme.muted("Install:")} ${install.source}`); if (install.spec) lines.push(`${theme.muted("Spec:")} ${install.spec}`); if (install.sourcePath) lines.push(`${theme.muted("Source path:")} ${shortenHomePath(install.sourcePath)}`); if (install.installPath) lines.push(`${theme.muted("Install path:")} ${shortenHomePath(install.installPath)}`); if (install.version) lines.push(`${theme.muted("Recorded version:")} ${install.version}`); if (install.installedAt) lines.push(`${theme.muted("Installed at:")} ${install.installedAt}`); } defaultRuntime.log(lines.join("\n")); }); plugins.command("enable").description("Enable a plugin in config").argument("<id>", "Plugin id").action(async (id) => { const cfg = loadConfig(); let next = { ...cfg, plugins: { ...cfg.plugins, entries: { ...cfg.plugins?.entries, [id]: { ...(cfg.plugins?.entries)?.[id], enabled: true } } } }; const slotResult = applySlotSelectionForPlugin(next, id); next = slotResult.config; await writeConfigFile(next); logSlotWarnings(slotResult.warnings); defaultRuntime.log(`Enabled plugin "${id}". Restart the gateway to apply.`); }); plugins.command("disable").description("Disable a plugin in config").argument("<id>", "Plugin id").action(async (id) => { const cfg = loadConfig(); await writeConfigFile({ ...cfg, plugins: { ...cfg.plugins, entries: { ...cfg.plugins?.entries, [id]: { ...(cfg.plugins?.entries)?.[id], enabled: false } } } }); defaultRuntime.log(`Disabled plugin "${id}". Restart the gateway to apply.`); }); plugins.command("uninstall").description("Uninstall a plugin").argument("<id>", "Plugin id").option("--keep-files", "Keep installed files on disk", false).option("--keep-config", "Deprecated alias for --keep-files", false).option("--force", "Skip confirmation prompt", false).option("--dry-run", "Show what would be removed without making changes", false).action(async (id, opts) => { const cfg = loadConfig(); const report = buildPluginStatusReport({ config: cfg }); const extensionsDir = path.join(resolveStateDir(process.env, os.homedir), "extensions"); const keepFiles = Boolean(opts.keepFiles || opts.keepConfig); if (opts.keepConfig) defaultRuntime.log(theme.warn("`--keep-config` is deprecated, use `--keep-files`.")); const plugin = report.plugins.find((p) => p.id === id || p.name === id); const pluginId = plugin?.id ?? id; const hasEntry = pluginId in (cfg.plugins?.entries ?? {}); const hasInstall = pluginId in (cfg.plugins?.installs ?? {}); if (!hasEntry && !hasInstall) { if (plugin) defaultRuntime.error(`Plugin "${pluginId}" is not managed by plugins config/install records and cannot be uninstalled.`); else defaultRuntime.error(`Plugin not found: ${id}`); process.exit(1); } const install = cfg.plugins?.installs?.[pluginId]; const isLinked = install?.source === "path"; const preview = []; if (hasEntry) preview.push("config entry"); if (hasInstall) preview.push("install record"); if (cfg.plugins?.allow?.includes(pluginId)) preview.push("allowlist entry"); if (isLinked && install?.sourcePath && cfg.plugins?.load?.paths?.includes(install.sourcePath)) preview.push("load path"); if (cfg.plugins?.slots?.memory === pluginId) preview.push(`memory slot (will reset to "memory-core")`); const deleteTarget = !keepFiles ? resolveUninstallDirectoryTarget({ pluginId, hasInstall, installRecord: install, extensionsDir }) : null; if (deleteTarget) preview.push(`directory: ${shortenHomePath(deleteTarget)}`); const pluginName = plugin?.name || pluginId; defaultRuntime.log(`Plugin: ${theme.command(pluginName)}${pluginName !== pluginId ? theme.muted(` (${pluginId})`) : ""}`); defaultRuntime.log(`Will remove: ${preview.length > 0 ? preview.join(", ") : "(nothing)"}`); if (opts.dryRun) { defaultRuntime.log(theme.muted("Dry run, no changes made.")); return; } if (!opts.force) { if (!await promptYesNo(`Uninstall plugin "${pluginId}"?`)) { defaultRuntime.log("Cancelled."); return; } } const result = await uninstallPlugin({ config: cfg, pluginId, deleteFiles: !keepFiles, extensionsDir }); if (!result.ok) { defaultRuntime.error(result.error); process.exit(1); } for (const warning of result.warnings) defaultRuntime.log(theme.warn(warning)); await writeConfigFile(result.config); const removed = []; if (result.actions.entry) removed.push("config entry"); if (result.actions.install) removed.push("install record"); if (result.actions.allowlist) removed.push("allowlist"); if (result.actions.loadPath) removed.push("load path"); if (result.actions.memorySlot) removed.push("memory slot"); if (result.actions.directory) removed.push("directory"); defaultRuntime.log(`Uninstalled plugin "${pluginId}". Removed: ${removed.length > 0 ? removed.join(", ") : "nothing"}.`); defaultRuntime.log("Restart the gateway to apply changes."); }); plugins.command("install").description("Install a plugin (path, archive, or npm spec)").argument("<path-or-spec>", "Path (.ts/.js/.zip/.tgz/.tar.gz) or an npm package spec").option("-l, --link", "Link a local path instead of copying", false).option("--pin", "Record npm installs as exact resolved <name>@<version>", false).action(async (raw, opts) => { const fileSpec = resolveFileNpmSpecToLocalPath(raw); if (fileSpec && !fileSpec.ok) { defaultRuntime.error(fileSpec.error); process.exit(1); } const resolved = resolveUserPath(fileSpec && fileSpec.ok ? fileSpec.path : raw); const cfg = loadConfig(); if (fs.existsSync(resolved)) { if (opts.link) { const existing = cfg.plugins?.load?.paths ?? []; const merged = Array.from(new Set([...existing, resolved])); const probe = await installPluginFromPath({ path: resolved, dryRun: true }); if (!probe.ok) { defaultRuntime.error(probe.error); process.exit(1); } let next = enablePluginInConfig({ ...cfg, plugins: { ...cfg.plugins, load: { ...cfg.plugins?.load, paths: merged } } }, probe.pluginId); next = recordPluginInstall(next, { pluginId: probe.pluginId, source: "path", sourcePath: resolved, installPath: resolved, version: probe.version }); const slotResult = applySlotSelectionForPlugin(next, probe.pluginId); next = slotResult.config; await writeConfigFile(next); logSlotWarnings(slotResult.warnings); defaultRuntime.log(`Linked plugin path: ${shortenHomePath(resolved)}`); defaultRuntime.log(`Restart the gateway to load plugins.`); return; } const result = await installPluginFromPath({ path: resolved, logger: createPluginInstallLogger() }); if (!result.ok) { defaultRuntime.error(result.error); process.exit(1); } clearPluginManifestRegistryCache(); let next = enablePluginInConfig(cfg, result.pluginId); const source = resolveArchiveKind(resolved) ? "archive" : "path"; next = recordPluginInstall(next, { pluginId: result.pluginId, source, sourcePath: resolved, installPath: result.targetDir, version: result.version }); const slotResult = applySlotSelectionForPlugin(next, result.pluginId); next = slotResult.config; await writeConfigFile(next); logSlotWarnings(slotResult.warnings); defaultRuntime.log(`Installed plugin: ${result.pluginId}`); defaultRuntime.log(`Restart the gateway to load plugins.`); return; } if (opts.link) { defaultRuntime.error("`--link` requires a local path."); process.exit(1); } if (raw.startsWith(".") || raw.startsWith("~") || path.isAbsolute(raw) || raw.endsWith(".ts") || raw.endsWith(".js") || raw.endsWith(".mjs") || raw.endsWith(".cjs") || raw.endsWith(".tgz") || raw.endsWith(".tar.gz") || raw.endsWith(".tar") || raw.endsWith(".zip")) { defaultRuntime.error(`Path not found: ${resolved}`); process.exit(1); } const result = await installPluginFromNpmSpec({ spec: raw, logger: createPluginInstallLogger() }); if (!result.ok) { defaultRuntime.error(result.error); process.exit(1); } clearPluginManifestRegistryCache(); let next = enablePluginInConfig(cfg, result.pluginId); const resolvedSpec = result.npmResolution?.resolvedSpec; const recordSpec = opts.pin && resolvedSpec ? resolvedSpec : raw; if (opts.pin && !resolvedSpec) defaultRuntime.log(theme.warn("Could not resolve exact npm version for --pin; storing original npm spec.")); if (opts.pin && resolvedSpec) defaultRuntime.log(`Pinned npm install record to ${resolvedSpec}.`); next = recordPluginInstall(next, { pluginId: result.pluginId, source: "npm", spec: recordSpec, installPath: result.targetDir, version: result.version, resolvedName: result.npmResolution?.name, resolvedVersion: result.npmResolution?.version, resolvedSpec: result.npmResolution?.resolvedSpec, integrity: result.npmResolution?.integrity, shasum: result.npmResolution?.shasum, resolvedAt: result.npmResolution?.resolvedAt }); const slotResult = applySlotSelectionForPlugin(next, result.pluginId); next = slotResult.config; await writeConfigFile(next); logSlotWarnings(slotResult.warnings); defaultRuntime.log(`Installed plugin: ${result.pluginId}`); defaultRuntime.log(`Restart the gateway to load plugins.`); }); plugins.command("update").description("Update installed plugins (npm installs only)").argument("[id]", "Plugin id (omit with --all)").option("--all", "Update all tracked plugins", false).option("--dry-run", "Show what would change without writing", false).action(async (id, opts) => { const cfg = loadConfig(); const installs = cfg.plugins?.installs ?? {}; const targets = opts.all ? Object.keys(installs) : id ? [id] : []; if (targets.length === 0) { if (opts.all) { defaultRuntime.log("No npm-installed plugins to update."); return; } defaultRuntime.error("Provide a plugin id or use --all."); process.exit(1); } const result = await updateNpmInstalledPlugins({ config: cfg, pluginIds: targets, dryRun: opts.dryRun, logger: { info: (msg) => defaultRuntime.log(msg), warn: (msg) => defaultRuntime.log(theme.warn(msg)) }, onIntegrityDrift: async (drift) => { const specLabel = drift.resolvedSpec ?? drift.spec; defaultRuntime.log(theme.warn(`Integrity drift detected for "${drift.pluginId}" (${specLabel})\nExpected: ${drift.expectedIntegrity}\nActual: ${drift.actualIntegrity}`)); if (drift.dryRun) return true; return await promptYesNo(`Continue updating "${drift.pluginId}" with this artifact?`); } }); for (const outcome of result.outcomes) { if (outcome.status === "error") { defaultRuntime.log(theme.error(outcome.message)); continue; } if (outcome.status === "skipped") { defaultRuntime.log(theme.warn(outcome.message)); continue; } defaultRuntime.log(outcome.message); } if (!opts.dryRun && result.changed) { await writeConfigFile(result.config); defaultRuntime.log("Restart the gateway to load plugins."); } }); plugins.command("doctor").description("Report plugin load issues").action(() => { const report = buildPluginStatusReport(); const errors = report.plugins.filter((p) => p.status === "error"); const diags = report.diagnostics.filter((d) => d.level === "error"); if (errors.length === 0 && diags.length === 0) { defaultRuntime.log("No plugin issues detected."); return; } const lines = []; if (errors.length > 0) { lines.push(theme.error("Plugin errors:")); for (const entry of errors) lines.push(`- ${entry.id}: ${entry.error ?? "failed to load"} (${entry.source})`); } if (diags.length > 0) { if (lines.length > 0) lines.push(""); lines.push(theme.warn("Diagnostics:")); for (const diag of diags) { const target = diag.pluginId ? `${diag.pluginId}: ` : ""; lines.push(`- ${target}${diag.message}`); } } const docs = formatDocsLink("/plugin", "docs.openclaw.ai/plugin"); lines.push(""); lines.push(`${theme.muted("Docs:")} ${docs}`); defaultRuntime.log(lines.join("\n")); }); } //#endregion export { registerPluginsCli };