merge
This commit is contained in:
commit
3adddfb9f1
13 changed files with 312 additions and 48 deletions
33
AGENTS.md
Normal file
33
AGENTS.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# MCP Inspector Development Guide
|
||||
|
||||
## Build Commands
|
||||
|
||||
- Build all: `npm run build`
|
||||
- Build client: `npm run build-client`
|
||||
- Build server: `npm run build-server`
|
||||
- Development mode: `npm run dev` (use `npm run dev:windows` on Windows)
|
||||
- Format code: `npm run prettier-fix`
|
||||
- Client lint: `cd client && npm run lint`
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
- Use TypeScript with proper type annotations
|
||||
- Follow React functional component patterns with hooks
|
||||
- Use ES modules (import/export) not CommonJS
|
||||
- Use Prettier for formatting (auto-formatted on commit)
|
||||
- Follow existing naming conventions:
|
||||
- camelCase for variables and functions
|
||||
- PascalCase for component names and types
|
||||
- kebab-case for file names
|
||||
- Use async/await for asynchronous operations
|
||||
- Implement proper error handling with try/catch blocks
|
||||
- Use Tailwind CSS for styling in the client
|
||||
- Keep components small and focused on a single responsibility
|
||||
|
||||
## Project Organization
|
||||
|
||||
The project is organized as a monorepo with workspaces:
|
||||
|
||||
- `client/`: React frontend with Vite, TypeScript and Tailwind
|
||||
- `server/`: Express backend with TypeScript
|
||||
- `cli/`: Command-line interface for testing and invoking MCP server methods directly
|
||||
34
CLAUDE.md
34
CLAUDE.md
|
|
@ -1,33 +1 @@
|
|||
# MCP Inspector Development Guide
|
||||
|
||||
## Build Commands
|
||||
|
||||
- Build all: `npm run build`
|
||||
- Build client: `npm run build-client`
|
||||
- Build server: `npm run build-server`
|
||||
- Development mode: `npm run dev` (use `npm run dev:windows` on Windows)
|
||||
- Format code: `npm run prettier-fix`
|
||||
- Client lint: `cd client && npm run lint`
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
- Use TypeScript with proper type annotations
|
||||
- Follow React functional component patterns with hooks
|
||||
- Use ES modules (import/export) not CommonJS
|
||||
- Use Prettier for formatting (auto-formatted on commit)
|
||||
- Follow existing naming conventions:
|
||||
- camelCase for variables and functions
|
||||
- PascalCase for component names and types
|
||||
- kebab-case for file names
|
||||
- Use async/await for asynchronous operations
|
||||
- Implement proper error handling with try/catch blocks
|
||||
- Use Tailwind CSS for styling in the client
|
||||
- Keep components small and focused on a single responsibility
|
||||
|
||||
## Project Organization
|
||||
|
||||
The project is organized as a monorepo with workspaces:
|
||||
|
||||
- `client/`: React frontend with Vite, TypeScript and Tailwind
|
||||
- `server/`: Express backend with TypeScript
|
||||
- `cli/`: Command-line interface for testing and invoking MCP server methods directly
|
||||
@./AGENTS.md
|
||||
|
|
|
|||
|
|
@ -141,6 +141,10 @@ const App = () => {
|
|||
return localStorage.getItem("lastOauthScope") || "";
|
||||
});
|
||||
|
||||
const [oauthClientSecret, setOauthClientSecret] = useState<string>(() => {
|
||||
return localStorage.getItem("lastOauthClientSecret") || "";
|
||||
});
|
||||
|
||||
// Custom headers state with migration from legacy auth
|
||||
const [customHeaders, setCustomHeaders] = useState<CustomHeaders>(() => {
|
||||
const savedHeaders = localStorage.getItem("lastCustomHeaders");
|
||||
|
|
@ -262,6 +266,7 @@ const App = () => {
|
|||
env,
|
||||
customHeaders,
|
||||
oauthClientId,
|
||||
oauthClientSecret,
|
||||
oauthScope,
|
||||
config,
|
||||
connectionType,
|
||||
|
|
@ -394,6 +399,10 @@ const App = () => {
|
|||
localStorage.setItem("lastOauthScope", oauthScope);
|
||||
}, [oauthScope]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem("lastOauthClientSecret", oauthClientSecret);
|
||||
}, [oauthClientSecret]);
|
||||
|
||||
useEffect(() => {
|
||||
saveInspectorConfig(CONFIG_LOCAL_STORAGE_KEY, config);
|
||||
}, [config]);
|
||||
|
|
@ -900,6 +909,8 @@ const App = () => {
|
|||
setCustomHeaders={setCustomHeaders}
|
||||
oauthClientId={oauthClientId}
|
||||
setOauthClientId={setOauthClientId}
|
||||
oauthClientSecret={oauthClientSecret}
|
||||
setOauthClientSecret={setOauthClientSecret}
|
||||
oauthScope={oauthScope}
|
||||
setOauthScope={setOauthScope}
|
||||
onConnect={connectMcpServer}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState, memo, useMemo, useCallback } from "react";
|
||||
import { useState, memo, useMemo, useCallback, useEffect } from "react";
|
||||
import type React from "react";
|
||||
import type { JsonValue } from "@/utils/jsonUtils";
|
||||
import clsx from "clsx";
|
||||
import { Copy, CheckCheck } from "lucide-react";
|
||||
|
|
@ -101,6 +102,7 @@ const JsonNode = memo(
|
|||
initialExpandDepth,
|
||||
isError = false,
|
||||
}: JsonNodeProps) => {
|
||||
const { toast } = useToast();
|
||||
const [isExpanded, setIsExpanded] = useState(depth < initialExpandDepth);
|
||||
const [typeStyleMap] = useState<Record<string, string>>({
|
||||
number: "text-blue-600",
|
||||
|
|
@ -113,6 +115,52 @@ const JsonNode = memo(
|
|||
});
|
||||
const dataType = getDataType(data);
|
||||
|
||||
const [copied, setCopied] = useState(false);
|
||||
useEffect(() => {
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
if (copied) {
|
||||
timeoutId = setTimeout(() => setCopied(false), 500);
|
||||
}
|
||||
return () => {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
};
|
||||
}, [copied]);
|
||||
|
||||
const handleCopyValue = useCallback(
|
||||
(value: JsonValue) => {
|
||||
try {
|
||||
let text: string;
|
||||
const valueType = getDataType(value);
|
||||
switch (valueType) {
|
||||
case "string":
|
||||
text = value as unknown as string;
|
||||
break;
|
||||
case "number":
|
||||
case "boolean":
|
||||
text = String(value);
|
||||
break;
|
||||
case "null":
|
||||
text = "null";
|
||||
break;
|
||||
case "undefined":
|
||||
text = "undefined";
|
||||
break;
|
||||
default:
|
||||
text = JSON.stringify(value);
|
||||
}
|
||||
navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: `There was an error coping result into the clipboard: ${error instanceof Error ? error.message : String(error)}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
},
|
||||
[toast],
|
||||
);
|
||||
|
||||
const renderCollapsible = (isArray: boolean) => {
|
||||
const items = isArray
|
||||
? (data as JsonValue[])
|
||||
|
|
@ -206,7 +254,7 @@ const JsonNode = memo(
|
|||
|
||||
if (!isTooLong) {
|
||||
return (
|
||||
<div className="flex mr-1 rounded hover:bg-gray-800/20">
|
||||
<div className="flex mr-1 rounded hover:bg-gray-800/20 group items-start">
|
||||
{name && (
|
||||
<span className="mr-1 text-gray-600 dark:text-gray-400">
|
||||
{name}:
|
||||
|
|
@ -220,12 +268,28 @@ const JsonNode = memo(
|
|||
>
|
||||
"{value}"
|
||||
</pre>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="ml-1 h-6 w-6 p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
handleCopyValue(value as unknown as JsonValue);
|
||||
}}
|
||||
aria-label={name ? `Copy value of ${name}` : "Copy value"}
|
||||
title={name ? `Copy value of ${name}` : "Copy value"}
|
||||
>
|
||||
{copied ? (
|
||||
<CheckCheck className="size-4 dark:text-green-700 text-green-600" />
|
||||
) : (
|
||||
<Copy className="size-4 text-foreground" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex mr-1 rounded group hover:bg-gray-800/20">
|
||||
<div className="flex mr-1 rounded group hover:bg-gray-800/20 items-start">
|
||||
{name && (
|
||||
<span className="mr-1 text-gray-600 dark:text-gray-400 dark:group-hover:text-gray-100 group-hover:text-gray-400">
|
||||
{name}:
|
||||
|
|
@ -241,6 +305,22 @@ const JsonNode = memo(
|
|||
>
|
||||
{isExpanded ? `"${value}"` : `"${value.slice(0, maxLength)}..."`}
|
||||
</pre>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="ml-1 h-6 w-6 p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
handleCopyValue(value as unknown as JsonValue);
|
||||
}}
|
||||
aria-label={name ? `Copy value of ${name}` : "Copy value"}
|
||||
title={name ? `Copy value of ${name}` : "Copy value"}
|
||||
>
|
||||
{copied ? (
|
||||
<CheckCheck className="size-4 dark:text-green-700 text-green-600" />
|
||||
) : (
|
||||
<Copy className="size-4 text-foreground" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -253,7 +333,7 @@ const JsonNode = memo(
|
|||
return renderString(data as string);
|
||||
default:
|
||||
return (
|
||||
<div className="flex items-center mr-1 rounded hover:bg-gray-800/20">
|
||||
<div className="flex items-center mr-1 rounded hover:bg-gray-800/20 group">
|
||||
{name && (
|
||||
<span className="mr-1 text-gray-600 dark:text-gray-400">
|
||||
{name}:
|
||||
|
|
@ -262,6 +342,22 @@ const JsonNode = memo(
|
|||
<span className={typeStyleMap[dataType] || typeStyleMap.default}>
|
||||
{data === null ? "null" : String(data)}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="ml-1 h-6 w-6 p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
handleCopyValue(data as JsonValue);
|
||||
}}
|
||||
aria-label={name ? `Copy value of ${name}` : "Copy value"}
|
||||
title={name ? `Copy value of ${name}` : "Copy value"}
|
||||
>
|
||||
{copied ? (
|
||||
<CheckCheck className="size-4 dark:text-green-700 text-green-600" />
|
||||
) : (
|
||||
<Copy className="size-4 text-foreground" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,7 +131,9 @@ const PromptsTab = ({
|
|||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
<AlertDescription className="break-all">
|
||||
{error}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : selectedPrompt ? (
|
||||
<div className="space-y-4">
|
||||
|
|
|
|||
|
|
@ -225,7 +225,9 @@ const ResourcesTab = ({
|
|||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
<AlertDescription className="break-all">
|
||||
{error}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : selectedResource ? (
|
||||
<JsonView
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ const SamplingRequest = ({
|
|||
},
|
||||
role: {
|
||||
type: "string",
|
||||
default: "endTurn",
|
||||
default: "assistant",
|
||||
description: "Role of the model",
|
||||
},
|
||||
content: {
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ interface SidebarProps {
|
|||
setCustomHeaders: (headers: CustomHeadersType) => void;
|
||||
oauthClientId: string;
|
||||
setOauthClientId: (id: string) => void;
|
||||
oauthClientSecret: string;
|
||||
setOauthClientSecret: (secret: string) => void;
|
||||
oauthScope: string;
|
||||
setOauthScope: (scope: string) => void;
|
||||
onConnect: () => void;
|
||||
|
|
@ -87,6 +89,8 @@ const Sidebar = ({
|
|||
setCustomHeaders,
|
||||
oauthClientId,
|
||||
setOauthClientId,
|
||||
oauthClientSecret,
|
||||
setOauthClientSecret,
|
||||
oauthScope,
|
||||
setOauthScope,
|
||||
onConnect,
|
||||
|
|
@ -104,6 +108,7 @@ const Sidebar = ({
|
|||
const [showAuthConfig, setShowAuthConfig] = useState(false);
|
||||
const [showConfig, setShowConfig] = useState(false);
|
||||
const [shownEnvVars, setShownEnvVars] = useState<Set<string>>(new Set());
|
||||
const [showClientSecret, setShowClientSecret] = useState(false);
|
||||
const [copiedServerEntry, setCopiedServerEntry] = useState(false);
|
||||
const [copiedServerFile, setCopiedServerFile] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
|
@ -555,6 +560,38 @@ const Sidebar = ({
|
|||
data-testid="oauth-client-id-input"
|
||||
className="font-mono"
|
||||
/>
|
||||
<label className="text-sm font-medium">
|
||||
Client Secret
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type={showClientSecret ? "text" : "password"}
|
||||
placeholder="Client Secret (optional)"
|
||||
onChange={(e) => setOauthClientSecret(e.target.value)}
|
||||
value={oauthClientSecret}
|
||||
data-testid="oauth-client-secret-input"
|
||||
className="font-mono"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-9 w-9 p-0 shrink-0"
|
||||
onClick={() => setShowClientSecret(!showClientSecret)}
|
||||
aria-label={
|
||||
showClientSecret ? "Hide secret" : "Show secret"
|
||||
}
|
||||
aria-pressed={showClientSecret}
|
||||
title={
|
||||
showClientSecret ? "Hide secret" : "Show secret"
|
||||
}
|
||||
>
|
||||
{showClientSecret ? (
|
||||
<Eye className="h-4 w-4" aria-hidden="true" />
|
||||
) : (
|
||||
<EyeOff className="h-4 w-4" aria-hidden="true" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<label className="text-sm font-medium">
|
||||
Redirect URL
|
||||
</label>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,13 @@ import { Input } from "@/components/ui/input";
|
|||
import { Label } from "@/components/ui/label";
|
||||
import { TabsContent } from "@/components/ui/tabs";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import DynamicJsonForm, { DynamicJsonFormRef } from "./DynamicJsonForm";
|
||||
import type { JsonValue, JsonSchemaType } from "@/utils/jsonUtils";
|
||||
import {
|
||||
|
|
@ -114,7 +121,7 @@ const ToolsTab = ({
|
|||
renderItem={(tool) => (
|
||||
<div className="flex flex-col items-start">
|
||||
<span className="flex-1">{tool.name}</span>
|
||||
<span className="text-sm text-gray-500 text-left">
|
||||
<span className="text-sm text-gray-500 text-left line-clamp-3">
|
||||
{tool.description}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -137,10 +144,12 @@ const ToolsTab = ({
|
|||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
<AlertDescription className="break-all">
|
||||
{error}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 whitespace-pre-wrap max-h-48 overflow-y-auto">
|
||||
{selectedTool.description}
|
||||
</p>
|
||||
{Object.entries(selectedTool.inputSchema.properties ?? []).map(
|
||||
|
|
@ -208,6 +217,42 @@ const ToolsTab = ({
|
|||
{prop.description || "Toggle this option"}
|
||||
</label>
|
||||
</div>
|
||||
) : prop.type === "string" && prop.enum ? (
|
||||
<Select
|
||||
value={
|
||||
params[key] === undefined
|
||||
? ""
|
||||
: String(params[key])
|
||||
}
|
||||
onValueChange={(value) => {
|
||||
if (value === "") {
|
||||
setParams({
|
||||
...params,
|
||||
[key]: undefined,
|
||||
});
|
||||
} else {
|
||||
setParams({
|
||||
...params,
|
||||
[key]: value,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id={key} className="mt-1">
|
||||
<SelectValue
|
||||
placeholder={
|
||||
prop.description || "Select an option"
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{prop.enum.map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{option}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : prop.type === "string" ? (
|
||||
<Textarea
|
||||
id={key}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ describe("Sidebar", () => {
|
|||
setSseUrl: jest.fn(),
|
||||
oauthClientId: "",
|
||||
setOauthClientId: jest.fn(),
|
||||
oauthClientSecret: "",
|
||||
setOauthClientSecret: jest.fn(),
|
||||
oauthScope: "",
|
||||
setOauthScope: jest.fn(),
|
||||
env: {},
|
||||
|
|
|
|||
|
|
@ -680,6 +680,64 @@ describe("ToolsTab", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("Enum Parameters", () => {
|
||||
const toolWithEnumParam: Tool = {
|
||||
name: "enumTool",
|
||||
description: "Tool with enum parameter",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
format: {
|
||||
type: "string" as const,
|
||||
enum: ["json", "xml", "csv", "yaml"],
|
||||
description: "Output format",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock scrollIntoView for Radix UI Select
|
||||
Element.prototype.scrollIntoView = jest.fn();
|
||||
});
|
||||
|
||||
it("should render enum parameter as dropdown", () => {
|
||||
renderToolsTab({
|
||||
tools: [toolWithEnumParam],
|
||||
selectedTool: toolWithEnumParam,
|
||||
});
|
||||
|
||||
// Should render a select button instead of textarea
|
||||
const selectTrigger = screen.getByRole("combobox", { name: /format/i });
|
||||
expect(selectTrigger).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render non-enum string parameter as textarea", () => {
|
||||
const toolWithStringParam: Tool = {
|
||||
name: "stringTool",
|
||||
description: "Tool with regular string parameter",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
text: {
|
||||
type: "string" as const,
|
||||
description: "Some text input",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderToolsTab({
|
||||
tools: [toolWithStringParam],
|
||||
selectedTool: toolWithStringParam,
|
||||
});
|
||||
|
||||
// Should render textarea, not select
|
||||
expect(screen.queryByRole("combobox")).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("textbox")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("JSON Validation Integration", () => {
|
||||
const toolWithJsonParams: Tool = {
|
||||
name: "jsonTool",
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ interface UseConnectionOptions {
|
|||
// Custom headers support
|
||||
customHeaders?: CustomHeaders;
|
||||
oauthClientId?: string;
|
||||
oauthClientSecret?: string;
|
||||
oauthScope?: string;
|
||||
config: InspectorConfig;
|
||||
connectionType?: "direct" | "proxy";
|
||||
|
|
@ -89,6 +90,7 @@ export function useConnection({
|
|||
env,
|
||||
customHeaders,
|
||||
oauthClientId,
|
||||
oauthClientSecret,
|
||||
oauthScope,
|
||||
config,
|
||||
connectionType = "proxy",
|
||||
|
|
@ -125,12 +127,20 @@ export function useConnection({
|
|||
return;
|
||||
}
|
||||
|
||||
const clientInformation: { client_id: string; client_secret?: string } = {
|
||||
client_id: oauthClientId,
|
||||
};
|
||||
|
||||
if (oauthClientSecret) {
|
||||
clientInformation.client_secret = oauthClientSecret;
|
||||
}
|
||||
|
||||
saveClientInformationToSessionStorage({
|
||||
serverUrl: sseUrl,
|
||||
clientInformation: { client_id: oauthClientId },
|
||||
clientInformation,
|
||||
isPreregistered: true,
|
||||
});
|
||||
}, [oauthClientId, sseUrl]);
|
||||
}, [oauthClientId, oauthClientSecret, sseUrl]);
|
||||
|
||||
const pushHistory = (request: object, response?: object) => {
|
||||
setRequestHistory((prev) => [
|
||||
|
|
|
|||
6
package-lock.json
generated
6
package-lock.json
generated
|
|
@ -11462,9 +11462,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
|
||||
"version": "6.3.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
|
||||
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
|
|||
Loading…
Reference in a new issue