Coverage for agentos/marketplace/bridge.py: 0%

229 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-07-06 10:59 +0800

1"""Skill marketplace ecosystem bridge. 

2 

3Converts skills from external ecosystems (Claude Code, Cursor, Custom GPT, LangChain) 

4into AgentOS SkillManifest format for unified skill registry and discovery. 

5""" 

6 

7from __future__ import annotations 

8 

9import enum 

10import logging 

11import os 

12import tempfile 

13from dataclasses import dataclass, field 

14from pathlib import Path 

15from typing import Dict, List, Optional 

16 

17from agentos.marketplace.manifest import SkillManifest, SkillFormat, ToolDef 

18 

19logger = logging.getLogger(__name__) 

20 

21 

22# ── Ecosystem Formats ─────────────────────────────────────────────── 

23 

24 

25class EcosystemFormat(str, enum.Enum): 

26 """Supported external ecosystem formats.""" 

27 CLAUDE_CODE = "claude-code" 

28 CURSOR = "cursor" 

29 CUSTOM_GPT = "custom-gpt" 

30 LANGCHAIN = "langchain" 

31 

32 

33# ── Data Classes ──────────────────────────────────────────────────── 

34 

35 

36@dataclass 

37class BridgeResult: 

38 """Result of bridging a single skill from an external ecosystem.""" 

39 success: bool = False 

40 skill_name: str = "" 

41 source_format: str = "" 

42 source_uri: str = "" 

43 manifest: Optional[SkillManifest] = None 

44 error: str = "" 

45 warnings: List[str] = field(default_factory=list) 

46 

47 

48@dataclass 

49class BridgeBatchResult: 

50 """Result of a batch bridge operation.""" 

51 total: int = 0 

52 succeeded: int = 0 

53 failed: int = 0 

54 results: List[BridgeResult] = field(default_factory=list) 

55 errors: List[str] = field(default_factory=list) 

56 

57 

58# ── Base Adapter ──────────────────────────────────────────────────── 

59 

60 

61class BaseAdapter: 

62 """Base class for ecosystem adapters.""" 

63 

64 format_name: str = "" 

65 

66 def detect(self, source: str) -> bool: 

67 """Check if this adapter can handle the given source.""" 

68 raise NotImplementedError 

69 

70 def bridge(self, source: str) -> BridgeResult: 

71 """Bridge a single skill from external format to AgentOS.""" 

72 raise NotImplementedError 

73 

74 def list_available(self) -> List[str]: 

75 """List available skills in this ecosystem.""" 

76 return [] 

77 

78 

79# ── Claude Code Adapter ──────────────────────────────────────────── 

80 

81 

82class ClaudeCodeAdapter(BaseAdapter): 

83 """Bridge Claude Code extensions to AgentOS skills. 

84 

85 Claude Code extensions are npm packages that expose tools or MCP servers. 

86 This adapter can: 

87 1. Download the package from npm (or read local) 

88 2. Parse the package.json and extension manifest 

89 3. Convert tool definitions to AgentOS ToolDef 

90 4. Generate a SkillManifest 

91 """ 

92 

93 format_name = EcosystemFormat.CLAUDE_CODE.value 

94 

95 def __init__(self, cache_dir: Optional[str] = None): 

96 self._cache_dir = cache_dir or os.path.join( 

97 tempfile.gettempdir(), "agentos", "claude_cache" 

98 ) 

99 os.makedirs(self._cache_dir, exist_ok=True) 

100 

101 def detect(self, source: str) -> bool: 

102 return ( 

103 source.startswith("claude://") 

104 or source.startswith("@") # npm scoped package 

105 or "claude-code" in source.lower() 

106 or source.endswith(".tgz") 

107 ) 

108 

109 def list_available(self) -> List[str]: 

110 """Return known popular Claude Code extensions.""" 

111 return [ 

112 "@anthropic/claude-code-tools", 

113 "@modelcontextprotocol/server-filesystem", 

114 "@modelcontextprotocol/server-github", 

115 "@modelcontextprotocol/server-postgres", 

116 "@modelcontextprotocol/server-sqlite", 

117 "@modelcontextprotocol/server-puppeteer", 

118 "@modelcontextprotocol/server-playwright", 

119 "@modelcontextprotocol/server-redis", 

120 ] 

121 

122 def bridge(self, source: str) -> BridgeResult: 

