mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
fix(ui): correct Claude Code skills install command and settings.json snippet
This commit is contained in:
parent
2f03789927
commit
60f73c9519
3 changed files with 68 additions and 58 deletions
|
|
@ -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 },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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 `@<marketplace>` 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 <name>@litellm
|
||||
*/
|
||||
export const formatInstallCommand = (plugin: { name: string }): string =>
|
||||
`/plugin install ${plugin.name}@${MARKETPLACE_NAME}`;
|
||||
|
||||
export interface MarketplaceSettings {
|
||||
extraKnownMarketplaces: Record<string, { source: { source: "url"; url: string } }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
|
|
|||
|
|
@ -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<SkillDetailProps> = ({ skill, onBack }) => {
|
|||
|
||||
const installCommand = formatInstallCommand(skill);
|
||||
|
||||
const marketplaceUrl = `${typeof window !== "undefined" ? window.location.origin : "<proxy-url>"}${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<SkillDetailProps> = ({ skill, onBack }) => {
|
|||
>
|
||||
<span style={{ fontSize: 13, color: "#3c4043", fontWeight: 500 }}>~/.claude/settings.json</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
const snippet = JSON.stringify(
|
||||
{
|
||||
extraKnownMarketplaces: {
|
||||
"my-org": {
|
||||
source: "url",
|
||||
url: `${typeof window !== "undefined" ? window.location.origin : ""}/claude-code/marketplace.json`,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
copyToClipboard(snippet, "settings");
|
||||
}}
|
||||
onClick={() => copyToClipboard(settingsSnippet, "settings")}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
|
|
@ -339,18 +328,7 @@ const SkillDetail: React.FC<SkillDetailProps> = ({ skill, onBack }) => {
|
|||
backgroundColor: "#fff",
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(
|
||||
{
|
||||
extraKnownMarketplaces: {
|
||||
"my-org": {
|
||||
source: "url",
|
||||
url: `${typeof window !== "undefined" ? window.location.origin : "<proxy-url>"}/claude-code/marketplace.json`,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}
|
||||
{settingsSnippet}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue