1"""Skill marketplace ecosystem bridge.

2

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

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

5"""

6

7 from __future__ import annotations

8

9 import enum

10 import json

11 import logging

12 import os

13 import re

14 import tempfile

15 import zipfile

16 from dataclasses import dataclass, field

17 from pathlib import Path

18 from typing import Any, Dict, List, Optional

19 from urllib.parse import urlparse

20

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

22

23 logger = logging.getLogger(__name__)

24

25

26# ── Ecosystem Formats ───────────────────────────────────────────────

27

28

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

35

36

37# ── Data Classes ────────────────────────────────────────────────────

38

39

40@dataclass

41 class 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)

50

51

52@dataclass

53 class 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)

60

61

62# ── Base Adapter ────────────────────────────────────────────────────

63

64

65 class BaseAdapter:

66 """Base class for ecosystem adapters."""

67

68 format_name: str = ""

69

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

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

72 raise NotImplementedError

73

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

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

76 raise NotImplementedError

77

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

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

80 return []

81

82

83# ── Claude Code Adapter ────────────────────────────────────────────

84

85

86 class ClaudeCodeAdapter(BaseAdapter):

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

88

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

96

97 format_name = EcosystemFormat.CLAUDE_CODE.value

98

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)

104

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 )

112

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 ]

125

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 )

132

133 try:

134 # Strip protocol prefix

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

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

137

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

151

152 except Exception as e:

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

154

155 return result

156

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

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

159

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

164

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

204

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 )

220

221

222# ── Cursor Adapter ──────────────────────────────────────────────────

223

224

225 class CursorAdapter(BaseAdapter):

226 """Bridge Cursor rules to AgentOS skills.

227

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

232

233 format_name = EcosystemFormat.CURSOR.value

234

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 )

242

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 ]

251

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 )

258

259 try:

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

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

262 else:

263 rule_path = source

264

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

273

274 except Exception as e:

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

276

277 return result

278

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("/", "-")

283

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 )

293

294

295# ── Custom GPT Adapter ──────────────────────────────────────────────

296

297

298 class CustomGPTAdapter(BaseAdapter):

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

300

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

305

306 format_name = EcosystemFormat.CUSTOM_GPT.value

307

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 )

314

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 ]

322

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 )

329

330 try:

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

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

333 else:

334 gpt_id = source

335

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

344

345 except Exception as e:

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

347

348 return result

349

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 )

361

362

363# ── LangChain Adapter ───────────────────────────────────────────────

364

365

366 class LangChainAdapter(BaseAdapter):

367 """Bridge LangChain tools to AgentOS skills.

368

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

373

374 format_name = EcosystemFormat.LANGCHAIN.value

375

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 }

435

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

437 return (

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

439 or "langchain" in source.lower()

440 )

441

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

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

444

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 )

451

452 try:

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

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

455 else:

456 tool_name = source

457

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

465

466 except Exception as e:

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

468

469 return result

470

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

472 if tool_name not in self.KNOWN_TOOLS:

473 return None

474

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 )

481

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 )

491

492

493# ── Adapter Factory ─────────────────────────────────────────────────

494

495

496 class AdapterFactory:

497 """Factory for creating ecosystem adapters."""

498

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

500

501 @classmethod

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

503 cls._adapters[fmt] = adapter_cls

504

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)

511

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

520

521 @classmethod

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

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

524

525

526# Register built-in adapters

527 AdapterFactory.register(EcosystemFormat.CLAUDE_CODE, ClaudeCodeAdapter)

528 AdapterFactory.register(EcosystemFormat.CURSOR, CursorAdapter)

529 AdapterFactory.register(EcosystemFormat.CUSTOM_GPT, CustomGPTAdapter)

530 AdapterFactory.register(EcosystemFormat.LANGCHAIN, LangChainAdapter)

531

532

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

534

535

536 class EcosystemBridge:

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

538

539 Usage:

540 bridge = EcosystemBridge()

541 # Single skill

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

543

544 # Batch (all available from one ecosystem)

545 results = bridge.bridge_all(EcosystemFormat.CLAUDE_CODE)

546

547 # Auto-detect and bridge

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

549 """

550

551 def __init__(self, skill_registry=None):

552 self._skill_registry = skill_registry

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

554

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]

559

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

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

562

563 Args:

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

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

566

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 )

578

579 adapter = self._get_adapter(fmt)

580 result = adapter.bridge(source)

581

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

588

589 return result

590

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

596

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)

605

606 batch.total = len(available)

607 return batch

608

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

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

611 batch = BridgeBatchResult()

612

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)

621

622 batch.total = len(sources)

623 return batch

624

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

629

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

631 return AdapterFactory.list_supported_formats()