UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

646 lines (645 loc) • 33 kB
import { l as normalizeOptionalString } from "./string-coerce-1VuiLaaR.mjs"; import { t as coerceErrorMessage } from "./error-coercion-CvJK4lmH.mjs"; import { o as resolveUserPath } from "./home-dir-Dc6nN6HB.mjs"; import { c as isRecord } from "./record-coerce-TIsquoVR.mjs"; import { p as shortenHomePath } from "./utils-DgZn2dLu.mjs"; import { a as normalizeWindowsPathForComparison } from "./path-guards-CH7noY0Z.mjs"; import { t as formatCliCommand } from "./command-format-DwEdrDKL.mjs"; import { u as tryReadJsonSync } from "./json-files-C5mNTScg.mjs"; import { i as loadInstalledPluginIndex } from "./installed-plugin-index-BayjZgqB.mjs"; import { M as normalizeBundledLookupPath } from "./discovery-Dma3bVNk.mjs"; import { a as resolveDefaultPluginExtensionsDir, s as resolveDefaultPluginNpmDir } from "./install-paths-R7__5hE9.mjs"; import { n as auditOpenClawPeerDependenciesInManagedNpmRoot, o as reconcileRegisteredOpenClawHostLinks, s as relinkOpenClawPeerDependenciesInManagedNpmRoot } from "./plugin-peer-link-B8_Vs9lb.mjs"; import { r as copyPluginInstallRecordMap, u as setPluginInstallRecordMapEntry } from "./installed-plugin-record-match-CVFkDlud.mjs"; import { a as isExternallyDistributedPlugin } from "./official-external-plugin-catalog-Dcv926GC.mjs"; import { n as resolveInstalledPluginIndexStateDatabaseOptions, r as resolveInstalledPluginIndexStorePath } from "./installed-plugin-index-store-path-vGDuKHrq.mjs"; import { c as inspectPersistedInstalledPluginIndexInstallRecordsSync, i as loadInstalledPluginIndexInstallRecordsSync, n as listRecoveredManagedNpmInstallCandidates, o as clearLoadInstalledPluginIndexInstallRecordsCache, r as loadInstalledPluginIndexInstallRecords } from "./installed-plugin-index-record-reader-BP4r74yH.mjs"; import { i as markRetainedManagedNpmInstall, r as hasRetainedManagedNpmInstallMarker, u as listManagedPluginNpmRootsSync } from "./managed-npm-retention-BPamOGs_.mjs"; import { a as resolveTrustedSourceLinkedOfficialClawHubInstall, i as resolveTrustedOfficialClawHubPackageName, r as isTrustedOfficialPluginInstallRecord } from "./official-external-install-records-BVI7ulqm.mjs"; import { a as resolveInstalledManifestRegistryIndexFingerprint } from "./plugin-control-plane-context-k1PmbRpn.mjs"; import { r as readPersistedInstalledPluginIndexSync } from "./installed-plugin-index-store-CC8tZSgR.mjs"; import { r as writeJsonTarget } from "./json-file-5C1aYJVp.mjs"; import { i as withPluginLifecycleLease } from "./plugin-lifecycle-lease-DHQMWweY.mjs"; import { a as writePersistedInstalledPluginIndex } from "./installed-plugin-index-store-write-DmdXABHD.mjs"; import { i as withoutPluginInstallRecords, n as removePluginInstallRecordFromRecords } from "./installed-plugin-index-records-D8YKQ3gZ.mjs"; import { t as note } from "./note-Byxgt4MB.mjs"; import { a as resolveBundledPluginSources } from "./bundled-sources-VMphqOte.mjs"; import { t as refreshPluginRegistry } from "./plugin-registry-refresh-BI183Buw.mjs"; import fs from "node:fs"; import path from "node:path"; //#region src/plugins/stale-local-bundled-plugin-install-records.ts function normalizePathForCompare(rawPath, env) { return path.resolve(normalizeBundledLookupPath(resolveUserPath(rawPath, env))); } function primaryInstallRecordPath(record) { if (typeof record.installPath === "string" && record.installPath.trim()) return { field: "installPath", path: record.installPath }; if (typeof record.sourcePath === "string" && record.sourcePath.trim()) return { field: "sourcePath", path: record.sourcePath }; return null; } function looksLikeCompiledBundledPluginPath(targetPath, pluginId) { const segments = normalizeBundledLookupPath(targetPath).split(/[\\/]+/u); return segments.some((segment, index) => { return (segment === "dist" || segment === "dist-runtime") && segments[index + 1] === "extensions" && segments[index + 2] === pluginId; }); } function hasStaleBundledVersion(record, bundledSource) { const recordVersion = record.version?.trim(); const bundledVersion = bundledSource.version?.trim(); return Boolean(recordVersion && bundledVersion && recordVersion !== bundledVersion); } /** Lists path install records that still point at stale compiled bundled plugin output. */ function listStaleLocalBundledPluginInstallRecords(params) { const bundled = params.bundled ?? resolveBundledPluginSources({ workspaceDir: params.workspaceDir, env: params.env }); const stale = []; for (const [pluginId, record] of Object.entries(params.installRecords).toSorted(([left], [right]) => left.localeCompare(right))) { if (record.source !== "path") continue; const bundledSource = bundled.get(pluginId); if (!bundledSource?.localPath) continue; if (!hasStaleBundledVersion(record, bundledSource)) continue; const recordPath = primaryInstallRecordPath(record); if (!recordPath) continue; const stalePath = normalizePathForCompare(recordPath.path, params.env); const bundledPath = normalizePathForCompare(bundledSource.localPath, params.env); if (stalePath === bundledPath) continue; if (!looksLikeCompiledBundledPluginPath(stalePath, pluginId)) continue; stale.push({ pluginId, record, recordPathField: recordPath.field, stalePath, bundledPath }); } return stale; } /** Removes stale compiled bundled plugin path records from an install record map. */ function pruneStaleLocalBundledPluginInstallRecords(params) { const stale = listStaleLocalBundledPluginInstallRecords(params); if (stale.length === 0) return { records: params.installRecords, stale }; const staleIds = new Set(stale.map((record) => record.pluginId)); return { records: Object.fromEntries(Object.entries(params.installRecords).filter(([pluginId]) => !staleIds.has(pluginId))), stale }; } //#endregion //#region src/commands/doctor-plugin-generations.ts const PLUGIN_REGISTRY_CHECK_ID = "core/doctor/plugin-registry"; function normalizeManagedInstallPath(filePath) { const resolved = path.resolve(filePath); return process.platform === "win32" ? normalizeWindowsPathForComparison(resolved) : resolved; } async function listStaleManagedNpmInstallGenerations(params) { const activeRecords = await loadInstalledPluginIndexInstallRecords(params); const candidates = listRecoveredManagedNpmInstallCandidates(params); const candidatesByPluginId = /* @__PURE__ */ new Map(); for (const candidate of candidates) { const entries = candidatesByPluginId.get(candidate.pluginId) ?? []; entries.push(candidate); candidatesByPluginId.set(candidate.pluginId, entries); } const stale = []; for (const [pluginId, pluginCandidates] of candidatesByPluginId) { const activeRecord = activeRecords[pluginId]; if (activeRecord?.source !== "npm" || !activeRecord.installPath) continue; const activePath = normalizeManagedInstallPath(activeRecord.installPath); if (!pluginCandidates.find((candidate) => candidate.installRecord.installPath && normalizeManagedInstallPath(candidate.installRecord.installPath) === activePath)) continue; for (const candidate of pluginCandidates) { const packageDir = candidate.installRecord.installPath; if (!packageDir || normalizeManagedInstallPath(packageDir) === activePath) continue; stale.push({ kind: "stale-managed-npm-install-generation", pluginId, activePackageDir: activeRecord.installPath, packageDir, ...candidate.installRecord.resolvedVersion ? { version: candidate.installRecord.resolvedVersion } : {} }); } } return stale.toSorted((left, right) => left.packageDir.localeCompare(right.packageDir)); } /** Marks non-authoritative managed npm trees for safe cleanup after gateway shutdown. */ async function maybeRepairStaleManagedNpmInstallGenerations(params) { const stale = await listStaleManagedNpmInstallGenerations(params); if (stale.length === 0) return false; if (!params.prompter.shouldRepair) { note([ "Managed npm plugin installs have stale non-authoritative generations:", ...stale.map((generation) => `- ${generation.pluginId}: ${shortenHomePath(generation.packageDir)}${generation.version ? ` (${generation.version})` : ""}`), `Repair with ${formatCliCommand("openclaw doctor --fix")} to retire stale generations after the gateway restarts.` ].join("\n"), "Plugin registry"); return false; } const retired = []; for (const generation of stale) if (await markRetainedManagedNpmInstall({ packageDir: generation.packageDir, pluginId: generation.pluginId, reason: "doctor-repaired-stale-managed-npm-generation" })) retired.push(generation); if (retired.length === 0) return false; clearLoadInstalledPluginIndexInstallRecordsCache(); note(["Retired stale managed npm plugin generation(s); they will be pruned after the gateway restarts:", ...retired.map((generation) => `- ${generation.pluginId}: ${shortenHomePath(generation.packageDir)}${generation.version ? ` (${generation.version})` : ""}`)].join("\n"), "Plugin registry"); return true; } function staleManagedNpmInstallGenerationToHealthFinding(issue) { return { checkId: PLUGIN_REGISTRY_CHECK_ID, severity: "warning", message: `Managed npm plugin ${issue.pluginId}${issue.version ? `@${issue.version}` : ""} is a stale non-authoritative generation.`, path: issue.packageDir, target: issue.pluginId, fixHint: "Run `openclaw doctor --fix` to retire the stale generation for pruning after the gateway restarts." }; } function staleManagedNpmInstallGenerationToRepairEffect(issue) { return { kind: "package", action: "would-retire-stale-managed-npm-install-generation", target: issue.packageDir, dryRunSafe: false }; } //#endregion //#region src/commands/doctor-plugin-host-links.ts function resolveRegisteredPluginExtensionsRoot(params) { return params.stateDir ? path.join(params.stateDir, "extensions") : resolveDefaultPluginExtensionsDir(params.env); } function resolveDoctorPluginNpmRoots(params) { const npmRoot = params.stateDir ? path.join(params.stateDir, "npm") : resolveDefaultPluginNpmDir(params.env); return listManagedPluginNpmRootsSync(npmRoot); } /** Audits managed npm and registered plugin host links without mutating either root. */ async function listPluginOpenClawHostLinkIssues(params) { const packageReadFailures = []; const registeredPackageReadFailures = []; const audits = await Promise.all(resolveDoctorPluginNpmRoots(params).map((npmRoot) => auditOpenClawPeerDependenciesInManagedNpmRoot({ npmRoot, onPackageReadError: (error, packageDir) => { packageReadFailures.push({ packageDir, reason: coerceErrorMessage(error) }); } }))); const registeredAudit = await reconcileRegisteredOpenClawHostLinks({ installRecords: await loadInstalledPluginIndexInstallRecords(params), extensionsDir: resolveRegisteredPluginExtensionsRoot(params), env: params.env, mode: "audit", onPackageReadError: (error, packageDir) => { registeredPackageReadFailures.push({ packageDir, reason: coerceErrorMessage(error) }); } }); return { peerLinkIssues: audits.flatMap((audit) => audit.issues), packageReadFailures, registeredPeerLinkIssues: registeredAudit.issues, registeredPackageReadFailures }; } async function maybeRepairPluginOpenClawHostLinks(params) { const npmRoots = resolveDoctorPluginNpmRoots(params); if (!params.prompter.shouldRepair) { const audit = await listPluginOpenClawHostLinkIssues(params); if (audit.peerLinkIssues.length > 0) note([ "Managed npm OpenClaw host peer links need repair:", ...audit.peerLinkIssues.map((issue) => `- ${issue.packageName}: ${issue.reason}`), `Repair with ${formatCliCommand("openclaw doctor --fix")} to relink managed npm plugin packages.` ].join("\n"), "Plugin registry"); for (const [label, failures] of [["Managed npm plugin", audit.packageReadFailures], ["Registered plugin", audit.registeredPackageReadFailures]]) if (failures.length > 0) note([`${label} packages could not be inspected:`, ...failures.map((failure) => `- ${shortenHomePath(failure.packageDir)}: ${failure.reason}`)].join("\n"), "Plugin registry"); if (audit.registeredPeerLinkIssues.length > 0) note([ "Registered plugin OpenClaw host links need repair:", ...audit.registeredPeerLinkIssues.map((issue) => `- ${issue.packageName}: ${issue.reason}`), `Repair with ${formatCliCommand("openclaw doctor --fix")} to relink registered plugin packages.` ].join("\n"), "Plugin registry"); return false; } const warnings = []; const logger = { info() {}, warn: (message) => warnings.push(`- ${message}`) }; const repaired = (await Promise.all(npmRoots.map((npmRoot) => relinkOpenClawPeerDependenciesInManagedNpmRoot({ npmRoot, logger, onPackageReadError: (error, packageDir) => { logger.warn(`Could not inspect managed npm package ${shortenHomePath(packageDir)}: ${coerceErrorMessage(error)}`); } })))).reduce((total, result) => total + result.repaired, 0); const registeredRepair = await reconcileRegisteredOpenClawHostLinks({ installRecords: await loadInstalledPluginIndexInstallRecords(params), extensionsDir: resolveRegisteredPluginExtensionsRoot(params), env: params.env, mode: "repair", logger, onPackageReadError: (error, packageDir) => { logger.warn(`Could not inspect registered package ${shortenHomePath(packageDir)}: ${coerceErrorMessage(error)}`); } }); if (repaired > 0) note(`Repaired OpenClaw host peer link(s) for ${repaired} managed npm plugin package(s).`, "Plugin registry"); if (registeredRepair.repaired > 0) note(`Repaired OpenClaw host peer link(s) for ${registeredRepair.repaired} registered plugin package(s).`, "Plugin registry"); if (warnings.length > 0) note(["Could not repair all managed OpenClaw host peer links:", ...warnings].join("\n"), "Plugin registry"); return repaired > 0 || registeredRepair.repaired > 0; } //#endregion //#region src/commands/doctor/shared/plugin-registry-migration.ts /** Backfill shipped ClawHub authority only from a catalog-bound legacy install record. */ function migrateOfficialPluginInstallProvenance(records) { const migrated = copyPluginInstallRecordMap(records); for (const [pluginId, record] of Object.entries(records)) { if (record.source !== "clawhub" || record.clawhubUrl !== void 0 || record.clawhubChannel !== void 0 || record.sourcePath !== void 0 || !resolveTrustedSourceLinkedOfficialClawHubInstall({ pluginId, record })) continue; const normalized = { ...record, clawhubUrl: "https://clawhub.ai", clawhubChannel: "official" }; const packageName = resolveTrustedOfficialClawHubPackageName(normalized); if (isTrustedOfficialPluginInstallRecord({ pluginId, packageName, record: normalized })) setPluginInstallRecordMapEntry(migrated, pluginId, normalized); } return migrated; } var InvalidPluginInstallRecordStateError = class extends Error {}; function invalidPersistedInstallRecordMessage(filePath) { return [`Persisted plugin install records are invalid at ${filePath}.`, "Stop the Gateway, back up this database, delete only the config_machine_state row with state_key='plugins.installedIndex' using SQLite tooling, then rerun `openclaw doctor --fix` to rebuild it."].join(" "); } /** Decide whether Doctor should migrate the plugin registry in this environment. */ function preflightPluginRegistryDoctorMigration(params = {}) { const filePath = resolveInstalledPluginIndexStorePath(params); const persistedState = inspectPersistedInstalledPluginIndexInstallRecordsSync(params); if (persistedState.status === "invalid") throw new InvalidPluginInstallRecordStateError(invalidPersistedInstallRecordMessage(filePath)); if ((params.existsSync ?? fs.existsSync)(filePath)) { const currentRegistry = readPersistedInstalledPluginIndexSync(params); if (currentRegistry) return { action: "skip-existing", filePath, current: currentRegistry }; if (persistedState.status !== "missing") return { action: "migrate", filePath }; } return { action: "initialize", filePath }; } async function readMigrationConfig(params) { if (params.config) return params.config; if (params.readConfig) return await params.readConfig(); return await (await import("./config/config.js")).readBestEffortConfig(); } /** Rebuild Doctor's plugin registry from canonical install records when needed. */ async function migratePluginRegistryForDoctor(params = {}) { const preflight = preflightPluginRegistryDoctorMigration(params); if (params.dryRun) return { status: preflight.action === "skip-existing" ? "skip-existing" : "dry-run", migrated: false, preflight }; return await withPluginLifecycleLease(resolveInstalledPluginIndexStateDatabaseOptions(params), async () => migratePluginRegistryForDoctorWithLease(params)); } async function migratePluginRegistryForDoctorWithLease(params) { const preflight = preflightPluginRegistryDoctorMigration(params); if (preflight.action === "skip-existing") return { status: "skip-existing", migrated: false, preflight }; const rawConfig = await readMigrationConfig(params); const config = withoutPluginInstallRecords(rawConfig); const installRecords = migrateOfficialPluginInstallProvenance(params.installRecords ?? await loadInstalledPluginIndexInstallRecords(params)); const migrationParams = { ...params, config, installRecords }; const current = { ...loadInstalledPluginIndex({ ...migrationParams }), refreshReason: "migration" }; await writePersistedInstalledPluginIndex(current, params); return { status: "migrated", migrated: true, preflight, current }; } //#endregion //#region src/commands/doctor-plugin-registry.ts /** Doctor repairs for stale plugin registry entries, managed npm shadows, and peer links. */ function readJsonObject(filePath) { const parsed = tryReadJsonSync(filePath); return isRecord(parsed) ? parsed : null; } function readStringMap(value) { if (!isRecord(value)) return {}; const result = {}; for (const [key, raw] of Object.entries(value)) if (typeof raw === "string" && raw.trim()) result[key] = raw.trim(); return result; } function deleteObjectKey(record, key) { if (!Object.hasOwn(record, key)) return false; delete record[key]; return true; } function listStaleManagedNpmBundledPlugins(params) { const currentBundled = loadInstalledPluginIndex({ ...params, installRecords: {} }).plugins.filter((plugin) => plugin.origin === "bundled" && !isExternallyDistributedPlugin(plugin)); const bundledByPackage = new Map(currentBundled.map((plugin) => [plugin.packageName, plugin])); const stale = []; for (const npmRoot of resolveDoctorPluginNpmRoots(params)) { const dependencies = readStringMap(readJsonObject(path.join(npmRoot, "package.json"))?.dependencies); for (const packageName of Object.keys(dependencies).toSorted((left, right) => left.localeCompare(right))) { if (!packageName.startsWith("@openclaw/")) continue; const bundled = bundledByPackage.get(packageName); if (!bundled) continue; const packageDir = path.join(npmRoot, "node_modules", ...packageName.split("/")); if (hasRetainedManagedNpmInstallMarker(packageDir)) continue; const pluginId = normalizeOptionalString(readJsonObject(path.join(packageDir, "openclaw.plugin.json"))?.id); if (!pluginId || pluginId !== bundled.pluginId) continue; const version = normalizeOptionalString(readJsonObject(path.join(packageDir, "package.json"))?.version); stale.push({ pluginId, packageName, packageDir, npmRoot, ...version ? { version } : {} }); } } return stale; } function loadCurrentBundledPluginSources(params) { const currentBundled = loadInstalledPluginIndex({ ...params, installRecords: {} }).plugins.filter((plugin) => plugin.origin === "bundled"); return new Map(currentBundled.map((plugin) => [plugin.pluginId, { pluginId: plugin.pluginId, localPath: plugin.rootDir, ...plugin.packageName ? { npmSpec: plugin.packageName } : {}, ...plugin.packageVersion ? { version: plugin.packageVersion } : {} }])); } async function listStaleLocalBundledPluginInstallRecordShadows(params) { return listStaleLocalBundledPluginInstallRecords({ installRecords: await loadInstalledPluginIndexInstallRecords(params), workspaceDir: params.workspaceDir, env: params.env, bundled: loadCurrentBundledPluginSources(params) }); } function removeManagedNpmDependency(params) { const npmPackageJsonPath = path.join(params.npmRoot, "package.json"); const packageJson = readJsonObject(npmPackageJsonPath) ?? {}; const dependencies = readStringMap(packageJson.dependencies); delete dependencies[params.packageName]; if (Object.keys(dependencies).length === 0) delete packageJson.dependencies; else packageJson.dependencies = dependencies; writeJsonTarget(npmPackageJsonPath, packageJson); removeManagedNpmPackageLockDependency(params); fs.rmSync(params.packageDir, { recursive: true, force: true }); const scopeDir = path.dirname(params.packageDir); if (path.basename(path.dirname(scopeDir)) === "node_modules") try { fs.rmdirSync(scopeDir); } catch {} } function removeManagedNpmPackageLockDependency(params) { const packageLockPath = path.join(params.npmRoot, "package-lock.json"); const packageLock = readJsonObject(packageLockPath); if (!packageLock) return; let changed = false; const packages = packageLock.packages; if (isRecord(packages)) { const rootPackage = packages[""]; if (isRecord(rootPackage)) { const rootDependencies = readStringMap(rootPackage.dependencies); if (deleteObjectKey(rootDependencies, params.packageName)) { changed = true; if (Object.keys(rootDependencies).length === 0) delete rootPackage.dependencies; else rootPackage.dependencies = rootDependencies; } } changed = deleteObjectKey(packages, `node_modules/${params.packageName}`) || changed; } const dependencies = packageLock.dependencies; if (isRecord(dependencies)) changed = deleteObjectKey(dependencies, params.packageName) || changed; if (changed) writeJsonTarget(packageLockPath, packageLock); } /** Removes managed npm packages that shadow current bundled plugins when repair is enabled. */ function maybeRepairStaleManagedNpmBundledPlugins(params) { const stale = listStaleManagedNpmBundledPlugins(params); if (stale.length === 0) return null; if (!params.prompter.shouldRepair) { note([ "Managed npm plugin packages shadow bundled plugins:", ...stale.map((plugin) => `- ${plugin.pluginId}: ${plugin.packageName}${plugin.version ? `@${plugin.version}` : ""}`), `Repair with ${formatCliCommand("openclaw doctor --fix")} to remove stale managed npm packages and rebuild the plugin registry.` ].join("\n"), "Plugin registry"); return null; } let installRecords = params.installRecords ?? loadInstalledPluginIndexInstallRecordsSync(params); const removedPluginIds = [...new Set(stale.map((plugin) => plugin.pluginId))].toSorted((left, right) => left.localeCompare(right)); for (const pluginId of removedPluginIds) installRecords = removePluginInstallRecordFromRecords(installRecords, pluginId); for (const plugin of stale) removeManagedNpmDependency(plugin); note(["Removed stale managed npm plugin package(s) shadowing bundled plugins:", ...stale.map((plugin) => `- ${plugin.pluginId}: ${plugin.packageName}${plugin.version ? `@${plugin.version}` : ""}`)].join("\n"), "Plugin registry"); return { installRecords, removedPluginIds }; } /** Removes local install records that shadow current bundled plugin sources. */ async function maybeRepairStaleLocalBundledPluginInstallRecords(params) { const stale = await listStaleLocalBundledPluginInstallRecordShadows(params); if (stale.length === 0) return []; if (!params.prompter.shouldRepair) { note([ "Local bundled plugin install records shadow bundled plugins:", ...stale.map((record) => `- ${record.pluginId}: ${shortenHomePath(record.stalePath)}`), `Repair with ${formatCliCommand("openclaw doctor --fix")} to remove stale local install records and rebuild the plugin registry.` ].join("\n"), "Plugin registry"); return []; } note(["Removed stale local bundled plugin install record(s) shadowing bundled plugins:", ...stale.map((record) => `- ${record.pluginId}: ${shortenHomePath(record.stalePath)}`)].join("\n"), "Plugin registry"); return stale.map((record) => record.pluginId); } async function loadRepairedPluginInstallRecords(params, pluginIds, baselineRecords) { let records = baselineRecords ?? await loadInstalledPluginIndexInstallRecords(params); for (const pluginId of pluginIds) records = removePluginInstallRecordFromRecords(records, pluginId); return migrateOfficialPluginInstallProvenance(records); } async function detectPluginRegistryHealthIssues(params) { const preflight = preflightPluginRegistryDoctorMigration(params); const issues = []; if (preflight.action === "migrate") issues.push({ kind: "registry-missing-or-stale", path: preflight.filePath }); for (const plugin of listStaleManagedNpmBundledPlugins(params)) issues.push({ kind: "stale-managed-npm-bundled-plugin", ...plugin }); for (const record of await listStaleLocalBundledPluginInstallRecordShadows(params)) issues.push({ kind: "stale-local-bundled-plugin-install-record", pluginId: record.pluginId, stalePath: record.stalePath }); issues.push(...await listStaleManagedNpmInstallGenerations(params)); const hostLinkAudit = await listPluginOpenClawHostLinkIssues(params); for (const issue of hostLinkAudit.peerLinkIssues) issues.push({ kind: "managed-npm-openclaw-peer-link", ...issue }); for (const failure of hostLinkAudit.packageReadFailures) issues.push({ kind: "managed-npm-package-unreadable", ...failure }); for (const issue of hostLinkAudit.registeredPeerLinkIssues) issues.push({ kind: "registered-npm-openclaw-host-link", ...issue }); for (const failure of hostLinkAudit.registeredPackageReadFailures) issues.push({ kind: "registered-npm-package-unreadable", ...failure }); return issues; } function pluginRegistryIssueToHealthFinding(issue) { const finding = (message, findingPath, fixHint, target) => ({ checkId: PLUGIN_REGISTRY_CHECK_ID, severity: "warning", message, path: findingPath, ...target === void 0 ? {} : { target }, fixHint }); switch (issue.kind) { case "registry-missing-or-stale": return finding("Persisted plugin registry is missing or stale.", issue.path, "Run `openclaw doctor --fix` to rebuild the plugin registry from enabled plugins."); case "stale-managed-npm-bundled-plugin": return finding(`Managed npm package ${issue.packageName}${issue.version ? `@${issue.version}` : ""} shadows bundled plugin ${issue.pluginId}.`, issue.packageDir, "Run `openclaw doctor --fix` to remove stale managed npm packages and rebuild the plugin registry.", issue.pluginId); case "stale-local-bundled-plugin-install-record": return finding(`Local install record for bundled plugin ${issue.pluginId} points at a stale path.`, issue.stalePath, "Run `openclaw doctor --fix` to remove stale local install records and rebuild the plugin registry.", issue.pluginId); case "managed-npm-openclaw-peer-link": return finding(`Managed npm package ${issue.packageName} has a broken OpenClaw peer link: ${issue.reason}.`, issue.packageDir, "Run `openclaw doctor --fix` to relink managed npm plugin packages.", issue.packageName); case "registered-npm-openclaw-host-link": return finding(`Registered plugin ${issue.packageName} has a broken OpenClaw host link: ${issue.reason}.`, issue.packageDir, "Run `openclaw doctor --fix` to relink the installed plugin package.", issue.packageName); case "managed-npm-package-unreadable": case "registered-npm-package-unreadable": return finding(`${issue.kind === "managed-npm-package-unreadable" ? "Managed npm" : "Registered plugin"} package could not be inspected: ${issue.reason}.`, issue.packageDir, "Restore access to the package files, then run `openclaw doctor` again."); case "stale-managed-npm-install-generation": return staleManagedNpmInstallGenerationToHealthFinding(issue); } return assertNeverPluginRegistryIssue(issue); } function pluginRegistryIssueToRepairEffect(issue) { const effect = (kind, action, target) => ({ kind, action, target, dryRunSafe: false }); switch (issue.kind) { case "registry-missing-or-stale": return effect("state", "would-rebuild-plugin-registry", issue.path); case "stale-managed-npm-bundled-plugin": return effect("package", "would-remove-stale-managed-npm-bundled-plugin", issue.packageDir); case "stale-local-bundled-plugin-install-record": return effect("state", "would-remove-stale-local-bundled-plugin-install-record", issue.pluginId); case "managed-npm-openclaw-peer-link": return effect("package", "would-relink-managed-npm-openclaw-peer", issue.packageDir); case "registered-npm-openclaw-host-link": return effect("package", "would-relink-registered-npm-openclaw-host", issue.packageDir); case "managed-npm-package-unreadable": return effect("package", "requires-managed-npm-package-readability-repair", issue.packageDir); case "registered-npm-package-unreadable": return effect("package", "requires-registered-npm-package-readability-repair", issue.packageDir); case "stale-managed-npm-install-generation": return staleManagedNpmInstallGenerationToRepairEffect(issue); } return assertNeverPluginRegistryIssue(issue); } function assertNeverPluginRegistryIssue(issue) { throw new Error(`Unhandled plugin registry issue kind: ${String(issue.kind)}`); } /** * Runs plugin registry doctor repairs and refreshes the persisted plugin index when needed. * * Stale bundled shadows are removed before registry migration so the rebuilt index resolves the * current bundled source instead of an obsolete managed/local install record. */ async function maybeRepairPluginRegistryState(params) { const readPreflight = () => { try { return preflightPluginRegistryDoctorMigration(params); } catch (error) { if (!(error instanceof InvalidPluginInstallRecordStateError)) throw error; note(error.message, "Plugin registry"); return; } }; const initial = readPreflight(); if (!initial) return { config: params.config }; if (!params.prompter.shouldRepair) return await inspectOrRepairPluginRegistryState(params, initial); return await withPluginLifecycleLease(resolveInstalledPluginIndexStateDatabaseOptions(params), async () => { const current = readPreflight(); return current ? inspectOrRepairPluginRegistryState(params, current) : { config: params.config }; }); } async function inspectOrRepairPluginRegistryState(params, preflight) { clearLoadInstalledPluginIndexInstallRecordsCache(); const staleManagedNpmBundledPluginRepair = maybeRepairStaleManagedNpmBundledPlugins(params); const removedStaleLocalBundledPluginIds = await maybeRepairStaleLocalBundledPluginInstallRecords(params); await maybeRepairStaleManagedNpmInstallGenerations(params); const repairedPluginOpenClawHostLinks = await maybeRepairPluginOpenClawHostLinks(params); const stalePluginIdsToRemove = [.../* @__PURE__ */ new Set([...staleManagedNpmBundledPluginRepair?.removedPluginIds ?? [], ...removedStaleLocalBundledPluginIds])]; if (!params.prompter.shouldRepair) { if (preflight.action === "migrate") note(["Persisted plugin registry is missing or stale.", `Repair with ${formatCliCommand("openclaw doctor --fix")} to rebuild ${shortenHomePath(preflight.filePath)} from enabled plugins.`].join("\n"), "Plugin registry"); return { config: params.config }; } const installRecords = await loadRepairedPluginInstallRecords(params, stalePluginIdsToRemove, staleManagedNpmBundledPluginRepair?.installRecords); if (preflight.action !== "skip-existing") { const result = await migratePluginRegistryForDoctor({ ...params, installRecords }); if (result.migrated) { const total = result.current.plugins.length; const enabled = result.current.plugins.filter((plugin) => plugin.enabled).length; note(`Plugin registry rebuilt: ${enabled}/${total} enabled plugins indexed.`, "Plugin registry"); } return { config: params.config, ...result.migrated ? { pluginInventoryChanged: true } : {} }; } const index = await refreshPluginRegistry({ ...params, reason: "migration", installRecords }); const total = index.plugins.length; const enabled = index.plugins.filter((plugin) => plugin.enabled).length; note(`Plugin registry refreshed: ${enabled}/${total} enabled plugins indexed.`, "Plugin registry"); const indexChanged = resolveInstalledManifestRegistryIndexFingerprint(preflight.current) !== resolveInstalledManifestRegistryIndexFingerprint(index); return { config: params.config, ...indexChanged || repairedPluginOpenClawHostLinks ? { pluginInventoryChanged: true } : {} }; } //#endregion export { pluginRegistryIssueToRepairEffect as a, pluginRegistryIssueToHealthFinding as i, maybeRepairPluginRegistryState as n, maybeRepairPluginOpenClawHostLinks as o, maybeRepairStaleManagedNpmBundledPlugins as r, pruneStaleLocalBundledPluginInstallRecords as s, detectPluginRegistryHealthIssues as t };