fix(claude-code): correct skill install command and marketplace setup UX

- formatInstallCommand now produces /plugin install {name}@litellm instead of /plugin marketplace add {source}
- extraKnownMarketplaces snippet fixed: source must be a nested object not a flat string; the flat string caused Claude Code to reject the settings file
- marketplace key renamed from my-org to litellm to match the name the proxy returns in marketplace.json
- setup tab now shows /plugin marketplace add command as primary option with settings.json as secondary
- usage tab now shows a hint to run /plugin marketplace update litellm when a plugin is not found
This commit is contained in:
ozolam 2026-07-16 14:12:28 +03:00
parent b0fac57fe4
commit 05c91aa5f2
3 changed files with 102 additions and 39 deletions

View file

@ -25,7 +25,7 @@ describe("buildMarketplaceSettingsSnippet", () => {
it("nests the url under a source object so Claude Code accepts the marketplace", () => {
expect(JSON.parse(buildMarketplaceSettingsSnippet("https://proxy.example.com"))).toEqual({
extraKnownMarketplaces: {
"my-org": {
litellm: {
source: {
source: "url",
url: "https://proxy.example.com/claude-code/marketplace.json",
@ -37,28 +37,12 @@ describe("buildMarketplaceSettingsSnippet", () => {
});
describe("formatInstallCommand", () => {
it("formats github source with repo", () => {
const source: PluginSource = { source: "github", repo: "org/repo" };
expect(formatInstallCommand({ name: "my-plugin", source })).toBe("/plugin marketplace add org/repo");
it("produces a /plugin install command scoped to the litellm marketplace", () => {
expect(formatInstallCommand({ name: "my-plugin" })).toBe("/plugin install my-plugin@litellm");
});
it("formats url source", () => {
const source: PluginSource = { source: "url", url: "https://example.com/plugin" };
expect(formatInstallCommand({ name: "my-plugin", source })).toBe(
"/plugin marketplace add https://example.com/plugin",
);
});
it("formats git-subdir source using its url", () => {
const source: PluginSource = { source: "git-subdir", url: "https://github.com/org/repo", path: "plugins/x" };
expect(formatInstallCommand({ name: "my-plugin", source })).toBe(
"/plugin marketplace add https://github.com/org/repo",
);
});
it("falls back to plugin name when no repo or url", () => {
const source: PluginSource = { source: "github" };
expect(formatInstallCommand({ name: "my-plugin", source })).toBe("/plugin marketplace add my-plugin");
it("uses the plugin name as the identifier", () => {
expect(formatInstallCommand({ name: "code-review" })).toBe("/plugin install code-review@litellm");
});
});

View file

@ -179,13 +179,14 @@ export const parseSkillSource = (rawUrl: string, subPath?: string): SkillSourceP
/**
* Build the `~/.claude/settings.json` snippet that registers the proxy as a marketplace.
* Claude Code expects `extraKnownMarketplaces.<name>.source` to be a source object, not a
* bare `"url"` string, so the url/source pair is nested one level deeper.
* bare `"url"` string, so the url/source pair is nested one level deeper. The key must be
* "litellm" to match the name the proxy returns in marketplace.json.
*/
export const buildMarketplaceSettingsSnippet = (proxyOrigin: string): string =>
JSON.stringify(
{
extraKnownMarketplaces: {
"my-org": {
litellm: {
source: {
source: "url",
url: `${proxyOrigin}/claude-code/marketplace.json`,
@ -198,20 +199,11 @@ export const buildMarketplaceSettingsSnippet = (proxyOrigin: string): string =>
);
/**
* Generate install command for Claude Code CLI
* Format: /plugin marketplace add org/repo OR /plugin marketplace add url
* Generate install command for Claude Code CLI.
* Installs the named plugin from the "litellm" marketplace registered in settings.json.
*/
export const formatInstallCommand = (plugin: { name: string; source: PluginSource }): string => {
const { source } = plugin;
if (source.source === "github" && source.repo) {
return `/plugin marketplace add ${source.repo}`;
}
if ((source.source === "url" || source.source === "git-subdir") && source.url) {
return `/plugin marketplace add ${source.url}`;
}
// Fallback to plugin name
return `/plugin marketplace add ${plugin.name}`;
};
export const formatInstallCommand = (plugin: { name: string }): string =>
`/plugin install ${plugin.name}@litellm`;
/**
* Extract unique categories from plugins list

View file

@ -261,6 +261,32 @@ const SkillDetail: React.FC<SkillDetailProps> = ({ skill, onBack }) => {
</pre>
</div>
{/* Shown when the marketplace catalog is stale and the plugin isn't found yet */}
<div
style={{
border: "1px solid #fce8b2",
borderRadius: 8,
padding: "12px 16px",
backgroundColor: "#fefce8",
marginBottom: 16,
}}
>
<p style={{ fontSize: 13, color: "#5f6368", lineHeight: 1.6, margin: "0 0 8px 0" }}>
If you see &quot;Plugin {skill.name} not found in marketplace&quot;, update the catalog first:
</p>
<pre
style={{
margin: 0,
fontSize: 13,
fontFamily: "monospace",
color: "#202124",
backgroundColor: "transparent",
}}
>
/plugin marketplace update litellm
</pre>
</div>
<p style={{ fontSize: 13, color: "#5f6368", lineHeight: 1.6, margin: 0 }}>
Don&apos;t have the marketplace configured yet?{" "}
<span onClick={() => setActiveTab("setup")} style={{ color: "#1a73e8", cursor: "pointer" }}>
@ -276,12 +302,73 @@ const SkillDetail: React.FC<SkillDetailProps> = ({ skill, onBack }) => {
<h2 style={{ fontSize: 18, fontWeight: 400, color: "#202124", margin: "0 0 8px 0" }}>
One-time marketplace setup
</h2>
<p style={{ fontSize: 14, color: "#5f6368", margin: "0 0 24px 0", lineHeight: 1.6 }}>
Add this to{" "}
{/* Option 1: single command — fastest path for most users */}
<p style={{ fontSize: 14, color: "#5f6368", margin: "0 0 12px 0", lineHeight: 1.6 }}>
Run this command in Claude Code to register the marketplace:
</p>
<div
style={{
border: "1px solid #dadce0",
borderRadius: 8,
overflow: "hidden",
marginBottom: 24,
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "10px 16px",
backgroundColor: "#f8f9fa",
borderBottom: "1px solid #dadce0",
}}
>
<span style={{ fontSize: 13, color: "#3c4043", fontWeight: 500 }}>Run in Claude Code</span>
<button
onClick={() => {
const origin = typeof window !== "undefined" ? window.location.origin : "";
copyToClipboard(`/plugin marketplace add ${origin}/claude-code/marketplace.json`, "marketplace-cmd");
}}
style={{
display: "flex",
alignItems: "center",
gap: 4,
fontSize: 12,
color: copiedKey === "marketplace-cmd" ? "#137333" : "#1a73e8",
background: "none",
border: "none",
cursor: "pointer",
padding: 0,
}}
>
{copiedKey === "marketplace-cmd" ? <CheckOutlined /> : <CopyOutlined />}
{copiedKey === "marketplace-cmd" ? "Copied" : "Copy"}
</button>
</div>
<pre
style={{
margin: 0,
padding: "14px 16px",
fontSize: 13,
fontFamily: "monospace",
color: "#202124",
backgroundColor: "#fff",
}}
>
{`/plugin marketplace add ${typeof window !== "undefined" ? window.location.origin : "<proxy-url>"}/claude-code/marketplace.json`}
</pre>
</div>
{/* Option 2: settings.json for persistent config or managed deployments.
extraKnownMarketplaces requires source to be a nested object, not a flat string. */}
<p style={{ fontSize: 14, color: "#5f6368", margin: "0 0 12px 0", lineHeight: 1.6 }}>
Or add this to{" "}
<code style={{ fontSize: 13, backgroundColor: "#f1f3f4", padding: "1px 6px", borderRadius: 4 }}>
~/.claude/settings.json
</code>{" "}
to point Claude Code at your proxy:
for a persistent configuration:
</p>
<div
style={{