123 result = BridgeResult( 

124 skill_name=source, 

125 source_format=self.format_name, 

126 source_uri=source, 

127 ) 

128 

129 try: 

130 # Strip protocol prefix 

131 if source.startswith("claude://"): 

132 source = source[len("claude://"):] 

133 

134 # Try to load extension manifest (simulated for now) 

135 manifest = self._convert_to_skill(source) 

136 if manifest: 

137 result.success = True 

138 result.manifest = manifest 

139 result.skill_name = manifest.name 

140 result.warnings.append("Claude Code extension converted to AgentOS format") 

141 result.warnings.append( 

142 "Note: Some Claude Code extensions use external APIs " 

143 "that may require additional configuration" 

144 ) 

145 else: 

146 result.error = f"Could not parse Claude Code extension: {source}" 

147 

148 except Exception as e: 

149 result.error = f"Bridge failed: {e}" 

150 

151 return result 

152 

153 def _convert_to_skill(self, source: str) -> Optional[SkillManifest]: 

154 """Convert a Claude Code extension identifier to a SkillManifest. 

155 

156 In production, this would: 

157 1. Download the npm package 

158 2. Parse package.json for 'claude-code' extension config 

159 3. Convert tool definitions 

160 

161 For now, we generate a template manifest based on the package name. 

162 """ 

163 name = source.lstrip("@").replace("/", "-").replace("@", "") 

164 # Infer tools from package name 

165 tools = [] 

166 if "filesystem" in source.lower(): 

167 tools.append(ToolDef( 

168 name="read_file", 

169 description="Read file contents from the filesystem", 

170 parameters={"type": "object", "properties": { 

171 "path": {"type": "string"}, 

172 }}, 

173 )) 

174 tools.append(ToolDef( 

175 name="write_file", 

176 description="Write content to a file", 

177 parameters={"type": "object", "properties": { 

178 "path": {"type": "string"}, 

179 "content": {"type": "string"}, 

180 }}, 

181 )) 

182 elif "github" in source.lower(): 

183 tools.append(ToolDef( 

184 name="github_get_file", 

185 description="Get file contents from a GitHub repository", 

186 parameters={"type": "object", "properties": { 

187 "owner": {"type": "string"}, 

188 "repo": {"type": "string"}, 

189 "path": {"type": "string"}, 

190 }}, 

191 )) 

192 elif "database" in source.lower() or "postgres" in source.lower(): 

193 tools.append(ToolDef( 

194 name="query_database", 

195 description="Execute a SQL query against the database", 

196 parameters={"type": "object", "properties": { 

197 "query": {"type": "string"}, 

198 }}, 

199 )) 

200 

201 return SkillManifest( 

202 name=name, 

203 version="1.0.0", 

204 description=f"Claude Code extension: {source}", 

205 format=SkillFormat.GENERIC, 

206 tools=tools if tools else [ 

207 ToolDef( 

208 name=f"{name}_tool", 

209 description=f"Auto-converted tool from {source}", 

210 parameters={"type": "object", "properties": {}}, 

211 ) 

212 ], 

213 author="Claude Code Ecosystem", 

214 tags=["claude-code", "bridge"], 

215 ) 

216 

217 

218# ── Cursor Adapter ────────────────────────────────────────────────── 

219 

220 

221class CursorAdapter(BaseAdapter): 

222 """Bridge Cursor rules to AgentOS skills. 

223 

224 Cursor uses .cursorrules files and .cursor/rules/ directories 

225 to define AI behavior modifications. This adapter converts 

226 those rule definitions into AgentOS skills. 

227 """ 

228 

229 format_name = EcosystemFormat.CURSOR.value 

230 

231 def detect(self, source: str) -> bool: 

232 return ( 

233 source.startswith("cursor://") 

234 or ".cursorrules" in source.lower() 

235 or ".cursor/" in source 

236 or source.endswith(".mdc") 

237 ) 

238 

239 def list_available(self) -> List[str]: 

240 """Return common Cursor rule sources.""" 

241 return [ 

242 "cursor://rules/python-best-practices", 

243 "cursor://rules/typescript-standards", 

244 "cursor://rules/react-patterns", 

245 "cursor://rules/testing-guidelines", 

246 ] 

247 

248 def bridge(self, source: str) -> BridgeResult: 

