From 60f73c95191da5fe94e65ca3edb1689143a6ff80 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:07:21 +0000 Subject: [PATCH] fix(ui): correct Claude Code skills install command and settings.json snippet --- .../claude_code_plugins/helpers.test.ts | 47 ++++++++++++------- .../components/claude_code_plugins/helpers.ts | 45 +++++++++++++----- .../claude_code_plugins/skill_detail.tsx | 34 +++----------- 3 files changed, 68 insertions(+), 58 deletions(-) diff --git a/ui/litellm-dashboard/src/components/claude_code_plugins/helpers.test.ts b/ui/litellm-dashboard/src/components/claude_code_plugins/helpers.test.ts index 4c84db2a97d..ef1072c5a7f 100644 --- a/ui/litellm-dashboard/src/components/claude_code_plugins/helpers.test.ts +++ b/ui/litellm-dashboard/src/components/claude_code_plugins/helpers.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it } from "vitest"; import { formatInstallCommand, + buildMarketplaceSettings, + MARKETPLACE_NAME, extractCategories, validatePluginName, getSourceDisplayText, @@ -18,31 +20,42 @@ import { parseSkillSource, isValidSubPath, } from "./helpers"; -import { MarketplacePluginEntry, PluginSource } from "./types"; +import { MarketplacePluginEntry } from "./types"; 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("installs the plugin from the litellm marketplace by name", () => { + 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("uses the marketplace name the proxy publishes, not the source", () => { + expect(formatInstallCommand({ name: "code-formatter" })).toBe(`/plugin install code-formatter@${MARKETPLACE_NAME}`); + expect(formatInstallCommand({ name: "code-formatter" })).not.toContain("marketplace add"); + }); +}); + +describe("buildMarketplaceSettings", () => { + const url = "https://proxy.example.com/claude-code/marketplace.json"; + + it("keys the marketplace under the name the proxy returns", () => { + const settings = buildMarketplaceSettings(url); + expect(Object.keys(settings.extraKnownMarketplaces)).toEqual([MARKETPLACE_NAME]); + expect(settings.extraKnownMarketplaces).not.toHaveProperty("my-org"); }); - 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("nests source as an object with a url, not a flat string", () => { + const settings = buildMarketplaceSettings(url); + expect(settings.extraKnownMarketplaces[MARKETPLACE_NAME].source).toEqual({ source: "url", url }); + expect(typeof settings.extraKnownMarketplaces[MARKETPLACE_NAME].source).toBe("object"); }); - 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("produces a snippet Claude Code accepts", () => { + expect(buildMarketplaceSettings(url)).toEqual({ + extraKnownMarketplaces: { + litellm: { + source: { source: "url", url }, + }, + }, + }); }); }); diff --git a/ui/litellm-dashboard/src/components/claude_code_plugins/helpers.ts b/ui/litellm-dashboard/src/components/claude_code_plugins/helpers.ts index cab3c5cba3c..5cf131681db 100644 --- a/ui/litellm-dashboard/src/components/claude_code_plugins/helpers.ts +++ b/ui/litellm-dashboard/src/components/claude_code_plugins/helpers.ts @@ -177,20 +177,39 @@ export const parseSkillSource = (rawUrl: string, subPath?: string): SkillSourceP }; /** - * Generate install command for Claude Code CLI - * Format: /plugin marketplace add org/repo OR /plugin marketplace add url + * Marketplace name the proxy publishes in marketplace.json. Claude Code keys its + * `extraKnownMarketplaces` config and `@` install suffix off this value. */ -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 MARKETPLACE_NAME = "litellm"; + +/** + * Path the proxy serves the Claude Code marketplace manifest from + */ +export const MARKETPLACE_MANIFEST_PATH = "/claude-code/marketplace.json"; + +/** + * Generate the Claude Code install command for a registered skill + * Format: /plugin install @litellm + */ +export const formatInstallCommand = (plugin: { name: string }): string => + `/plugin install ${plugin.name}@${MARKETPLACE_NAME}`; + +export interface MarketplaceSettings { + extraKnownMarketplaces: Record; +} + +/** + * Build the `~/.claude/settings.json` snippet that registers the proxy as a Claude Code + * marketplace. Claude Code requires `source` to be a nested object, and the marketplace key + * must match the name the proxy returns in marketplace.json. + */ +export const buildMarketplaceSettings = (marketplaceUrl: string): MarketplaceSettings => ({ + extraKnownMarketplaces: { + [MARKETPLACE_NAME]: { + source: { source: "url", url: marketplaceUrl }, + }, + }, +}); /** * Extract unique categories from plugins list diff --git a/ui/litellm-dashboard/src/components/claude_code_plugins/skill_detail.tsx b/ui/litellm-dashboard/src/components/claude_code_plugins/skill_detail.tsx index 8b7537c1988..4fe0d3fa9ae 100644 --- a/ui/litellm-dashboard/src/components/claude_code_plugins/skill_detail.tsx +++ b/ui/litellm-dashboard/src/components/claude_code_plugins/skill_detail.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { ArrowLeftOutlined, CopyOutlined, CheckOutlined, LinkOutlined } from "@ant-design/icons"; -import { formatInstallCommand } from "./helpers"; +import { formatInstallCommand, buildMarketplaceSettings, MARKETPLACE_MANIFEST_PATH } from "./helpers"; import { Plugin } from "./types"; interface SkillDetailProps { @@ -31,6 +31,9 @@ const SkillDetail: React.FC = ({ skill, onBack }) => { const installCommand = formatInstallCommand(skill); + const marketplaceUrl = `${typeof window !== "undefined" ? window.location.origin : ""}${MARKETPLACE_MANIFEST_PATH}`; + const settingsSnippet = JSON.stringify(buildMarketplaceSettings(marketplaceUrl), null, 2); + const detailRows = [ ...(skill.category ? [{ property: "Category", value: skill.category }] : []), ...(skill.domain ? [{ property: "Domain", value: skill.domain }] : []), @@ -298,21 +301,7 @@ const SkillDetail: React.FC = ({ skill, onBack }) => { > ~/.claude/settings.json