Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/anthropic_endpoints/claude_code_endpoints/claude_code_marketplace.py: 47%
247 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
1"""
2CLAUDE CODE MARKETPLACE
4Provides a registry/discovery layer for Claude Code plugins.
5Plugins are stored as metadata + source references in LiteLLM database.
6Actual plugin files are hosted on GitHub/GitLab/Bitbucket or as a zip archive on
7any HTTPS host (S3, Artifactory, a static file server).
9Endpoints:
10/claude-code/marketplace.json - GET - List plugins for Claude Code discovery (unauthenticated; `?key=` adds the key's granted skills)
11/claude-code/plugins - POST - Register a new plugin (create-only, proxy admin only)
12/claude-code/plugins - GET - List plugins visible to the key (enabled, plus granted disabled ones)
13/claude-code/plugins/{name} - GET - Get plugin details (403 on a disabled plugin the key is not granted)
14/claude-code/plugins/{name} - PUT - Update an existing plugin (proxy admin only)
15/claude-code/plugins/{name}/enable - POST - Enable a plugin (proxy admin only)
16/claude-code/plugins/{name}/disable - POST - Disable a plugin (proxy admin only)
17/claude-code/plugins/{name} - DELETE - Delete a plugin (proxy admin only)
18"""
20import json
21import re
22from collections.abc import Mapping, Sequence
23from datetime import datetime, timezone
24from typing import Annotated, Final, Protocol, TypedDict
25from urllib.parse import urlsplit
27from fastapi import APIRouter, Depends, HTTPException, Request
28from fastapi.responses import JSONResponse
30from litellm._logging import verbose_proxy_logger
31from litellm.proxy._types import CommonProxyErrors, ProxyException, UserAPIKeyAuth
32from litellm.proxy.anthropic_endpoints.claude_code_endpoints.claude_code_skill_access import (
33 SkillVisibility,
34 skill_visibility,
35)
36from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
37from litellm.proxy.common_utils.resource_ownership import is_proxy_admin
38from litellm.repositories.table_repositories import ClaudeCodePluginRepository
39from litellm.types.proxy.claude_code_endpoints import (
40 ListPluginsResponse,
41 PluginListItem,
42 PluginResponse,
43 PluginSpec,
44 RegisterPluginRequest,
45 RegisterPluginResponse,
46 UpdatePluginRequest,
47)
49router: Final = APIRouter()
52class _PluginRecord(Protocol):
53 id: str
54 name: str
55 version: str | None
56 description: str | None
57 manifest_json: str | None
58 enabled: bool
59 created_at: datetime | None
60 updated_at: datetime | None
61 created_by: str | None
64class _MarketplaceEntry(TypedDict, total=False):
65 name: str
66 source: object
67 version: str
68 description: str
69 author: object
70 homepage: object
71 keywords: object
72 category: object
75async def _get_prisma_client() -> object:
76 """Get the prisma client from proxy_server."""
77 from litellm.proxy.proxy_server import prisma_client
79 if prisma_client is None: 79 ↛ 80line 79 didn't jump to line 80 because the condition on line 79 was never true
80 raise HTTPException(
81 status_code=500,
82 detail={"error": CommonProxyErrors.db_not_connected_error.value},
83 )
84 return prisma_client
87@router.get(
88 "/claude-code/marketplace.json",
89 tags=["Claude Code Marketplace"],
90)
91async def get_marketplace(request: Request, key: str | None = None):
92 """
93 Serve marketplace.json for Claude Code plugin discovery.
95 This endpoint is accessed by Claude Code CLI when users run:
96 - claude plugin marketplace add <url>
97 - claude plugin install <name>@<marketplace>
99 Without `key` the catalog holds the enabled (public) plugins. With `?key=sk-...`
100 the key is authenticated and the catalog also holds the disabled plugins granted
101 to it through `object_permission.skills` on the key or its team.
103 Returns:
104 Marketplace catalog with list of available plugins and their git sources.
106 Example:
107 ```bash
108 claude plugin marketplace add http://localhost:4000/claude-code/marketplace.json
109 claude plugin marketplace add "http://localhost:4000/claude-code/marketplace.json?key=sk-..."
110 claude plugin install my-plugin@litellm
111 ```
112 """
113 try:
114 prisma_client: Final = await _get_prisma_client()
116 caller: Final[UserAPIKeyAuth | None] = (
117 await user_api_key_auth(request=request, api_key=f"Bearer {key}") if key else None
118 )
119 visibility: Final[SkillVisibility] = skill_visibility(caller)
120 plugins: Final[Sequence[_PluginRecord]] = await ClaudeCodePluginRepository(prisma_client).table.find_many(
121 where=visibility.where()
122 )
124 plugin_list: Final = []
125 for plugin in plugins: 125 ↛ 126line 125 didn't jump to line 126 because the loop on line 125 never started
126 if not visibility.allows(plugin):
127 continue
128 try:
129 manifest: Mapping[str, object] = json.loads(plugin.manifest_json or "{}")
130 except json.JSONDecodeError:
131 verbose_proxy_logger.warning("Plugin %s has invalid manifest JSON, skipping", plugin.name)
132 continue
134 # Source must be specified for URL-based marketplaces
135 if "source" not in manifest:
136 verbose_proxy_logger.warning("Plugin %s has no source field, skipping", plugin.name)
137 continue
139 entry: _MarketplaceEntry = {
140 "name": plugin.name,
141 "source": manifest["source"],
142 }
144 if plugin.version:
145 entry["version"] = plugin.version
146 if plugin.description:
147 entry["description"] = plugin.description
148 if "author" in manifest:
149 entry["author"] = manifest["author"]
150 if "homepage" in manifest:
151 entry["homepage"] = manifest["homepage"]
152 if "keywords" in manifest:
153 entry["keywords"] = manifest["keywords"]
154 if "category" in manifest:
155 entry["category"] = manifest["category"]
157 plugin_list.append(entry)
159 marketplace: Final = {
160 "name": "litellm",
161 "owner": {"name": "LiteLLM", "email": "support@litellm.ai"},
162 "plugins": plugin_list,
163 }
165 return JSONResponse(content=marketplace)
167 except (HTTPException, ProxyException):
168 raise
169 except Exception as e:
170 verbose_proxy_logger.exception("Error generating marketplace: %s", e)
171 raise HTTPException(
172 status_code=500,
173 detail={"error": f"Failed to generate marketplace: {e}"},
174 )
177# Allowlist for git-subdir paths: one or more segments separated by '/'.
178# Each segment must start with an alphanumeric character and contain only
179# alphanumeric characters, dots, hyphens, and underscores.
180# This implicitly blocks '..', leading '/', backslashes, and percent-encoded sequences.
181_VALID_GIT_SUBDIR_PATH_RE: Final = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9._-]*(/[a-zA-Z0-9][a-zA-Z0-9._-]*)*$")
182_VALID_SHA256_RE: Final = re.compile(r"^[0-9a-fA-F]{64}$")
185def _is_https_url_with_host(url: str) -> bool:
186 try:
187 parts: Final = urlsplit(url)
188 except ValueError:
189 return False
190 return parts.scheme == "https" and bool(parts.hostname)
193def _validate_plugin_source(source: Mapping[str, str]) -> None:
194 """Validate plugin source format, raising HTTPException on invalid input."""
195 source_type: Final = source.get("source")
196 if source_type == "github": 196 ↛ 197line 196 didn't jump to line 197 because the condition on line 196 was never true
197 if "repo" not in source:
198 raise HTTPException(
199 status_code=400,
200 detail={"error": "GitHub source must include 'repo' field (e.g., 'org/repo')"},
201 )
202 elif source_type == "url": 202 ↛ 203line 202 didn't jump to line 203 because the condition on line 202 was never true
203 if "url" not in source:
204 raise HTTPException(
205 status_code=400,
206 detail={"error": "URL source must include 'url' field (e.g., 'https://github.com/org/repo.git')"},
207 )
208 elif source_type == "git-subdir": 208 ↛ 209line 208 didn't jump to line 209 because the condition on line 208 was never true
209 if not source.get("url"):
210 raise HTTPException(
211 status_code=400,
212 detail={
213 "error": "git-subdir source must include 'url' field (e.g., 'https://github.com/org/repo.git')"
214 },
215 )
216 if not source.get("path"):
217 raise HTTPException(
218 status_code=400,
219 detail={"error": "git-subdir source must include 'path' field (e.g., 'plugins/plugin-name')"},
220 )
221 if not _VALID_GIT_SUBDIR_PATH_RE.match(source["path"]):
222 raise HTTPException(
223 status_code=400,
224 detail={
225 "error": "git-subdir 'path' must be a relative path of the form 'segment/segment' (alphanumeric, dots, hyphens, underscores only)"
226 },
227 )
228 elif source_type == "archive": 228 ↛ 229line 228 didn't jump to line 229 because the condition on line 228 was never true
229 if not _is_https_url_with_host(source.get("url", "")):
230 raise HTTPException(
231 status_code=400,
232 detail={
233 "error": "archive source must include an https 'url' field "
234 "(e.g., 'https://bucket.s3.amazonaws.com/plugins/plugin-name.zip')"
235 },
236 )
237 if "sha256" in source and not _VALID_SHA256_RE.match(source["sha256"]):
238 raise HTTPException(
239 status_code=400,
240 detail={"error": "archive 'sha256' must be a 64-character hex digest"},
241 )
242 else:
243 raise HTTPException(
244 status_code=400,
245 detail={"error": "source.source must be 'github', 'url', 'git-subdir', or 'archive'"},
246 )
249def _build_plugin_manifest(name: str, spec: PluginSpec) -> Mapping[str, object]:
250 """Build the stored manifest dict shared by plugin create and update."""
251 dumped: Final[Mapping[str, object]] = spec.model_dump(exclude_none=True)
252 return {"name": name, **{key: value for key, value in dumped.items() if value and key != "name"}}
255def _error_response(status_code: int, message: str) -> HTTPException:
256 return HTTPException(status_code=status_code, detail={"error": message})
259def _name_conflict_error(name: str) -> HTTPException:
260 return _error_response(
261 409, f"A skill named '{name}' already exists. Update the existing skill instead of adding it again."
262 )
265def _require_proxy_admin(user_api_key_dict: UserAPIKeyAuth) -> None:
266 """Catalog mutations are restricted to proxy admins: marketplace.json is served
267 unauthenticated and any registered/updated entry is immediately installable by
268 every user, so a non-admin key must never be able to add or overwrite one.
269 """
270 if not is_proxy_admin(user_api_key_dict): 270 ↛ 271line 270 didn't jump to line 271 because the condition on line 270 was never true
271 raise HTTPException(
272 status_code=403,
273 detail={"error": "Only proxy admins may modify the Claude Code plugin marketplace."},
274 )
277@router.post(
278 "/claude-code/plugins",
279 tags=["Claude Code Marketplace"],
280 dependencies=[Depends(user_api_key_auth)],
281 response_model=RegisterPluginResponse,
282)
283async def register_plugin(
284 request: RegisterPluginRequest,
285 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
286):
287 """
288 Register a new plugin in the LiteLLM marketplace.
290 LiteLLM acts as a registry/discovery layer. Plugins are hosted on
291 GitHub/GitLab/Bitbucket or as a zip archive on any https host (e.g. S3).
292 Claude Code clones the git source or downloads the archive when users install.
294 This endpoint is create-only and never overwrites. If a plugin with
295 the same name already exists it returns 409 Conflict; use
296 PUT /claude-code/plugins/{plugin_name} to update an existing plugin.
298 Requires a proxy admin API key.
300 Parameters:
301 - name: Plugin name (kebab-case)
302 - source: Plugin source reference (github, url, git-subdir, or archive format)
303 - version: Semantic version (optional)
304 - description: Plugin description (optional)
305 - author: Author information (optional)
306 - homepage: Plugin homepage URL (optional)
307 - keywords: Search keywords (optional)
308 - category: Plugin category (optional)
310 Returns:
311 Registration status (action is always "created") and plugin information.
313 Example:
314 ```bash
315 curl -X POST http://localhost:4000/claude-code/plugins \\
316 -H "Authorization: Bearer sk-..." \\
317 -H "Content-Type: application/json" \\
318 -d '{
319 "name": "my-plugin",
320 "source": {"source": "github", "repo": "org/my-plugin"},
321 "version": "1.0.0",
322 "description": "My awesome plugin"
323 }'
324 ```
325 """
326 from prisma.errors import UniqueViolationError
328 try:
329 _require_proxy_admin(user_api_key_dict)
331 prisma_client: Final = await _get_prisma_client()
333 if not re.match(r"^[a-z0-9-]+$", request.name): 333 ↛ 334line 333 didn't jump to line 334 because the condition on line 333 was never true
334 raise HTTPException(
335 status_code=400,
336 detail={"error": "Plugin name must be kebab-case (lowercase letters, numbers, hyphens)"},
337 )
339 _validate_plugin_source(request.source)
341 existing: Final[_PluginRecord | None] = await ClaudeCodePluginRepository(prisma_client).table.find_unique(
342 where={"name": request.name}
343 )
344 if existing:
345 raise _name_conflict_error(request.name)
347 manifest: Final[Mapping[str, object]] = _build_plugin_manifest(request.name, request)
349 try:
350 plugin: Final[_PluginRecord] = await ClaudeCodePluginRepository(prisma_client).table.create(
351 data={
352 "name": request.name,
353 "version": request.version,
354 "description": request.description,
355 "manifest_json": json.dumps(manifest),
356 "files_json": "{}",
357 "enabled": True,
358 "created_at": datetime.now(timezone.utc),
359 "updated_at": datetime.now(timezone.utc),
360 "created_by": user_api_key_dict.user_id,
361 }
362 )
363 except UniqueViolationError:
364 raise _name_conflict_error(request.name)
366 verbose_proxy_logger.info("Plugin %s created successfully", request.name)
368 return RegisterPluginResponse(
369 status="success",
370 action="created",
371 plugin=PluginResponse(
372 id=plugin.id,
373 name=plugin.name,
374 version=plugin.version,
375 description=plugin.description,
376 source=request.source,
377 enabled=plugin.enabled,
378 ),
379 )
381 except HTTPException:
382 raise
383 except Exception as e:
384 verbose_proxy_logger.exception("Error registering plugin: %s", e)
385 raise HTTPException(
386 status_code=500,
387 detail={"error": f"Registration failed: {e}"},
388 )
391@router.get(
392 "/claude-code/plugins",
393 tags=["Claude Code Marketplace"],
394 dependencies=[Depends(user_api_key_auth)],
395 response_model=ListPluginsResponse,
396)
397async def list_plugins(
398 enabled_only: bool = False,
399 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
400):
401 """
402 List all plugins in the marketplace.
404 Parameters:
405 - enabled_only: If true, only return enabled plugins
407 Returns:
408 List of plugins with their metadata.
409 """
410 try:
411 prisma_client: Final = await _get_prisma_client()
413 visibility: Final[SkillVisibility] = skill_visibility(user_api_key_dict)
414 plugins: Final[Sequence[_PluginRecord]] = await ClaudeCodePluginRepository(prisma_client).table.find_many(
415 where={"enabled": True} if enabled_only else visibility.where()
416 )
418 plugin_list: Final = []
419 for p in plugins: 419 ↛ 420line 419 didn't jump to line 420 because the loop on line 419 never started
420 if not visibility.allows(p):
421 continue
422 # Parse manifest to get additional fields
423 manifest = json.loads(p.manifest_json) if p.manifest_json else {}
425 plugin_list.append(
426 PluginListItem(
427 id=p.id,
428 name=p.name,
429 version=p.version,
430 description=p.description,
431 source=manifest.get("source", {}),
432 author=manifest.get("author"),
433 homepage=manifest.get("homepage"),
434 keywords=manifest.get("keywords"),
435 category=manifest.get("category"),
436 domain=manifest.get("domain"),
437 namespace=manifest.get("namespace"),
438 enabled=p.enabled,
439 created_at=p.created_at.isoformat() if p.created_at else None,
440 updated_at=p.updated_at.isoformat() if p.updated_at else None,
441 )
442 )
444 # Sort by created_at descending (newest first)
445 plugin_list.sort(key=lambda x: x.created_at or "", reverse=True)
447 return ListPluginsResponse(
448 plugins=plugin_list,
449 count=len(plugin_list),
450 )
452 except HTTPException:
453 raise
454 except Exception as e:
455 verbose_proxy_logger.exception("Error listing plugins: %s", e)
456 raise HTTPException(
457 status_code=500,
458 detail={"error": str(e)},
459 )
462@router.get(
463 "/claude-code/plugins/{plugin_name}",
464 tags=["Claude Code Marketplace"],
465 dependencies=[Depends(user_api_key_auth)],
466)
467async def get_plugin(
468 plugin_name: str,
469 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
470):
471 """
472 Get details of a specific plugin.
474 Parameters:
475 - plugin_name: The name of the plugin
477 Returns:
478 Plugin details including source and metadata.
479 """
480 try:
481 prisma_client: Final = await _get_prisma_client()
483 plugin: Final[_PluginRecord | None] = await ClaudeCodePluginRepository(prisma_client).table.find_unique(
484 where={"name": plugin_name}
485 )
487 if not plugin: 487 ↛ 493line 487 didn't jump to line 493 because the condition on line 487 was always true
488 raise HTTPException(
489 status_code=404,
490 detail={"error": f"Plugin '{plugin_name}' not found"},
491 )
493 if not skill_visibility(user_api_key_dict).allows(plugin):
494 raise HTTPException(
495 status_code=403,
496 detail={"error": f"Plugin '{plugin_name}' is not granted to this key"},
497 )
499 manifest: Final[Mapping[str, object]] = json.loads(plugin.manifest_json or "{}") if plugin.manifest_json else {}
501 return {
502 "id": plugin.id,
503 "name": plugin.name,
504 "version": plugin.version,
505 "description": plugin.description,
506 "source": manifest.get("source"),
507 "author": manifest.get("author"),
508 "homepage": manifest.get("homepage"),
509 "keywords": manifest.get("keywords"),
510 "category": manifest.get("category"),
511 "enabled": plugin.enabled,
512 "created_at": plugin.created_at.isoformat() if plugin.created_at else None,
513 "updated_at": plugin.updated_at.isoformat() if plugin.updated_at else None,
514 "created_by": plugin.created_by,
515 }
517 except HTTPException:
518 raise
519 except Exception as e:
520 verbose_proxy_logger.exception("Error getting plugin: %s", e)
521 raise HTTPException(
522 status_code=500,
523 detail={"error": str(e)},
524 )
527@router.put(
528 "/claude-code/plugins/{plugin_name}",
529 tags=["Claude Code Marketplace"],
530 dependencies=[Depends(user_api_key_auth)],
531 response_model=RegisterPluginResponse,
532)
533async def update_plugin(
534 plugin_name: str,
535 request: UpdatePluginRequest,
536 user_api_key_dict: Annotated[UserAPIKeyAuth, Depends(user_api_key_auth)],
537):
538 """
539 Update an existing plugin in the LiteLLM marketplace.
541 The plugin is identified by its name in the path, which is the resource
542 identity and cannot be changed here. This is a full replace, not a merge:
543 the manifest is rebuilt from the request body, so any optional field left
544 out is reset to its default (e.g. an omitted version is cleared, not kept).
545 Send the full desired state.
547 Returns 404 if no plugin with the given name exists; use
548 POST /claude-code/plugins to create a new plugin.
550 Requires a proxy admin API key.
552 Parameters:
553 - plugin_name: Name of the plugin to update (path parameter)
554 - source: Plugin source reference (github, url, git-subdir, or archive format)
555 - version: Semantic version (optional)
556 - description: Plugin description (optional)
557 - author: Author information (optional)
558 - homepage: Plugin homepage URL (optional)
559 - keywords: Search keywords (optional)
560 - category: Plugin category (optional)
562 Returns:
563 Update status (action is always "updated") and plugin information.
565 Example:
566 ```bash
567 curl -X PUT http://localhost:4000/claude-code/plugins/my-plugin \\
568 -H "Authorization: Bearer sk-..." \\
569 -H "Content-Type: application/json" \\
570 -d '{
571 "source": {"source": "github", "repo": "org/my-plugin"},
572 "version": "2.0.0",
573 "description": "My awesome plugin"
574 }'
575 ```
576 """
577 from prisma.errors import PrismaError
579 try:
580 _require_proxy_admin(user_api_key_dict)
582 prisma_client: Final = await _get_prisma_client()
584 _validate_plugin_source(request.source)
586 existing: Final[_PluginRecord | None] = await ClaudeCodePluginRepository(prisma_client).table.find_unique(
587 where={"name": plugin_name} # mutable-ok: prisma query arguments must be plain dicts
588 )
589 if not existing:
590 raise _error_response(404, f"Plugin '{plugin_name}' not found")
592 manifest: Final[Mapping[str, object]] = _build_plugin_manifest(plugin_name, request)
594 plugin: Final[_PluginRecord | None] = await ClaudeCodePluginRepository(prisma_client).table.update(
595 where={"name": plugin_name}, # mutable-ok: prisma query arguments must be plain dicts
596 data={ # mutable-ok: prisma query arguments must be plain dicts
597 "version": request.version,
598 "description": request.description,
599 "manifest_json": json.dumps(manifest),
600 "files_json": "{}",
601 "updated_at": datetime.now(timezone.utc),
602 },
603 )
604 if plugin is None:
605 raise _error_response(404, f"Plugin '{plugin_name}' not found")
607 verbose_proxy_logger.info("Plugin %s updated successfully", plugin_name)
609 return RegisterPluginResponse(
610 status="success",
611 action="updated",
612 plugin=PluginResponse(
613 id=plugin.id,
614 name=plugin.name,
615 version=plugin.version,
616 description=plugin.description,
617 source=request.source,
618 enabled=plugin.enabled,
619 ),
620 )
622 except HTTPException:
623 raise
624 except PrismaError as e:
625 verbose_proxy_logger.exception("Error updating plugin: %s", e)
626 raise _error_response(500, f"Update failed: {e}")
629@router.post(
630 "/claude-code/plugins/{plugin_name}/enable",
631 tags=["Claude Code Marketplace"],
632 dependencies=[Depends(user_api_key_auth)],
633)
634async def enable_plugin(
635 plugin_name: str,
636 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
637):
638 """
639 Enable a disabled plugin.
641 Requires a proxy admin API key.
643 Parameters:
644 - plugin_name: The name of the plugin to enable
645 """
646 try:
647 _require_proxy_admin(user_api_key_dict)
649 prisma_client: Final = await _get_prisma_client()
651 plugin: Final[_PluginRecord | None] = await ClaudeCodePluginRepository(prisma_client).table.find_unique(
652 where={"name": plugin_name}
653 )
654 if not plugin: 654 ↛ 660line 654 didn't jump to line 660 because the condition on line 654 was always true
655 raise HTTPException(
656 status_code=404,
657 detail={"error": f"Plugin '{plugin_name}' not found"},
658 )
660 await ClaudeCodePluginRepository(prisma_client).table.update(
661 where={"name": plugin_name},
662 data={"enabled": True, "updated_at": datetime.now(timezone.utc)},
663 )
665 verbose_proxy_logger.info("Plugin %s enabled", plugin_name)
666 return {"status": "success", "message": f"Plugin '{plugin_name}' enabled"}
668 except HTTPException:
669 raise
670 except Exception as e:
671 verbose_proxy_logger.exception("Error enabling plugin: %s", e)
672 raise HTTPException(
673 status_code=500,
674 detail={"error": str(e)},
675 )
678@router.post(
679 "/claude-code/plugins/{plugin_name}/disable",
680 tags=["Claude Code Marketplace"],
681 dependencies=[Depends(user_api_key_auth)],
682)
683async def disable_plugin(
684 plugin_name: str,
685 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
686):
687 """
688 Disable a plugin without deleting it.
690 Requires a proxy admin API key.
692 Parameters:
693 - plugin_name: The name of the plugin to disable
694 """
695 try:
696 _require_proxy_admin(user_api_key_dict)
698 prisma_client: Final = await _get_prisma_client()
700 plugin: Final[_PluginRecord | None] = await ClaudeCodePluginRepository(prisma_client).table.find_unique(
701 where={"name": plugin_name}
702 )
703 if not plugin: 703 ↛ 709line 703 didn't jump to line 709 because the condition on line 703 was always true
704 raise HTTPException(
705 status_code=404,
706 detail={"error": f"Plugin '{plugin_name}' not found"},
707 )
709 await ClaudeCodePluginRepository(prisma_client).table.update(
710 where={"name": plugin_name},
711 data={"enabled": False, "updated_at": datetime.now(timezone.utc)},
712 )
714 verbose_proxy_logger.info("Plugin %s disabled", plugin_name)
715 return {"status": "success", "message": f"Plugin '{plugin_name}' disabled"}
717 except HTTPException:
718 raise
719 except Exception as e:
720 verbose_proxy_logger.exception("Error disabling plugin: %s", e)
721 raise HTTPException(
722 status_code=500,
723 detail={"error": str(e)},
724 )
727@router.delete(
728 "/claude-code/plugins/{plugin_name}",
729 tags=["Claude Code Marketplace"],
730 dependencies=[Depends(user_api_key_auth)],
731)
732async def delete_plugin(
733 plugin_name: str,
734 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
735):
736 """
737 Delete a plugin from the marketplace.
739 Requires a proxy admin API key.
741 Parameters:
742 - plugin_name: The name of the plugin to delete
743 """
744 try:
745 _require_proxy_admin(user_api_key_dict)
747 prisma_client: Final = await _get_prisma_client()
749 plugin: Final[_PluginRecord | None] = await ClaudeCodePluginRepository(prisma_client).table.find_unique(
750 where={"name": plugin_name}
751 )
752 if not plugin: 752 ↛ 758line 752 didn't jump to line 758 because the condition on line 752 was always true
753 raise HTTPException(
754 status_code=404,
755 detail={"error": f"Plugin '{plugin_name}' not found"},
756 )
758 await ClaudeCodePluginRepository(prisma_client).table.delete(where={"name": plugin_name})
760 verbose_proxy_logger.info("Plugin %s deleted", plugin_name)
761 return {"status": "success", "message": f"Plugin '{plugin_name}' deleted"}
763 except HTTPException:
764 raise
765 except Exception as e:
766 verbose_proxy_logger.exception("Error deleting plugin: %s", e)
767 raise HTTPException(
768 status_code=500,
769 detail={"error": str(e)},
770 )