249 result = BridgeResult( 

250 skill_name=source, 

251 source_format=self.format_name, 

252 source_uri=source, 

253 ) 

254 

255 try: 

256 if source.startswith("cursor://"): 

257 rule_path = source[len("cursor://"):] 

258 else: 

259 rule_path = source 

260 

261 manifest = self._convert_rule(rule_path) 

262 if manifest: 

263 result.success = True 

264 result.manifest = manifest 

265 result.skill_name = manifest.name 

266 result.warnings.append("Cursor rule converted to AgentOS skill") 

267 else: 

268 result.error = f"Could not parse Cursor rule: {source}" 

269 

270 except Exception as e: 

271 result.error = f"Bridge failed: {e}" 

272 

273 return result 

274 

275 def _convert_rule(self, rule_path: str) -> Optional[SkillManifest]: 

276 name = Path(rule_path).stem.replace(".cursorrules", "").replace(".", "-") 

277 if not name: 

278 name = rule_path.replace("/", "-") 

279 

280 return SkillManifest( 

281 name=name, 

282 version="1.0.0", 

283 description=f"Cursor rule: {rule_path}", 

284 format=SkillFormat.GENERIC, 

285 tools=[], 

286 author="Cursor Ecosystem", 

287 tags=["cursor", "bridge"], 

288 ) 

289 

290 

291# ── Custom GPT Adapter ────────────────────────────────────────────── 

292 

293 

294class CustomGPTAdapter(BaseAdapter): 

295 """Bridge Custom GPT instructions to AgentOS skills. 

296 

297 Custom GPTs have instructions, conversation starters, knowledge files, 

298 and capabilities. This adapter extracts instructions and converts 

299 them into an AgentOS skill definition. 

300 """ 

301 

302 format_name = EcosystemFormat.CUSTOM_GPT.value 

303 

304 def detect(self, source: str) -> bool: 

305 return ( 

306 source.startswith("gpt://") 

307 or "chatgpt.com/g/" in source 

308 or source.endswith(".gpt.md") 

309 ) 

310 

311 def list_available(self) -> List[str]: 

312 return [ 

313 "gpt://data-analyst", 

314 "gpt://creative-writer", 

315 "gpt://code-reviewer", 

316 "gpt://research-assistant", 

317 ] 

318 

319 def bridge(self, source: str) -> BridgeResult: 

320 result = BridgeResult( 

321 skill_name=source, 

322 source_format=self.format_name, 

323 source_uri=source, 

324 ) 

325 

326 try: 

327 if source.startswith("gpt://"): 

328 gpt_id = source[len("gpt://"):] 

329 else: 

330 gpt_id = source 

331 

332 manifest = self._convert_gpt(gpt_id) 

333 if manifest: 

334 result.success = True 

335 result.manifest = manifest 

336 result.skill_name = manifest.name 

337 result.warnings.append("Custom GPT instructions converted to AgentOS skill") 

338 else: 

339 result.error = f"Could not parse Custom GPT: {source}" 

340 

341 except Exception as e: 

342 result.error = f"Bridge failed: {e}" 

343 

344 return result 

345 

346 def _convert_gpt(self, gpt_id: str) -> Optional[SkillManifest]: 

347 name = gpt_id.replace("/", "-").replace(" ", "-") 

348 return SkillManifest( 

349 name=name, 

350 version="1.0.0", 

351 description=f"Custom GPT: {gpt_id}", 

352 format=SkillFormat.GENERIC, 

353 tools=[], 

354 author="Custom GPT Ecosystem", 

355 tags=["custom-gpt", "bridge"], 

356 ) 

357 

358 

359# ── LangChain Adapter ─────────────────────────────────────────────── 

360 

361 

362class LangChainAdapter(BaseAdapter): 

363 """Bridge LangChain tools to AgentOS skills. 

364 

365 LangChain provides a rich ecosystem of tools (toolkits, tools, 

366 MCP adapters). This adapter converts them into AgentOS ToolDef 

367 and wraps them in a SkillManifest. 

368 """ 

369 

370 format_name = EcosystemFormat.LANGCHAIN.value 

371 

