diff --git a/src/plugins/bundled-channel-runtime.ts b/src/plugins/bundled-channel-runtime.ts index 57f00e90909..6f43fb8bff8 100644 --- a/src/plugins/bundled-channel-runtime.ts +++ b/src/plugins/bundled-channel-runtime.ts @@ -1,11 +1,85 @@ -import { - listBundledPluginMetadata, - resolveBundledPluginGeneratedPath, - resolveBundledPluginWorkspaceSourcePath, - type BundledPluginMetadata, -} from "./bundled-plugin-metadata.js"; +import fs from "node:fs"; +import path from "node:path"; +import { resolveBundledPluginGeneratedPath } from "./bundled-plugin-metadata.js"; +import type { PluginManifestRecord } from "./manifest-registry.js"; +import type { OpenClawPackageManifest } from "./manifest.js"; +import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js"; -export type BundledChannelPluginMetadata = BundledPluginMetadata; +type BundledChannelEntryPathPair = { + source: string; + built: string; +}; + +export type BundledChannelPluginMetadata = { + dirName: string; + source: BundledChannelEntryPathPair; + setupSource?: BundledChannelEntryPathPair; + manifest: { + id: string; + channels?: readonly string[]; + }; + packageManifest?: OpenClawPackageManifest; + rootDir: string; +}; + +function resolveBundledMetadataEnv(params?: { + rootDir?: string; + scanDir?: string; +}): NodeJS.ProcessEnv | undefined { + const overrideDir = params?.scanDir + ? path.resolve(params.scanDir) + : params?.rootDir + ? resolveBundledPluginsDirForRoot(params.rootDir) + : undefined; + if (!overrideDir) { + return undefined; + } + return { + ...process.env, + OPENCLAW_BUNDLED_PLUGINS_DIR: overrideDir, + OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR: "1", + }; +} + +function resolveBundledPluginsDirForRoot(rootDir: string): string | undefined { + const candidates = [ + path.join(rootDir, "extensions"), + path.join(rootDir, "dist-runtime", "extensions"), + path.join(rootDir, "dist", "extensions"), + ]; + return candidates.find((candidate) => fs.existsSync(candidate)); +} + +function toBundledChannelEntryPair(source: string | undefined): BundledChannelEntryPathPair | null { + if (!source) { + return null; + } + return { source, built: source }; +} + +function toBundledChannelPluginMetadata( + record: PluginManifestRecord, +): BundledChannelPluginMetadata | null { + if (record.origin !== "bundled") { + return null; + } + const source = toBundledChannelEntryPair(record.source); + if (!source) { + return null; + } + const setupSource = toBundledChannelEntryPair(record.setupSource); + return { + dirName: path.basename(record.rootDir), + source, + ...(setupSource ? { setupSource } : {}), + manifest: { + id: record.id, + channels: record.channels, + }, + ...(record.packageManifest ? { packageManifest: record.packageManifest } : {}), + rootDir: record.rootDir, + }; +} export function listBundledChannelPluginMetadata(params?: { rootDir?: string; @@ -13,12 +87,15 @@ export function listBundledChannelPluginMetadata(params?: { includeChannelConfigs?: boolean; includeSyntheticChannelConfigs?: boolean; }): readonly BundledChannelPluginMetadata[] { - return listBundledPluginMetadata(params); + return loadPluginManifestRegistryForPluginRegistry({ + env: resolveBundledMetadataEnv(params), + includeDisabled: true, + }).plugins.flatMap((record) => toBundledChannelPluginMetadata(record) ?? []); } export function resolveBundledChannelGeneratedPath( rootDir: string, - entry: BundledPluginMetadata["source"] | BundledPluginMetadata["setupSource"], + entry: BundledChannelPluginMetadata["source"] | BundledChannelPluginMetadata["setupSource"], pluginDirName?: string, scanDir?: string, ): string | null { @@ -30,5 +107,12 @@ export function resolveBundledChannelWorkspacePath(params: { scanDir?: string; pluginId: string; }): string | null { - return resolveBundledPluginWorkspaceSourcePath(params); + return ( + listBundledChannelPluginMetadata({ + rootDir: params.rootDir, + ...(params.scanDir ? { scanDir: params.scanDir } : {}), + includeChannelConfigs: false, + includeSyntheticChannelConfigs: false, + }).find((metadata) => metadata.manifest.id === params.pluginId)?.rootDir ?? null + ); } diff --git a/src/plugins/bundled-plugin-metadata.ts b/src/plugins/bundled-plugin-metadata.ts index fd5f593c8d5..a0dd55ed3ed 100644 --- a/src/plugins/bundled-plugin-metadata.ts +++ b/src/plugins/bundled-plugin-metadata.ts @@ -29,7 +29,6 @@ const CURRENT_MODULE_PATH = fileURLToPath(import.meta.url); const RUNNING_FROM_BUILT_ARTIFACT = CURRENT_MODULE_PATH.includes(`${path.sep}dist${path.sep}`) || CURRENT_MODULE_PATH.includes(`${path.sep}dist-runtime${path.sep}`); -const DEFAULT_ROOT_METADATA_CACHE = new Map(); type BundledPluginPathPair = { source: string; @@ -186,18 +185,6 @@ export function listBundledPluginMetadata(params?: { const includeChannelConfigs = params?.includeChannelConfigs ?? !RUNNING_FROM_BUILT_ARTIFACT; const includeSyntheticChannelConfigs = params?.includeSyntheticChannelConfigs ?? includeChannelConfigs; - const cacheKey = - !params?.rootDir && !scanDir - ? JSON.stringify({ - resolvedScanDir, - includeChannelConfigs, - includeSyntheticChannelConfigs, - }) - : undefined; - const cached = cacheKey ? DEFAULT_ROOT_METADATA_CACHE.get(cacheKey) : undefined; - if (cached) { - return cached; - } const metadata = Object.freeze( collectBundledPluginMetadata( resolvedScanDir, @@ -205,9 +192,6 @@ export function listBundledPluginMetadata(params?: { includeSyntheticChannelConfigs, ), ); - if (cacheKey) { - DEFAULT_ROOT_METADATA_CACHE.set(cacheKey, metadata); - } return metadata; } diff --git a/src/plugins/config-contracts.test.ts b/src/plugins/config-contracts.test.ts index 938b8a69485..b654ea043a4 100644 --- a/src/plugins/config-contracts.test.ts +++ b/src/plugins/config-contracts.test.ts @@ -4,15 +4,20 @@ import type { PluginManifestRegistry } from "./manifest-registry.js"; const mocks = vi.hoisted(() => { const loadManifestRegistry = vi.fn(); return { - findBundledPluginMetadataById: vi.fn(), + discoverOpenClawPlugins: vi.fn(() => ({ candidates: [], diagnostics: [] })), + loadBundledManifestRegistry: vi.fn(), loadPluginManifestRegistryForInstalledIndex: loadManifestRegistry, loadPluginManifestRegistryForPluginRegistry: loadManifestRegistry, loadPluginRegistrySnapshot: vi.fn(() => ({ plugins: [] })), }; }); -vi.mock("./bundled-plugin-metadata.js", () => ({ - findBundledPluginMetadataById: mocks.findBundledPluginMetadataById, +vi.mock("./discovery.js", () => ({ + discoverOpenClawPlugins: mocks.discoverOpenClawPlugins, +})); + +vi.mock("./manifest-registry.js", () => ({ + loadPluginManifestRegistry: mocks.loadBundledManifestRegistry, })); vi.mock("./manifest-registry-installed.js", () => ({ @@ -77,14 +82,17 @@ function createPluginRecord( describe("resolvePluginConfigContractsById", () => { beforeEach(() => { - mocks.findBundledPluginMetadataById.mockReset(); + mocks.discoverOpenClawPlugins.mockReset(); + mocks.discoverOpenClawPlugins.mockReturnValue({ candidates: [], diagnostics: [] }); + mocks.loadBundledManifestRegistry.mockReset(); + mocks.loadBundledManifestRegistry.mockReturnValue(createRegistry([])); mocks.loadPluginManifestRegistryForInstalledIndex.mockReset(); mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue(createRegistry([])); mocks.loadPluginRegistrySnapshot.mockReset(); mocks.loadPluginRegistrySnapshot.mockReturnValue({ plugins: [] }); }); - it("does not fall back to bundled metadata when registry already resolved a plugin without config contracts", () => { + it("does not fall back to bundled registry when registry already resolved a plugin without config contracts", () => { mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue( createRegistry([ createPluginRecord({ @@ -99,10 +107,10 @@ describe("resolvePluginConfigContractsById", () => { pluginIds: ["brave"], }), ).toEqual(new Map()); - expect(mocks.findBundledPluginMetadataById).not.toHaveBeenCalled(); + expect(mocks.loadBundledManifestRegistry).not.toHaveBeenCalled(); }); - it("can hydrate missing contracts from bundled metadata for resolved bundled plugins", () => { + it("can hydrate missing contracts from bundled registry for resolved bundled plugins", () => { mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue( createRegistry([ createPluginRecord({ @@ -114,15 +122,19 @@ describe("resolvePluginConfigContractsById", () => { }), ]), ); - mocks.findBundledPluginMetadataById.mockReturnValue({ - manifest: { - configContracts: { - secretInputs: { - paths: [{ path: "twilio.authToken", expected: "string" }], + mocks.loadBundledManifestRegistry.mockReturnValue( + createRegistry([ + createPluginRecord({ + id: "voice-call", + origin: "bundled", + configContracts: { + secretInputs: { + paths: [{ path: "twilio.authToken", expected: "string" }], + }, }, - }, - }, - }); + }), + ]), + ); expect( resolvePluginConfigContractsById({ @@ -147,7 +159,7 @@ describe("resolvePluginConfigContractsById", () => { ); }); - it("refreshes stale bundled SecretInput contracts from bundled metadata", () => { + it("refreshes stale bundled SecretInput contracts from bundled registry", () => { mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue( createRegistry([ createPluginRecord({ @@ -162,18 +174,22 @@ describe("resolvePluginConfigContractsById", () => { }), ]), ); - mocks.findBundledPluginMetadataById.mockReturnValue({ - manifest: { - configContracts: { - secretInputs: { - paths: [ - { path: "twilio.authToken", expected: "string" }, - { path: "realtime.providers.*.apiKey", expected: "string" }, - ], + mocks.loadBundledManifestRegistry.mockReturnValue( + createRegistry([ + createPluginRecord({ + id: "voice-call", + origin: "bundled", + configContracts: { + secretInputs: { + paths: [ + { path: "twilio.authToken", expected: "string" }, + { path: "realtime.providers.*.apiKey", expected: "string" }, + ], + }, }, - }, - }, - }); + }), + ]), + ); expect( resolvePluginConfigContractsById({ @@ -210,15 +226,19 @@ describe("resolvePluginConfigContractsById", () => { }), ]), ); - mocks.findBundledPluginMetadataById.mockReturnValue({ - manifest: { - configContracts: { - secretInputs: { - paths: [{ path: "tts.providers.*.apiKey", expected: "string" }], + mocks.loadBundledManifestRegistry.mockReturnValue( + createRegistry([ + createPluginRecord({ + id: "voice-call", + origin: "bundled", + configContracts: { + secretInputs: { + paths: [{ path: "tts.providers.*.apiKey", expected: "string" }], + }, }, - }, - }, - }); + }), + ]), + ); expect( resolvePluginConfigContractsById({ @@ -249,6 +269,6 @@ describe("resolvePluginConfigContractsById", () => { fallbackToBundledMetadata: false, }), ).toEqual(new Map()); - expect(mocks.findBundledPluginMetadataById).not.toHaveBeenCalled(); + expect(mocks.loadBundledManifestRegistry).not.toHaveBeenCalled(); }); }); diff --git a/src/plugins/config-contracts.ts b/src/plugins/config-contracts.ts index 0b08bab8ac1..a8f1bd69b4c 100644 --- a/src/plugins/config-contracts.ts +++ b/src/plugins/config-contracts.ts @@ -1,6 +1,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import { isRecord } from "../utils.js"; -import { findBundledPluginMetadataById } from "./bundled-plugin-metadata.js"; +import { discoverOpenClawPlugins } from "./discovery.js"; +import { loadPluginManifestRegistry } from "./manifest-registry.js"; import type { PluginManifestConfigContracts } from "./manifest.js"; import type { PluginOrigin } from "./plugin-origin.types.js"; import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js"; @@ -116,6 +117,32 @@ export function resolvePluginConfigContractsById(params: { const fallbackBundledPluginIds = new Set( (params.fallbackBundledPluginIds ?? []).map((pluginId) => pluginId.trim()).filter(Boolean), ); + const bundledContractFallbacks = new Map(); + const findBundledConfigContracts = ( + pluginId: string, + ): PluginManifestConfigContracts | undefined => { + if (bundledContractFallbacks.has(pluginId)) { + return bundledContractFallbacks.get(pluginId); + } + const discovery = discoverOpenClawPlugins({ + workspaceDir: params.workspaceDir, + env: params.env, + }); + const registry = loadPluginManifestRegistry({ + config: params.config, + workspaceDir: params.workspaceDir, + env: params.env, + candidates: discovery.candidates.filter((candidate) => candidate.origin === "bundled"), + diagnostics: discovery.diagnostics, + }); + for (const plugin of registry.plugins) { + bundledContractFallbacks.set(plugin.id, plugin.configContracts); + } + if (!bundledContractFallbacks.has(pluginId)) { + bundledContractFallbacks.set(pluginId, undefined); + } + return bundledContractFallbacks.get(pluginId); + }; const resolvedPluginOrigins = new Map(); const registry = loadPluginManifestRegistryForPluginRegistry({ @@ -146,18 +173,15 @@ export function resolvePluginConfigContractsById(params: { ((params.fallbackToBundledMetadataForResolvedBundled && existing.origin === "bundled") || fallbackBundledPluginIds.has(pluginId)); if (shouldHydrateBundledMatch) { - const bundled = findBundledPluginMetadataById(pluginId, { - includeChannelConfigs: false, - includeSyntheticChannelConfigs: false, - }); - if (bundled?.manifest.configContracts) { + const bundledConfigContracts = findBundledConfigContracts(pluginId); + if (bundledConfigContracts) { matches.set(pluginId, { origin: fallbackBundledPluginIds.has(pluginId) ? "bundled" : existing.origin, configContracts: { - ...bundled.manifest.configContracts, + ...bundledConfigContracts, ...existing.configContracts, - ...(bundled.manifest.configContracts.secretInputs - ? { secretInputs: bundled.manifest.configContracts.secretInputs } + ...(bundledConfigContracts.secretInputs + ? { secretInputs: bundledConfigContracts.secretInputs } : {}), }, }); @@ -175,16 +199,13 @@ export function resolvePluginConfigContractsById(params: { ) { continue; } - const bundled = findBundledPluginMetadataById(pluginId, { - includeChannelConfigs: false, - includeSyntheticChannelConfigs: false, - }); - if (!bundled?.manifest.configContracts) { + const bundledConfigContracts = findBundledConfigContracts(pluginId); + if (!bundledConfigContracts) { continue; } matches.set(pluginId, { origin: "bundled", - configContracts: bundled.manifest.configContracts, + configContracts: bundledConfigContracts, }); } } diff --git a/src/plugins/config-state.test.ts b/src/plugins/config-state.test.ts index f4597f1e00c..afce22dc940 100644 --- a/src/plugins/config-state.test.ts +++ b/src/plugins/config-state.test.ts @@ -155,12 +155,13 @@ describe("normalizePluginsConfig", () => { expect(result.entries.minimax?.enabled).toBe(false); }); - it("reuses the bundled alias scan during one config normalization", async () => { + it("reuses the plugin alias discovery during one config normalization", async () => { vi.resetModules(); - const bundledPluginMetadata = await import("./bundled-plugin-metadata.js"); - const listBundledMetadata = vi.spyOn(bundledPluginMetadata, "listBundledPluginMetadata"); + const discovery = await import("./discovery.js"); + const discoverPlugins = vi.spyOn(discovery, "discoverOpenClawPlugins"); const { normalizePluginsConfig: normalizeFreshPluginsConfig } = await import("./config-state.js"); + discoverPlugins.mockClear(); const result = normalizeFreshPluginsConfig({ allow: ["unknown-plugin-one", "unknown-plugin-two"], @@ -175,7 +176,7 @@ describe("normalizePluginsConfig", () => { expect(result.allow).toEqual(["unknown-plugin-one", "unknown-plugin-two"]); expect(result.deny).toEqual(["unknown-plugin-three"]); expect(result.entries["unknown-plugin-four"]?.enabled).toBe(true); - expect(listBundledMetadata).toHaveBeenCalledTimes(1); + expect(discoverPlugins).toHaveBeenCalledTimes(1); }); }); diff --git a/src/plugins/config-state.ts b/src/plugins/config-state.ts index 7f33b889dde..614fda6d2e5 100644 --- a/src/plugins/config-state.ts +++ b/src/plugins/config-state.ts @@ -3,7 +3,6 @@ import { normalizeOptionalLowercaseString, normalizeOptionalString, } from "../shared/string-coerce.js"; -import { listBundledPluginMetadata } from "./bundled-plugin-metadata.js"; import { createEffectiveEnableStateResolver, createPluginEnableStateResolver, @@ -21,6 +20,8 @@ import { type NormalizePluginId, type NormalizedPluginsConfig as SharedNormalizedPluginsConfig, } from "./config-normalization-shared.js"; +import { discoverOpenClawPlugins } from "./discovery.js"; +import { loadPluginManifest } from "./manifest.js"; import type { PluginOrigin } from "./plugin-origin.types.js"; import { defaultSlotIdForKey } from "./slots.js"; @@ -45,36 +46,38 @@ const BUILT_IN_PLUGIN_ALIAS_LOOKUP = new Map([ ...BUILT_IN_PLUGIN_ALIAS_FALLBACKS.map(([, pluginId]) => [pluginId, pluginId] as const), ]); -let bundledPluginAliasLookup: ReadonlyMap | undefined; - function getBundledPluginAliasLookup(): ReadonlyMap { - if (bundledPluginAliasLookup) { - return bundledPluginAliasLookup; - } const lookup = new Map(); - for (const plugin of listBundledPluginMetadata({ includeChannelConfigs: false })) { - const pluginId = normalizeOptionalLowercaseString(plugin.manifest.id); - if (pluginId) { - lookup.set(pluginId, plugin.manifest.id); + for (const candidate of discoverOpenClawPlugins({}).candidates) { + const manifestResult = + candidate.origin === "bundled" && candidate.bundledManifest + ? { ok: true as const, manifest: candidate.bundledManifest } + : loadPluginManifest(candidate.rootDir, candidate.origin !== "bundled"); + if (!manifestResult.ok) { + continue; } - for (const providerId of plugin.manifest.providers ?? []) { + const manifest = manifestResult.manifest; + const pluginId = normalizeOptionalLowercaseString(manifest.id); + if (pluginId) { + lookup.set(pluginId, manifest.id); + } + for (const providerId of manifest.providers ?? []) { const normalizedProviderId = normalizeOptionalLowercaseString(providerId); if (normalizedProviderId) { - lookup.set(normalizedProviderId, plugin.manifest.id); + lookup.set(normalizedProviderId, manifest.id); } } - for (const legacyPluginId of plugin.manifest.legacyPluginIds ?? []) { + for (const legacyPluginId of manifest.legacyPluginIds ?? []) { const normalizedLegacyPluginId = normalizeOptionalLowercaseString(legacyPluginId); if (normalizedLegacyPluginId) { - lookup.set(normalizedLegacyPluginId, plugin.manifest.id); + lookup.set(normalizedLegacyPluginId, manifest.id); } } } for (const [alias, pluginId] of BUILT_IN_PLUGIN_ALIAS_FALLBACKS) { lookup.set(alias, pluginId); } - bundledPluginAliasLookup = lookup; - return bundledPluginAliasLookup; + return lookup; } function normalizePluginIdWithLookup( diff --git a/src/plugins/installed-plugin-index-record-builder.ts b/src/plugins/installed-plugin-index-record-builder.ts index 9df34c47e4b..8c87b64f3eb 100644 --- a/src/plugins/installed-plugin-index-record-builder.ts +++ b/src/plugins/installed-plugin-index-record-builder.ts @@ -135,19 +135,6 @@ function normalizeStringField(value: unknown): string | undefined { return normalized ? normalized : undefined; } -function normalizeStringListField(value: unknown): readonly string[] | undefined { - if (!Array.isArray(value)) { - return undefined; - } - const normalized = value - .flatMap((entry) => { - const normalizedEntry = normalizeStringField(entry); - return normalizedEntry ? [normalizedEntry] : []; - }) - .filter((entry, index, all) => all.indexOf(entry) === index); - return normalized.length > 0 ? normalized : undefined; -} - function normalizePackageChannel( channel: PluginPackageChannel | undefined, ): InstalledPluginPackageChannelInfo | undefined { @@ -155,30 +142,9 @@ function normalizePackageChannel( if (!id) { return undefined; } - const label = normalizeStringField(channel?.label); - const blurb = normalizeStringField(channel?.blurb); - const preferOver = normalizeStringListField(channel?.preferOver); - const commands = - channel?.commands && - typeof channel.commands === "object" && - !Array.isArray(channel.commands) && - (typeof channel.commands.nativeCommandsAutoEnabled === "boolean" || - typeof channel.commands.nativeSkillsAutoEnabled === "boolean") - ? { - ...(typeof channel.commands.nativeCommandsAutoEnabled === "boolean" - ? { nativeCommandsAutoEnabled: channel.commands.nativeCommandsAutoEnabled } - : {}), - ...(typeof channel.commands.nativeSkillsAutoEnabled === "boolean" - ? { nativeSkillsAutoEnabled: channel.commands.nativeSkillsAutoEnabled } - : {}), - } - : undefined; return { + ...structuredClone(channel), id, - ...(label ? { label } : {}), - ...(blurb ? { blurb } : {}), - ...(preferOver ? { preferOver } : {}), - ...(commands ? { commands } : {}), }; } @@ -240,7 +206,9 @@ export function buildInstalledPluginIndexRecords(params: { const packageJsonPath = resolvePackageJsonPath(candidate); const installRecord = params.installRecords[record.id]; const packageInstall = describePackageInstallSource(candidate); - const packageChannel = normalizePackageChannel(candidate?.packageManifest?.channel); + const packageChannel = normalizePackageChannel( + record.packageChannel ?? candidate?.packageManifest?.channel, + ); const manifestHash = resolveManifestHash({ record, diagnostics: params.diagnostics }); const packageJson = resolvePackageJsonRecord({ candidate, diff --git a/src/plugins/installed-plugin-index-types.ts b/src/plugins/installed-plugin-index-types.ts index 6862eb4b815..2eca68e6c82 100644 --- a/src/plugins/installed-plugin-index-types.ts +++ b/src/plugins/installed-plugin-index-types.ts @@ -60,10 +60,7 @@ export type InstalledPluginInstallRecordInfo = Pick< | "marketplacePlugin" >; -export type InstalledPluginPackageChannelInfo = Pick< - PluginPackageChannel, - "id" | "label" | "blurb" | "preferOver" | "commands" ->; +export type InstalledPluginPackageChannelInfo = PluginPackageChannel; export type InstalledPluginIndexRecord = { pluginId: string; diff --git a/src/plugins/manifest-registry-installed.ts b/src/plugins/manifest-registry-installed.ts index 07138c127f1..77e63e872f7 100644 --- a/src/plugins/manifest-registry-installed.ts +++ b/src/plugins/manifest-registry-installed.ts @@ -104,36 +104,36 @@ function resolveFallbackPluginSource(record: InstalledPluginIndexRecord): string function resolveInstalledPackageManifest( record: InstalledPluginIndexRecord, ): OpenClawPackageManifest | undefined { - if (!record.packageChannel) { - return undefined; - } - if (record.packageChannel.commands) { - return { channel: record.packageChannel }; - } const rootDir = resolveInstalledPluginRootDir(record); const packageJsonPath = record.packageJson?.path ? path.resolve(rootDir, record.packageJson.path) : undefined; if (!packageJsonPath) { - return { channel: record.packageChannel }; + return record.packageChannel ? { channel: record.packageChannel } : undefined; } const relative = path.relative(rootDir, packageJsonPath); if (relative.startsWith("..") || path.isAbsolute(relative)) { - return { channel: record.packageChannel }; + return record.packageChannel ? { channel: record.packageChannel } : undefined; } try { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as PackageManifest; const packageManifest = getPackageManifestMetadata(packageJson); + if (!packageManifest) { + return record.packageChannel ? { channel: record.packageChannel } : undefined; + } + const channel = + record.packageChannel || packageManifest.channel + ? { + ...record.packageChannel, + ...packageManifest.channel, + } + : undefined; return { - channel: { - ...record.packageChannel, - ...(packageManifest?.channel?.commands - ? { commands: packageManifest.channel.commands } - : {}), - }, + ...packageManifest, + ...(channel ? { channel } : {}), }; } catch { - return { channel: record.packageChannel }; + return record.packageChannel ? { channel: record.packageChannel } : undefined; } } diff --git a/src/plugins/manifest-registry.ts b/src/plugins/manifest-registry.ts index 8898d4c94f4..6315c061c59 100644 --- a/src/plugins/manifest-registry.ts +++ b/src/plugins/manifest-registry.ts @@ -37,6 +37,8 @@ import { type PluginManifestProviderRequest, type PluginManifestQaRunner, type PluginManifestSetup, + type PluginPackageChannel, + type PluginPackageInstall, } from "./manifest.js"; import { checkMinHostVersion } from "./min-host-version.js"; import { isPathInside, safeRealpathSync } from "./path-safety.js"; @@ -96,6 +98,9 @@ export type PluginManifestRecord = { name?: string; description?: string; version?: string; + packageName?: string; + packageVersion?: string; + packageDescription?: string; enabledByDefault?: boolean; autoEnableWhenConfiguredProviders?: string[]; legacyPluginIds?: string[]; @@ -122,6 +127,9 @@ export type PluginManifestRecord = { providerAuthChoices?: PluginManifest["providerAuthChoices"]; activation?: PluginManifestActivation; setup?: PluginManifestSetup; + packageManifest?: OpenClawPackageManifest; + packageChannel?: PluginPackageChannel; + packageInstall?: PluginPackageInstall; qaRunners?: PluginManifestQaRunner[]; skills: string[]; settingsFiles?: string[]; @@ -269,6 +277,9 @@ function buildRecord(params: { description: normalizeOptionalString(params.manifest.description) ?? params.candidate.packageDescription, version: normalizeOptionalString(params.manifest.version) ?? params.candidate.packageVersion, + packageName: params.candidate.packageName, + packageVersion: params.candidate.packageVersion, + packageDescription: params.candidate.packageDescription, enabledByDefault: params.manifest.enabledByDefault === true ? true : undefined, autoEnableWhenConfiguredProviders: params.manifest.autoEnableWhenConfiguredProviders, legacyPluginIds: params.manifest.legacyPluginIds, @@ -298,6 +309,9 @@ function buildRecord(params: { providerAuthChoices: params.manifest.providerAuthChoices, activation: params.manifest.activation, setup: params.manifest.setup, + packageManifest: params.candidate.packageManifest, + packageChannel: params.candidate.packageManifest?.channel, + packageInstall: params.candidate.packageManifest?.install, qaRunners: params.manifest.qaRunners, skills: params.manifest.skills ?? [], settingsFiles: [], @@ -357,6 +371,12 @@ function buildBundleRecord(params: { name: normalizeOptionalString(params.manifest.name) ?? params.candidate.idHint, description: normalizeOptionalString(params.manifest.description), version: normalizeOptionalString(params.manifest.version), + packageName: params.candidate.packageName, + packageVersion: params.candidate.packageVersion, + packageDescription: params.candidate.packageDescription, + packageManifest: params.candidate.packageManifest, + packageChannel: params.candidate.packageManifest?.channel, + packageInstall: params.candidate.packageManifest?.install, format: "bundle", bundleFormat: params.candidate.bundleFormat, bundleCapabilities: params.manifest.capabilities,