diff --git a/src/cli/plugins-cli.ts b/src/cli/plugins-cli.ts index fa70fce794f..954359c1908 100644 --- a/src/cli/plugins-cli.ts +++ b/src/cli/plugins-cli.ts @@ -159,6 +159,19 @@ function isPackageNotFoundInstallError(message: string): boolean { ); } +/** + * True when npm downloaded a package successfully but it is not a valid + * OpenClaw plugin (e.g. `diffs` resolves to the unrelated npm package + * `diffs@0.1.1` instead of `@openclaw/diffs`). + * See: https://github.com/openclaw/openclaw/issues/32019 + */ +function isNotAnOpenClawPluginError(message: string): boolean { + const lower = message.toLowerCase(); + return ( + lower.includes("missing openclaw.extensions") || lower.includes("openclaw.extensions is empty") + ); +} + export function registerPluginsCli(program: Command) { const plugins = program .command("plugins") @@ -625,7 +638,9 @@ export function registerPluginsCli(program: Command) { logger: createPluginInstallLogger(), }); if (!result.ok) { - const bundledFallback = isPackageNotFoundInstallError(result.error) + const shouldTryBundledFallback = + isPackageNotFoundInstallError(result.error) || isNotAnOpenClawPluginError(result.error); + const bundledFallback = shouldTryBundledFallback ? findBundledPluginByNpmSpec({ spec: raw }) : undefined; if (!bundledFallback) { diff --git a/src/plugins/bundled-sources.test.ts b/src/plugins/bundled-sources.test.ts index 437b06c193e..603b387fc90 100644 --- a/src/plugins/bundled-sources.test.ts +++ b/src/plugins/bundled-sources.test.ts @@ -94,4 +94,26 @@ describe("bundled plugin sources", () => { expect(resolved?.localPath).toBe("/app/extensions/feishu"); expect(missing).toBeUndefined(); }); + + it("finds bundled source by plugin id when npm spec does not match (#32019)", () => { + discoverOpenClawPluginsMock.mockReturnValue({ + candidates: [ + { + origin: "bundled", + rootDir: "/app/extensions/diffs", + packageName: "@openclaw/diffs", + packageManifest: {}, + }, + ], + diagnostics: [], + }); + loadPluginManifestMock.mockReturnValue({ ok: true, manifest: { id: "diffs" } }); + + // Searching by unscoped name "diffs" should match by pluginId even though + // the npmSpec is "@openclaw/diffs". + const resolved = findBundledPluginByNpmSpec({ spec: "diffs" }); + + expect(resolved?.pluginId).toBe("diffs"); + expect(resolved?.localPath).toBe("/app/extensions/diffs"); + }); }); diff --git a/src/plugins/bundled-sources.ts b/src/plugins/bundled-sources.ts index 44ac618f211..097e980ca35 100644 --- a/src/plugins/bundled-sources.ts +++ b/src/plugins/bundled-sources.ts @@ -54,6 +54,13 @@ export function findBundledPluginByNpmSpec(params: { if (source.npmSpec === targetSpec) { return source; } + // Also match by plugin id so that e.g. `openclaw plugins install diffs` + // resolves to the bundled @openclaw/diffs plugin when the unscoped npm + // package `diffs` is not a valid OpenClaw plugin. + // See: https://github.com/openclaw/openclaw/issues/32019 + if (source.pluginId === targetSpec) { + return source; + } } return undefined; }