From d7a1945b270e0644e854b6e470ab9904c0202d3a Mon Sep 17 00:00:00 2001 From: MoerAI Date: Wed, 25 Mar 2026 17:10:07 +0900 Subject: [PATCH] fix(plugin-loader): preserve scoped npm package names in plugin key parsing Scoped packages like @scope/pkg were truncated to just 'pkg' because basename() strips the scope prefix. Fix: - Detect scoped packages (starting with @) and find version separator after the scope slash, not at the leading @ - Return full scoped name (@scope/pkg) instead of calling basename - Add regression test for scoped package name preservation --- .../discovery.test.ts | 35 +++++++++++++++++++ .../claude-code-plugin-loader/discovery.ts | 13 ++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/features/claude-code-plugin-loader/discovery.test.ts b/src/features/claude-code-plugin-loader/discovery.test.ts index dab25cd22..63e2340a6 100644 --- a/src/features/claude-code-plugin-loader/discovery.test.ts +++ b/src/features/claude-code-plugin-loader/discovery.test.ts @@ -32,6 +32,41 @@ describe("discoverInstalledPlugins", () => { } }) + it("preserves scoped package name from npm plugin keys", () => { + //#given + const pluginsHome = process.env.CLAUDE_PLUGINS_HOME as string + const installPath = join(createTemporaryDirectory("omo-plugin-install-"), "@myorg", "my-plugin") + mkdirSync(installPath, { recursive: true }) + + const databasePath = join(pluginsHome, "installed_plugins.json") + writeFileSync( + databasePath, + JSON.stringify({ + version: 2, + plugins: { + "@myorg/my-plugin@1.0.0": [ + { + scope: "user", + installPath, + version: "1.0.0", + installedAt: "2026-03-25T00:00:00Z", + lastUpdated: "2026-03-25T00:00:00Z", + }, + ], + }, + }), + "utf-8", + ) + + //#when + const discovered = discoverInstalledPlugins() + + //#then + expect(discovered.errors).toHaveLength(0) + expect(discovered.plugins).toHaveLength(1) + expect(discovered.plugins[0]?.name).toBe("@myorg/my-plugin") + }) + it("derives package name from file URL plugin keys", () => { //#given const pluginsHome = process.env.CLAUDE_PLUGINS_HOME as string diff --git a/src/features/claude-code-plugin-loader/discovery.ts b/src/features/claude-code-plugin-loader/discovery.ts index 217e7211e..d4d9bb577 100644 --- a/src/features/claude-code-plugin-loader/discovery.ts +++ b/src/features/claude-code-plugin-loader/discovery.ts @@ -81,7 +81,14 @@ function loadPluginManifest(installPath: string): PluginManifest | null { function derivePluginNameFromKey(pluginKey: string): string { const keyWithoutSource = pluginKey.startsWith("npm:") ? pluginKey.slice(4) : pluginKey - const versionSeparator = keyWithoutSource.lastIndexOf("@") + + let versionSeparator: number + if (keyWithoutSource.startsWith("@")) { + const scopeEnd = keyWithoutSource.indexOf("/") + versionSeparator = scopeEnd > 0 ? keyWithoutSource.indexOf("@", scopeEnd) : -1 + } else { + versionSeparator = keyWithoutSource.lastIndexOf("@") + } const keyWithoutVersion = versionSeparator > 0 ? keyWithoutSource.slice(0, versionSeparator) : keyWithoutSource if (keyWithoutVersion.startsWith("file://")) { @@ -92,6 +99,10 @@ function derivePluginNameFromKey(pluginKey: string): string { } } + if (keyWithoutVersion.startsWith("@") && keyWithoutVersion.includes("/")) { + return keyWithoutVersion + } + if (keyWithoutVersion.includes("/") || keyWithoutVersion.includes("\\")) { return basename(keyWithoutVersion) }