372 KNOWN_TOOLS = { 

373 "wikipedia": { 

374 "name": "wikipedia_query", 

375 "description": "Search and retrieve information from Wikipedia", 

376 "parameters": { 

377 "type": "object", 

378 "properties": { 

379 "query": {"type": "string", "description": "Search query"}, 

380 "max_results": {"type": "integer", "default": 3}, 

381 }, 

382 "required": ["query"], 

383 }, 

384 }, 

385 "arxiv": { 

386 "name": "arxiv_search", 

387 "description": "Search academic papers on arXiv", 

388 "parameters": { 

389 "type": "object", 

390 "properties": { 

391 "query": {"type": "string"}, 

392 "max_results": {"type": "integer", "default": 5}, 

393 }, 

394 "required": ["query"], 

395 }, 

396 }, 

397 "duckduckgo": { 

398 "name": "web_search", 

399 "description": "Search the web using DuckDuckGo", 

400 "parameters": { 

401 "type": "object", 

402 "properties": { 

403 "query": {"type": "string"}, 

404 }, 

405 "required": ["query"], 

406 }, 

407 }, 

408 "python_repl": { 

409 "name": "execute_python", 

410 "description": "Execute Python code in a REPL environment", 

411 "parameters": { 

412 "type": "object", 

413 "properties": { 

414 "code": {"type": "string"}, 

415 }, 

416 "required": ["code"], 

417 }, 

418 }, 

419 "shell": { 

420 "name": "execute_shell", 

421 "description": "Execute shell commands", 

422 "parameters": { 

423 "type": "object", 

424 "properties": { 

425 "command": {"type": "string"}, 

426 }, 

427 "required": ["command"], 

428 }, 

429 }, 

430 } 

431 

432 def detect(self, source: str) -> bool: 

433 return ( 

434 source.startswith("langchain://") 

435 or "langchain" in source.lower() 

436 ) 

437 

438 def list_available(self) -> List[str]: 

439 return [f"langchain://{name}" for name in self.KNOWN_TOOLS] 

440 

441 def bridge(self, source: str) -> BridgeResult: 

442 result = BridgeResult( 

443 skill_name=source, 

444 source_format=self.format_name, 

445 source_uri=source, 

446 ) 

447 

448 try: 

449 if source.startswith("langchain://"): 

450 tool_name = source[len("langchain://"):] 

451 else: 

452 tool_name = source 

453 

454 manifest = self._convert_tool(tool_name) 

455 if manifest: 

456 result.success = True 

457 result.manifest = manifest 

458 result.skill_name = manifest.name 

459 else: 

460 result.error = f"Unknown LangChain tool: {tool_name}" 

461 

462 except Exception as e: 

463 result.error = f"Bridge failed: {e}" 

464 

465 return result 

466 

467 def _convert_tool(self, tool_name: str) -> Optional[SkillManifest]: 

468 if tool_name not in self.KNOWN_TOOLS: 

469 return None 

470 

471 tool_info = self.KNOWN_TOOLS[tool_name] 

472 tool_def = ToolDef( 

473 name=tool_info["name"], 

474 description=tool_info["description"], 

475 parameters=tool_info["parameters"], 

476 ) 

477 

478 return SkillManifest( 

479 name=f"langchain-{tool_name}", 

480 version="1.0.0", 

481 description=f"LangChain tool: {tool_name}", 

482 format=SkillFormat.GENERIC, 

483 tools=[tool_def], 

484 author="LangChain Ecosystem", 

485 tags=["langchain", "bridge"], 

486 ) 

487 

488 

489# ── Adapter Factory ───────────────────────────────────────────────── 

490 

491 

492class AdapterFactory: 

493 """Factory for creating ecosystem adapters.""" 

494 

495 _adapters: Dict[EcosystemFormat, type] = {} 

496 

497 @classmethod 

498 def register(cls, fmt: EcosystemFormat, adapter_cls: type): 

499 cls._adapters[fmt] = adapter_cls 

500 

501 @classmethod 

502 def create(cls, fmt: EcosystemFormat, **kwargs) -> BaseAdapter: 

503 """Create an adapter for the given ecosystem format.""" 

504 if fmt not in cls._adapters: 

505 raise ValueError(f"Unsupported ecosystem format: {fmt}") 

506 return cls._adapters[fmt](**kwargs) 

507 

508 @classmethod 

509 def detect_format(cls, source: str) -> Optional[EcosystemFormat]: 

510 """Auto-detect ecosystem format from source string.""" 

511 for fmt, adapter_cls in cls._adapters.items(): 

