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

1""" 

2CLAUDE CODE MARKETPLACE 

3 

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). 

8 

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""" 

19 

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 

26 

27from fastapi import APIRouter, Depends, HTTPException, Request 

28from fastapi.responses import JSONResponse 

29 

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) 

48 

49router: Final = APIRouter() 

50 

51 

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 

62 

63 

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 

73 

74 

75async def _get_prisma_client() -> object: 

76 """Get the prisma client from proxy_server.""" 

77 from litellm.proxy.proxy_server import prisma_client 

78 

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 

85 

86 

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. 

94 

95 This endpoint is accessed by Claude Code CLI when users run: 

96 - claude plugin marketplace add <url> 

97 - claude plugin install <name>@<marketplace> 

98 

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. 

102 

103 Returns: 

104 Marketplace catalog with list of available plugins and their git sources. 

105 

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() 

115 

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 ) 

123 

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 

133 

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 

138 

139 entry: _MarketplaceEntry = { 

140 "name": plugin.name, 

141 "source": manifest["source"], 

142 } 

143 

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"] 

156 

157 plugin_list.append(entry) 

158 

159 marketplace: Final = { 

160 "name": "litellm", 

161 "owner": {"name": "LiteLLM", "email": "support@litellm.ai"}, 

162 "plugins": plugin_list, 

163 } 

164 

165 return JSONResponse(content=marketplace) 

166 

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 ) 

175 

176 

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}$") 

183 

184 

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) 

191 

192 

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 ) 

247 

248 

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"}} 

253 

254 

255def _error_response(status_code: int, message: str) -> HTTPException: 

256 return HTTPException(status_code=status_code, detail={"error": message}) 

257 

258 

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 ) 

263 

264 

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 ) 

275 

276 

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. 

289 

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. 

293 

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. 

297 

298 Requires a proxy admin API key. 

299 

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) 

309 

310 Returns: 

311 Registration status (action is always "created") and plugin information. 

312 

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 

327 

328 try: 

329 _require_proxy_admin(user_api_key_dict) 

330 

331 prisma_client: Final = await _get_prisma_client() 

332 

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 ) 

338 

339 _validate_plugin_source(request.source) 

340 

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) 

346 

347 manifest: Final[Mapping[str, object]] = _build_plugin_manifest(request.name, request) 

348 

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) 

365 

366 verbose_proxy_logger.info("Plugin %s created successfully", request.name) 

367 

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 ) 

380 

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 ) 

389 

390 

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. 

403 

404 Parameters: 

405 - enabled_only: If true, only return enabled plugins 

406 

407 Returns: 

408 List of plugins with their metadata. 

409 """ 

410 try: 

411 prisma_client: Final = await _get_prisma_client() 

412 

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 ) 

417 

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 {} 

424 

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 ) 

443 

444 # Sort by created_at descending (newest first) 

445 plugin_list.sort(key=lambda x: x.created_at or "", reverse=True) 

446 

447 return ListPluginsResponse( 

448 plugins=plugin_list, 

449 count=len(plugin_list), 

450 ) 

451 

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 ) 

460 

461 

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. 

473 

474 Parameters: 

475 - plugin_name: The name of the plugin 

476 

477 Returns: 

478 Plugin details including source and metadata. 

479 """ 

480 try: 

481 prisma_client: Final = await _get_prisma_client() 

482 

483 plugin: Final[_PluginRecord | None] = await ClaudeCodePluginRepository(prisma_client).table.find_unique( 

484 where={"name": plugin_name} 

485 ) 

486 

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 ) 

492 

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 ) 

498 

499 manifest: Final[Mapping[str, object]] = json.loads(plugin.manifest_json or "{}") if plugin.manifest_json else {} 

500 

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 } 

516 

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 ) 

525 

526 

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. 

540 

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. 

546 

547 Returns 404 if no plugin with the given name exists; use 

548 POST /claude-code/plugins to create a new plugin. 

549 

550 Requires a proxy admin API key. 

551 

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) 

561 

562 Returns: 

563 Update status (action is always "updated") and plugin information. 

564 

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 

578 

579 try: 

580 _require_proxy_admin(user_api_key_dict) 

581 

582 prisma_client: Final = await _get_prisma_client() 

583 

584 _validate_plugin_source(request.source) 

585 

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") 

591 

592 manifest: Final[Mapping[str, object]] = _build_plugin_manifest(plugin_name, request) 

593 

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") 

606 

607 verbose_proxy_logger.info("Plugin %s updated successfully", plugin_name) 

608 

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 ) 

621 

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}") 

627 

628 

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. 

640 

641 Requires a proxy admin API key. 

642 

643 Parameters: 

644 - plugin_name: The name of the plugin to enable 

645 """ 

646 try: 

647 _require_proxy_admin(user_api_key_dict) 

648 

649 prisma_client: Final = await _get_prisma_client() 

650 

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 ) 

659 

660 await ClaudeCodePluginRepository(prisma_client).table.update( 

661 where={"name": plugin_name}, 

662 data={"enabled": True, "updated_at": datetime.now(timezone.utc)}, 

663 ) 

664 

665 verbose_proxy_logger.info("Plugin %s enabled", plugin_name) 

666 return {"status": "success", "message": f"Plugin '{plugin_name}' enabled"} 

667 

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 ) 

676 

677 

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. 

689 

690 Requires a proxy admin API key. 

691 

692 Parameters: 

693 - plugin_name: The name of the plugin to disable 

694 """ 

695 try: 

696 _require_proxy_admin(user_api_key_dict) 

697 

698 prisma_client: Final = await _get_prisma_client() 

699 

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 ) 

708 

709 await ClaudeCodePluginRepository(prisma_client).table.update( 

710 where={"name": plugin_name}, 

711 data={"enabled": False, "updated_at": datetime.now(timezone.utc)}, 

712 ) 

713 

714 verbose_proxy_logger.info("Plugin %s disabled", plugin_name) 

715 return {"status": "success", "message": f"Plugin '{plugin_name}' disabled"} 

716 

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 ) 

725 

726 

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. 

738 

739 Requires a proxy admin API key. 

740 

741 Parameters: 

742 - plugin_name: The name of the plugin to delete 

743 """ 

744 try: 

745 _require_proxy_admin(user_api_key_dict) 

746 

747 prisma_client: Final = await _get_prisma_client() 

748 

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 ) 

757 

758 await ClaudeCodePluginRepository(prisma_client).table.delete(where={"name": plugin_name}) 

759 

760 verbose_proxy_logger.info("Plugin %s deleted", plugin_name) 

761 return {"status": "success", "message": f"Plugin '{plugin_name}' deleted"} 

762 

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 )