Coverage for agentos/marketplace/bridge.py: 0%
233 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-02 09:59 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-02 09:59 +0800
1"""Skill marketplace ecosystem bridge.
3Converts skills from external ecosystems (Claude Code, Cursor, Custom GPT, LangChain)
4into AgentOS SkillManifest format for unified skill registry and discovery.
5"""
7from __future__ import annotations
9import enum
10import json
11import logging
12import os
13import re
14import tempfile
15import zipfile
16from dataclasses import dataclass, field
17from pathlib import Path
18from typing import Any, Dict, List, Optional
19from urllib.parse import urlparse
21from agentos.marketplace.manifest import SkillManifest, SkillFormat, ToolDef
23logger = logging.getLogger(__name__)
26# ── Ecosystem Formats ───────────────────────────────────────────────
29class EcosystemFormat(str, enum.Enum):
30 """Supported external ecosystem formats."""
31 CLAUDE_CODE = "claude-code"
32 CURSOR = "cursor"
33 CUSTOM_GPT = "custom-gpt"
34 LANGCHAIN = "langchain"
37# ── Data Classes ────────────────────────────────────────────────────
40@dataclass
41class BridgeResult:
42 """Result of bridging a single skill from an external ecosystem."""
43 success: bool = False
44 skill_name: str = ""
45 source_format: str = ""
46 source_uri: str = ""
47 manifest: Optional[SkillManifest] = None
48 error: str = ""
49 warnings: List[str] = field(default_factory=list)
52@dataclass
53class BridgeBatchResult:
54 """Result of a batch bridge operation."""
55 total: int = 0
56 succeeded: int = 0
57 failed: int = 0
58 results: List[BridgeResult] = field(default_factory=list)
59 errors: List[str] = field(default_factory=list)
62# ── Base Adapter ────────────────────────────────────────────────────
65class BaseAdapter:
66 """Base class for ecosystem adapters."""
68 format_name: str = ""
70 def detect(self, source: str) -> bool:
71 """Check if this adapter can handle the given source."""
72 raise NotImplementedError
74 def bridge(self, source: str) -> BridgeResult:
75 """Bridge a single skill from external format to AgentOS."""
76 raise NotImplementedError
78 def list_available(self) -> List[str]:
79 """List available skills in this ecosystem."""
80 return []
83# ── Claude Code Adapter ────────────────────────────────────────────
86class ClaudeCodeAdapter(BaseAdapter):
87 """Bridge Claude Code extensions to AgentOS skills.
89 Claude Code extensions are npm packages that expose tools or MCP servers.
90 This adapter can:
91 1. Download the package from npm (or read local)
92 2. Parse the package.json and extension manifest
93 3. Convert tool definitions to AgentOS ToolDef
94 4. Generate a SkillManifest
95 """
97 format_name = EcosystemFormat.CLAUDE_CODE.value
99 def __init__(self, cache_dir: Optional[str] = None):
100 self._cache_dir = cache_dir or os.path.join(
101 tempfile.gettempdir(), "agentos", "claude_cache"
102 )
103 os.makedirs(self._cache_dir, exist_ok=True)
105 def detect(self, source: str) -> bool:
106 return (
107 source.startswith("claude://")
108 or source.startswith("@") # npm scoped package
109 or "claude-code" in source.lower()
110 or source.endswith(".tgz")
111 )
113 def list_available(self) -> List[str]:
114 """Return known popular Claude Code extensions."""
115 return [
116 "@anthropic/claude-code-tools",
117 "@modelcontextprotocol/server-filesystem",
118 "@modelcontextprotocol/server-github",
119 "@modelcontextprotocol/server-postgres",
120 "@modelcontextprotocol/server-sqlite",
121 "@modelcontextprotocol/server-puppeteer",
122 "@modelcontextprotocol/server-playwright",
123 "@modelcontextprotocol/server-redis",
124 ]
126 def bridge(self, source: str) -> BridgeResult:
127 result = BridgeResult(
128 skill_name=source,
129 source_format=self.format_name,
130 source_uri=source,
131 )
133 try:
134 # Strip protocol prefix
135 if source.startswith("claude://"):
136 source = source[len("claude://"):]
138 # Try to load extension manifest (simulated for now)
139 manifest = self._convert_to_skill(source)
140 if manifest:
141 result.success = True
142 result.manifest = manifest
143 result.skill_name = manifest.name
144 result.warnings.append("Claude Code extension converted to AgentOS format")
145 result.warnings.append(
146 "Note: Some Claude Code extensions use external APIs "
147 "that may require additional configuration"
148 )
149 else:
150 result.error = f"Could not parse Claude Code extension: {source}"
152 except Exception as e:
153 result.error = f"Bridge failed: {e}"
155 return result
157 def _convert_to_skill(self, source: str) -> Optional[SkillManifest]:
158 """Convert a Claude Code extension identifier to a SkillManifest.
160 In production, this would:
161 1. Download the npm package
162 2. Parse package.json for 'claude-code' extension config
163 3. Convert tool definitions
165 For now, we generate a template manifest based on the package name.
166 """
167 name = source.lstrip("@").replace("/", "-").replace("@", "")
168 # Infer tools from package name
169 tools = []
170 if "filesystem" in source.lower():
171 tools.append(ToolDef(
172 name="read_file",
173 description="Read file contents from the filesystem",
174 parameters={"type": "object", "properties": {
175 "path": {"type": "string"},
176 }},
177 ))
178 tools.append(ToolDef(
179 name="write_file",
180 description="Write content to a file",
181 parameters={"type": "object", "properties": {
182 "path": {"type": "string"},
183 "content": {"type": "string"},
184 }},
185 ))
186 elif "github" in source.lower():
187 tools.append(ToolDef(
188 name="github_get_file",
189 description="Get file contents from a GitHub repository",
190 parameters={"type": "object", "properties": {
191 "owner": {"type": "string"},
192 "repo": {"type": "string"},
193 "path": {"type": "string"},
194 }},
195 ))
196 elif "database" in source.lower() or "postgres" in source.lower():
197 tools.append(ToolDef(
198 name="query_database",
199 description="Execute a SQL query against the database",
200 parameters={"type": "object", "properties": {
201 "query": {"type": "string"},
202 }},
203 ))
205 return SkillManifest(
206 name=name,
207 version="1.0.0",
208 description=f"Claude Code extension: {source}",
209 format=SkillFormat.GENERIC,
210 tools=tools if tools else [
211 ToolDef(
212 name=f"{name}_tool",
213 description=f"Auto-converted tool from {source}",
214 parameters={"type": "object", "properties": {}},
215 )
216 ],
217 author="Claude Code Ecosystem",
218 tags=["claude-code", "bridge"],
219 )
222# ── Cursor Adapter ──────────────────────────────────────────────────
225class CursorAdapter(BaseAdapter):
226 """Bridge Cursor rules to AgentOS skills.
228 Cursor uses .cursorrules files and .cursor/rules/ directories
229 to define AI behavior modifications. This adapter converts
230 those rule definitions into AgentOS skills.
231 """
233 format_name = EcosystemFormat.CURSOR.value
235 def detect(self, source: str) -> bool:
236 return (
237 source.startswith("cursor://")
238 or ".cursorrules" in source.lower()
239 or ".cursor/" in source
240 or source.endswith(".mdc")
241 )
243 def list_available(self) -> List[str]:
244 """Return common Cursor rule sources."""
245 return [
246 "cursor://rules/python-best-practices",
247 "cursor://rules/typescript-standards",
248 "cursor://rules/react-patterns",
249 "cursor://rules/testing-guidelines",
250 ]
252 def bridge(self, source: str) -> BridgeResult:
253 result = BridgeResult(
254 skill_name=source,
255 source_format=self.format_name,
256 source_uri=source,
257 )
259 try:
260 if source.startswith("cursor://"):
261 rule_path = source[len("cursor://"):]
262 else:
263 rule_path = source
265 manifest = self._convert_rule(rule_path)
266 if manifest:
267 result.success = True
268 result.manifest = manifest
269 result.skill_name = manifest.name
270 result.warnings.append("Cursor rule converted to AgentOS skill")
271 else:
272 result.error = f"Could not parse Cursor rule: {source}"
274 except Exception as e:
275 result.error = f"Bridge failed: {e}"
277 return result
279 def _convert_rule(self, rule_path: str) -> Optional[SkillManifest]:
280 name = Path(rule_path).stem.replace(".cursorrules", "").replace(".", "-")
281 if not name:
282 name = rule_path.replace("/", "-")
284 return SkillManifest(
285 name=name,
286 version="1.0.0",
287 description=f"Cursor rule: {rule_path}",
288 format=SkillFormat.GENERIC,
289 tools=[],
290 author="Cursor Ecosystem",
291 tags=["cursor", "bridge"],
292 )
295# ── Custom GPT Adapter ──────────────────────────────────────────────
298class CustomGPTAdapter(BaseAdapter):
299 """Bridge Custom GPT instructions to AgentOS skills.
301 Custom GPTs have instructions, conversation starters, knowledge files,
302 and capabilities. This adapter extracts instructions and converts
303 them into an AgentOS skill definition.
304 """
306 format_name = EcosystemFormat.CUSTOM_GPT.value
308 def detect(self, source: str) -> bool:
309 return (
310 source.startswith("gpt://")
311 or "chatgpt.com/g/" in source
312 or source.endswith(".gpt.md")
313 )
315 def list_available(self) -> List[str]:
316 return [
317 "gpt://data-analyst",
318 "gpt://creative-writer",
319 "gpt://code-reviewer",
320 "gpt://research-assistant",
321 ]
323 def bridge(self, source: str) -> BridgeResult:
324 result = BridgeResult(
325 skill_name=source,
326 source_format=self.format_name,
327 source_uri=source,
328 )
330 try:
331 if source.startswith("gpt://"):
332 gpt_id = source[len("gpt://"):]
333 else:
334 gpt_id = source
336 manifest = self._convert_gpt(gpt_id)
337 if manifest:
338 result.success = True
339 result.manifest = manifest
340 result.skill_name = manifest.name
341 result.warnings.append("Custom GPT instructions converted to AgentOS skill")
342 else:
343 result.error = f"Could not parse Custom GPT: {source}"
345 except Exception as e:
346 result.error = f"Bridge failed: {e}"
348 return result
350 def _convert_gpt(self, gpt_id: str) -> Optional[SkillManifest]:
351 name = gpt_id.replace("/", "-").replace(" ", "-")
352 return SkillManifest(
353 name=name,
354 version="1.0.0",
355 description=f"Custom GPT: {gpt_id}",
356 format=SkillFormat.GENERIC,
357 tools=[],
358 author="Custom GPT Ecosystem",
359 tags=["custom-gpt", "bridge"],
360 )
363# ── LangChain Adapter ───────────────────────────────────────────────
366class LangChainAdapter(BaseAdapter):
367 """Bridge LangChain tools to AgentOS skills.
369 LangChain provides a rich ecosystem of tools (toolkits, tools,
370 MCP adapters). This adapter converts them into AgentOS ToolDef
371 and wraps them in a SkillManifest.
372 """
374 format_name = EcosystemFormat.LANGCHAIN.value
376 KNOWN_TOOLS = {
377 "wikipedia": {
378 "name": "wikipedia_query",
379 "description": "Search and retrieve information from Wikipedia",
380 "parameters": {
381 "type": "object",
382 "properties": {
383 "query": {"type": "string", "description": "Search query"},
384 "max_results": {"type": "integer", "default": 3},
385 },
386 "required": ["query"],
387 },
388 },
389 "arxiv": {
390 "name": "arxiv_search",
391 "description": "Search academic papers on arXiv",
392 "parameters": {
393 "type": "object",
394 "properties": {
395 "query": {"type": "string"},
396 "max_results": {"type": "integer", "default": 5},
397 },
398 "required": ["query"],
399 },
400 },
401 "duckduckgo": {
402 "name": "web_search",
403 "description": "Search the web using DuckDuckGo",
404 "parameters": {
405 "type": "object",
406 "properties": {
407 "query": {"type": "string"},
408 },
409 "required": ["query"],
410 },
411 },
412 "python_repl": {
413 "name": "execute_python",
414 "description": "Execute Python code in a REPL environment",
415 "parameters": {
416 "type": "object",
417 "properties": {
418 "code": {"type": "string"},
419 },
420 "required": ["code"],
421 },
422 },
423 "shell": {
424 "name": "execute_shell",
425 "description": "Execute shell commands",
426 "parameters": {
427 "type": "object",
428 "properties": {
429 "command": {"type": "string"},
430 },
431 "required": ["command"],
432 },
433 },
434 }
436 def detect(self, source: str) -> bool:
437 return (
438 source.startswith("langchain://")
439 or "langchain" in source.lower()
440 )
442 def list_available(self) -> List[str]:
443 return [f"langchain://{name}" for name in self.KNOWN_TOOLS]
445 def bridge(self, source: str) -> BridgeResult:
446 result = BridgeResult(
447 skill_name=source,
448 source_format=self.format_name,
449 source_uri=source,
450 )
452 try:
453 if source.startswith("langchain://"):
454 tool_name = source[len("langchain://"):]
455 else:
456 tool_name = source
458 manifest = self._convert_tool(tool_name)
459 if manifest:
460 result.success = True
461 result.manifest = manifest
462 result.skill_name = manifest.name
463 else:
464 result.error = f"Unknown LangChain tool: {tool_name}"
466 except Exception as e:
467 result.error = f"Bridge failed: {e}"
469 return result
471 def _convert_tool(self, tool_name: str) -> Optional[SkillManifest]:
472 if tool_name not in self.KNOWN_TOOLS:
473 return None
475 tool_info = self.KNOWN_TOOLS[tool_name]
476 tool_def = ToolDef(
477 name=tool_info["name"],
478 description=tool_info["description"],
479 parameters=tool_info["parameters"],
480 )
482 return SkillManifest(
483 name=f"langchain-{tool_name}",
484 version="1.0.0",
485 description=f"LangChain tool: {tool_name}",
486 format=SkillFormat.GENERIC,
487 tools=[tool_def],
488 author="LangChain Ecosystem",
489 tags=["langchain", "bridge"],
490 )
493# ── Adapter Factory ─────────────────────────────────────────────────
496class AdapterFactory:
497 """Factory for creating ecosystem adapters."""
499 _adapters: Dict[EcosystemFormat, type] = {}
501 @classmethod
502 def register(cls, fmt: EcosystemFormat, adapter_cls: type):
503 cls._adapters[fmt] = adapter_cls
505 @classmethod
506 def create(cls, fmt: EcosystemFormat, **kwargs) -> BaseAdapter:
507 """Create an adapter for the given ecosystem format."""
508 if fmt not in cls._adapters:
509 raise ValueError(f"Unsupported ecosystem format: {fmt}")
510 return cls._adapters[fmt](**kwargs)
512 @classmethod
513 def detect_format(cls, source: str) -> Optional[EcosystemFormat]:
514 """Auto-detect ecosystem format from source string."""
515 for fmt, adapter_cls in cls._adapters.items():
516 adapter = adapter_cls()
517 if adapter.detect(source):
518 return fmt
519 return None
521 @classmethod
522 def list_supported_formats(cls) -> List[str]:
523 return [f.value for f in cls._adapters]
526# Register built-in adapters
527AdapterFactory.register(EcosystemFormat.CLAUDE_CODE, ClaudeCodeAdapter)
528AdapterFactory.register(EcosystemFormat.CURSOR, CursorAdapter)
529AdapterFactory.register(EcosystemFormat.CUSTOM_GPT, CustomGPTAdapter)
530AdapterFactory.register(EcosystemFormat.LANGCHAIN, LangChainAdapter)
533# ── Ecosystem Bridge (Main Entry) ───────────────────────────────────
536class EcosystemBridge:
537 """Bridge external skill ecosystems into AgentOS SkillRegistry.
539 Usage:
540 bridge = EcosystemBridge()
541 # Single skill
542 result = bridge.bridge("claude://@anthropic/claude-code-tools")
544 # Batch (all available from one ecosystem)
545 results = bridge.bridge_all(EcosystemFormat.CLAUDE_CODE)
547 # Auto-detect and bridge
548 result = bridge.bridge("langchain://wikipedia")
549 """
551 def __init__(self, skill_registry=None):
552 self._skill_registry = skill_registry
553 self._adapters: Dict[EcosystemFormat, BaseAdapter] = {}
555 def _get_adapter(self, fmt: EcosystemFormat) -> BaseAdapter:
556 if fmt not in self._adapters:
557 self._adapters[fmt] = AdapterFactory.create(fmt)
558 return self._adapters[fmt]
560 def bridge(self, source: str, fmt: Optional[EcosystemFormat] = None) -> BridgeResult:
561 """Bridge a single skill from external ecosystem.
563 Args:
564 source: Source identifier (e.g., "claude://pkg", "cursor://rule")
565 fmt: Ecosystem format. Auto-detected if not specified.
567 Returns:
568 BridgeResult with converted SkillManifest.
569 """
570 if not fmt:
571 fmt = AdapterFactory.detect_format(source)
572 if not fmt:
573 return BridgeResult(
574 success=False,
575 skill_name=source,
576 error=f"Could not auto-detect ecosystem format for: {source}",
577 )
579 adapter = self._get_adapter(fmt)
580 result = adapter.bridge(source)
582 # Auto-register to skill registry if available
583 if result.success and result.manifest and self._skill_registry:
584 try:
585 self._skill_registry.register_skill(result.manifest)
586 except Exception as e:
587 result.warnings.append(f"Registered but SKillRegistry error: {e}")
589 return result
591 def bridge_all(self, fmt: EcosystemFormat) -> BridgeBatchResult:
592 """Bridge all available skills from an ecosystem."""
593 batch = BridgeBatchResult()
594 adapter = self._get_adapter(fmt)
595 available = adapter.list_available()
597 for source in available:
598 result = adapter.bridge(source)
599 batch.results.append(result)
600 if result.success:
601 batch.succeeded += 1
602 else:
603 batch.failed += 1
604 batch.errors.append(result.error)
606 batch.total = len(available)
607 return batch
609 def batch_bridge(self, sources: List[str]) -> BridgeBatchResult:
610 """Bridge multiple sources, auto-detecting formats."""
611 batch = BridgeBatchResult()
613 for source in sources:
614 result = self.bridge(source)
615 batch.results.append(result)
616 if result.success:
617 batch.succeeded += 1
618 else:
619 batch.failed += 1
620 batch.errors.append(result.error)
622 batch.total = len(sources)
623 return batch
625 def list_available(self, fmt: EcosystemFormat) -> List[str]:
626 """List available skills in an ecosystem."""
627 adapter = self._get_adapter(fmt)
628 return adapter.list_available()
630 def supported_formats(self) -> List[str]:
631 return AdapterFactory.list_supported_formats()