fix(plugins): fall back to bundled plugin when npm spec resolves to non-OpenClaw package (#32019)

When `openclaw plugins install diffs` downloads the unrelated npm
package `diffs@0.1.1` (which lacks `openclaw.extensions`), the install
fails without trying the bundled `@openclaw/diffs` plugin.

Two fixes:
1. Broaden the bundled-fallback trigger to also fire on
   "missing openclaw.extensions" errors (not just npm 404s)
2. Match bundled plugins by pluginId in addition to npmSpec so
   unscoped names like "diffs" resolve to `@openclaw/diffs`

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
scoootscooob 2026-03-02 11:34:20 -08:00 committed by Peter Steinberger
parent 089a8785b9
commit da8a17d8de
3 changed files with 45 additions and 1 deletions

View File

@ -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) {

View File

@ -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");
});
});

View File

@ -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;
}