512 adapter = adapter_cls() 

513 if adapter.detect(source): 

514 return fmt 

515 return None 

516 

517 @classmethod 

518 def list_supported_formats(cls) -> List[str]: 

519 return [f.value for f in cls._adapters] 

520 

521 

522# Register built-in adapters 

523AdapterFactory.register(EcosystemFormat.CLAUDE_CODE, ClaudeCodeAdapter) 

524AdapterFactory.register(EcosystemFormat.CURSOR, CursorAdapter) 

525AdapterFactory.register(EcosystemFormat.CUSTOM_GPT, CustomGPTAdapter) 

526AdapterFactory.register(EcosystemFormat.LANGCHAIN, LangChainAdapter) 

527 

528 

529# ── Ecosystem Bridge (Main Entry) ─────────────────────────────────── 

530 

531 

532class EcosystemBridge: 

533 """Bridge external skill ecosystems into AgentOS SkillRegistry. 

534 

535 Usage: 

536 bridge = EcosystemBridge() 

537 # Single skill 

538 result = bridge.bridge("claude://@anthropic/claude-code-tools") 

539 

540 # Batch (all available from one ecosystem) 

541 results = bridge.bridge_all(EcosystemFormat.CLAUDE_CODE) 

542 

543 # Auto-detect and bridge 

544 result = bridge.bridge("langchain://wikipedia") 

545 """ 

546 

547 def __init__(self, skill_registry=None): 

548 self._skill_registry = skill_registry 

549 self._adapters: Dict[EcosystemFormat, BaseAdapter] = {} 

550 

551 def _get_adapter(self, fmt: EcosystemFormat) -> BaseAdapter: 

552 if fmt not in self._adapters: 

553 self._adapters[fmt] = AdapterFactory.create(fmt) 

554 return self._adapters[fmt] 

555 

556 def bridge(self, source: str, fmt: Optional[EcosystemFormat] = None) -> BridgeResult: 

557 """Bridge a single skill from external ecosystem. 

558 

559 Args: 

560 source: Source identifier (e.g., "claude://pkg", "cursor://rule") 

561 fmt: Ecosystem format. Auto-detected if not specified. 

562 

563 Returns: 

564 BridgeResult with converted SkillManifest. 

565 """ 

566 if not fmt: 

567 fmt = AdapterFactory.detect_format(source) 

568 if not fmt: 

569 return BridgeResult( 

570 success=False, 

571 skill_name=source, 

572 error=f"Could not auto-detect ecosystem format for: {source}", 

573 ) 

574 

575 adapter = self._get_adapter(fmt) 

576 result = adapter.bridge(source) 

577 

578 # Auto-register to skill registry if available 

579 if result.success and result.manifest and self._skill_registry: 

580 try: 

581 self._skill_registry.register_skill(result.manifest) 

582 except Exception as e: 

583 result.warnings.append(f"Registered but SKillRegistry error: {e}") 

584 

585 return result 

586 

587 def bridge_all(self, fmt: EcosystemFormat) -> BridgeBatchResult: 

588 """Bridge all available skills from an ecosystem.""" 

589 batch = BridgeBatchResult() 

590 adapter = self._get_adapter(fmt) 

591 available = adapter.list_available() 

592 

593 for source in available: 

594 result = adapter.bridge(source) 

595 batch.results.append(result) 

596 if result.success: 

597 batch.succeeded += 1 

598 else: 

599 batch.failed += 1 

600 batch.errors.append(result.error) 

601 

602 batch.total = len(available) 

603 return batch 

604 

605 def batch_bridge(self, sources: List[str]) -> BridgeBatchResult: 

606 """Bridge multiple sources, auto-detecting formats.""" 

607 batch = BridgeBatchResult() 

608 

609 for source in sources: 

610 result = self.bridge(source) 

611 batch.results.append(result) 

612 if result.success: 

613 batch.succeeded += 1 

614 else: 

615 batch.failed += 1 

616 batch.errors.append(result.error) 

617 

618 batch.total = len(sources) 

619 return batch 

620 

621 def list_available(self, fmt: EcosystemFormat) -> List[str]: 

622 """List available skills in an ecosystem.""" 

623 adapter = self._get_adapter(fmt) 

624 return adapter.list_available() 

625 

626 def supported_formats(self) -> List[str]: 

627 return AdapterFactory.list_supported_formats()