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

241 statements  

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

1""" 

2Marketplace Importer — Import skills from external sources (OpenClaw, HuggingFace, GitHub). 

3 

4OpenClaw Community: https://github.com/openclaw/skills 

5 - skill.yaml → SkillManifest (openclaw format) 

6 - Auto-detect format, convert, register 

7 

8Usage: 

9 from agentos.marketplace.importer import OpenClawImporter 

10 importer = OpenClawImporter(registry) 

11 skill = await importer.import_skill("pdf-tools") 

12""" 

13 

14from __future__ import annotations 

15 

16import asyncio 

17from collections.abc import Callable 

18from dataclasses import dataclass, field 

19from pathlib import Path 

20 

21from agentos.marketplace.manifest import SkillManifest 

22from agentos.marketplace.registry import InstallResult, SkillRegistry 

23 

24# ── OpenClaw Importer ── 

25 

26OPENCLAW_SKILLS_REPO = "https://github.com/nicepkg/openclaw-skill-store" 

27OPENCLAW_RAW_BASE = "https://raw.githubusercontent.com/nicepkg/openclaw-skill-store/main" 

28OPENCLAW_API = "https://api.github.com/repos/nicepkg/openclaw-skill-store" 

29 

30 

31@dataclass 

32class RemoteSkill: 

33 """Skill metadata discovered from a remote source.""" 

34 

35 name: str 

36 path: str # Relative path in the repo 

37 description: str = "" 

38 author: str = "" 

39 version: str = "0.1.0" 

40 tags: list[str] = field(default_factory=list) 

41 download_url: str = "" 

42 raw_url: str = "" 

43 source: str = "openclaw" 

44 

45 

46class OpenClawImporter: 

47 """Import skills from the OpenClaw community skill store. 

48 

49 Flow: 

50 1. list_available() — fetch skill catalog from GitHub API 

51 2. import_skill(name) — download skill.yaml → parse → register 

52 3. import_all() — batch import all available skills 

53 """ 

54 

55 def __init__(self, registry: SkillRegistry, cache_dir: str = ""): 

56 self._registry = registry 

57 self._cache_dir = ( 

58 Path(cache_dir) if cache_dir else Path.home() / ".agentos" / "marketplace" / "openclaw" 

59 ) 

60 self._cache_dir.mkdir(parents=True, exist_ok=True) 

61 

62 self._catalog: list[RemoteSkill] = [] 

63 self._fetch_fn: Callable[[str], str] | None = None # Injection point for testing 

64 

65 # ── Catalog ── 

66 

67 async def list_available(self, refresh: bool = False) -> list[RemoteSkill]: 

68 """List all available OpenClaw community skills. 

69 

70 Returns cached catalog unless refresh=True. 

71 """ 

72 if self._catalog and not refresh: 

73 return self._catalog 

74 

75 skills = [] 

76 

77 # Try fetching directory listing from the raw GitHub API 

78 try: 

79 import aiohttp 

80 except ImportError: 

81 return await self._list_fallback() 

82 

83 try: 

84 async with aiohttp.ClientSession() as session: 

85 # Fetch the top-level directory listing 

86 url = f"{OPENCLAW_API}/contents/skills" 

87 async with session.get( 

88 url, headers={"Accept": "application/vnd.github.v3+json"} 

89 ) as resp: 

90 if resp.status != 200: 

91 return await self._list_fallback() 

92 

93 entries = await resp.json() 

94 for entry in entries: 

95 if entry.get("type") != "dir": 

96 continue 

97 

98 skill_name = entry["name"] 

99 skill_path = f"skills/{skill_name}" 

100 raw_url = f"{OPENCLAW_RAW_BASE}/{skill_path}/skill.yaml" 

101 download_url = entry.get("url", "") 

102 

103 # Try to read skill.yaml for metadata 

104 meta = await self._fetch_skill_meta(session, skill_path) 

105 skills.append( 

106 RemoteSkill( 

107 name=meta.get("name", skill_name), 

108 path=skill_path, 

109 description=meta.get("description", ""), 

110 author=meta.get("author", ""), 

111 version=meta.get("version", "0.1.0"), 

112 tags=meta.get("tags", []), 

113 raw_url=raw_url, 

114 download_url=download_url, 

115 ) 

116 ) 

117 

118 except Exception: 

119 return await self._list_fallback() 

120 

121 self._catalog = skills 

122 return skills 

123 

124 async def _list_fallback(self) -> list[RemoteSkill]: 

125 """Fallback: return a curated list of known OpenClaw skills (60+).""" 

126 known_skills = [ 

127 # ── Meta & Creator (3) ── 

128 RemoteSkill( 

129 name="skill-creator", 

130 path="skills/skill-creator", 

131 description="Guide for creating effective skills", 

132 tags=["meta", "creator"], 

133 ), 

134 RemoteSkill( 

135 name="mcp-builder", 

136 path="skills/mcp-builder", 

137 description="Guide for creating MCP servers and tools", 

138 tags=["mcp", "infra"], 

139 ), 

140 RemoteSkill( 

141 name="coding-agent", 

142 path="skills/coding-agent", 

143 description="Autonomous coding agent for complex software tasks", 

144 tags=["dev", "agent"], 

145 ), 

146 # ── Office & Documents (8) ── 

147 RemoteSkill( 

148 name="docx", 

149 path="skills/docx", 

150 description="Create and edit .docx documents", 

151 tags=["document", "office"], 

152 ), 

153 RemoteSkill( 

154 name="pdf", 

155 path="skills/pdf", 

156 description="PDF manipulation toolkit: merge, split, extract, annotate", 

157 tags=["document", "pdf"], 

158 ), 

159 RemoteSkill( 

160 name="pptx", 

161 path="skills/pptx", 

162 description="Create and edit .pptx presentations", 

163 tags=["presentation", "office"], 

164 ), 

165 RemoteSkill( 

166 name="xlsx", 

167 path="skills/xlsx", 

168 description="Create and edit .xlsx spreadsheets with formulas and charts", 

169 tags=["spreadsheet", "office"], 

170 ), 

171 RemoteSkill( 

172 name="nano-pdf", 

173 path="skills/nano-pdf", 

174 description="Lightweight PDF reading and text extraction", 

175 tags=["document", "pdf"], 

176 ), 

177 RemoteSkill( 

178 name="notion", 

179 path="skills/notion", 

180 description="Notion integration: pages, databases, blocks CRUD", 

181 tags=["productivity", "notion"], 

182 ), 

183 RemoteSkill( 

184 name="obsidian", 

185 path="skills/obsidian", 

186 description="Obsidian vault integration: read/write notes, backlinks", 

187 tags=["knowledge", "obsidian"], 

188 ), 

189 RemoteSkill( 

190 name="bear-notes", 

191 path="skills/bear-notes", 

192 description="Bear notes app integration for Apple ecosystem", 

193 tags=["notes", "apple"], 

194 ), 

195 # ── Design & Creative (6) ── 

196 RemoteSkill( 

197 name="brand-guidelines", 

198 path="skills/brand-guidelines", 

199 description="Applies brand colors and typography to any artifact", 

200 tags=["design", "brand"], 

201 ), 

202 RemoteSkill( 

203 name="canvas-design", 

204 path="skills/canvas-design", 

205 description="Create beautiful visual art in .png and .pdf", 

206 tags=["art", "design"], 

207 ), 

208 RemoteSkill( 

209 name="algorithmic-art", 

210 path="skills/algorithmic-art", 

211 description="Creating algorithmic art using p5.js", 

212 tags=["art", "creative"], 

213 ), 

214 RemoteSkill( 

215 name="theme-factory", 

216 path="skills/theme-factory", 

217 description="Apply visual themes: color schemes, typography, spacing", 

218 tags=["design", "theme"], 

219 ), 

220 RemoteSkill( 

221 name="slack-gif-creator", 

222 path="skills/slack-gif-creator", 

223 description="Create animated GIFs optimized for Slack", 

224 tags=["media", "slack"], 

225 ), 

226 RemoteSkill( 

227 name="openai-image-gen", 

228 path="skills/openai-image-gen", 

229 description="Generate images using DALL-E / OpenAI image API", 

230 tags=["ai", "image"], 

231 ), 

232 # ── Web & Frontend (5) ── 

233 RemoteSkill( 

234 name="frontend-design", 

235 path="skills/frontend-design", 

236 description="Create distinctive production-grade frontend interfaces", 

237 tags=["web", "frontend"], 

238 ), 

239 RemoteSkill( 

240 name="web-artifacts-builder", 

241 path="skills/web-artifacts-builder", 

242 description="Build complex multi-file HTML artifacts with CSS/JS", 

243 tags=["web", "html"], 

244 ), 

245 RemoteSkill( 

246 name="web-search", 

247 path="skills/web-search", 

248 description="Web search with multiple engines and result parsing", 

249 tags=["web", "search"], 

250 ), 

251 RemoteSkill( 

252 name="blogwatcher", 

253 path="skills/blogwatcher", 

254 description="Monitor blogs and RSS feeds for updates", 

255 tags=["web", "monitoring"], 

256 ), 

257 RemoteSkill( 

258 name="wikipedia", 

259 path="skills/wikipedia", 

260 description="Search and extract content from Wikipedia", 

261 tags=["web", "knowledge"], 

262 ), 

263 # ── Developer Tools (10) ── 

264 RemoteSkill( 

265 name="github", 

266 path="skills/github", 

267 description="GitHub API: repos, issues, PRs, actions, gists", 

268 tags=["dev", "github"], 

269 ), 

270 RemoteSkill( 

271 name="gh-issues", 

272 path="skills/gh-issues", 

273 description="Deep GitHub issues management and triage", 

274 tags=["dev", "github"], 

275 ), 

276 RemoteSkill( 

277 name="git", 

278 path="skills/git", 

279 description="Git version control: commit, branch, merge, rebase", 

280 tags=["dev", "vcs"], 

281 ), 

282 RemoteSkill( 

283 name="docker", 

284 path="skills/docker", 

285 description="Docker container management: build, run, compose", 

286 tags=["dev", "infra"], 

287 ), 

288 RemoteSkill( 

289 name="code-review", 

290 path="skills/code-review", 

291 description="Automated code review with best-practice suggestions", 

292 tags=["dev", "quality"], 

293 ), 

294 RemoteSkill( 

295 name="database", 

296 path="skills/database", 

297 description="SQL/NoSQL database query and schema management", 

298 tags=["dev", "data"], 

299 ), 

300 RemoteSkill( 

301 name="api-tester", 

302 path="skills/api-tester", 

303 description="REST/GraphQL API testing and documentation", 

304 tags=["dev", "api"], 

305 ), 

306 RemoteSkill( 

307 name="tmux", 

308 path="skills/tmux", 

309 description="Tmux session management and automation", 

310 tags=["dev", "terminal"], 

311 ), 

312 RemoteSkill( 

313 name="node-connect", 

314 path="skills/node-connect", 

315 description="Node.js runtime integration and package management", 

316 tags=["dev", "node"], 

317 ), 

318 RemoteSkill( 

319 name="model-usage", 

320 path="skills/model-usage", 

321 description="Track and optimize AI model usage and costs", 

322 tags=["dev", "ai"], 

323 ), 

324 # ── Communication & Messaging (6) ── 

325 RemoteSkill( 

326 name="internal-comms", 

327 path="skills/internal-comms", 

328 description="Internal communications: announcements, memos, updates", 

329 tags=["writing", "business"], 

330 ), 

331 RemoteSkill( 

332 name="slack", 

333 path="skills/slack", 

334 description="Slack integration: messages, channels, reactions", 

335 tags=["communication", "slack"], 

336 ), 

337 RemoteSkill( 

338 name="discord", 

339 path="skills/discord", 

340 description="Discord bot integration for servers and DMs", 

341 tags=["communication", "discord"], 

342 ), 

343 RemoteSkill( 

344 name="email", 

345 path="skills/email", 

346 description="Email composition, sending, and inbox management", 

347 tags=["communication", "email"], 

348 ), 

349 RemoteSkill( 

350 name="telegram", 

351 path="skills/telegram", 

352 description="Telegram bot API: messages, channels, inline queries", 

353 tags=["communication", "telegram"], 

354 ), 

355 RemoteSkill( 

356 name="imsg", 

357 path="skills/imsg", 

358 description="iMessage integration for Apple ecosystem", 

359 tags=["communication", "apple"], 

360 ), 

361 # ── Productivity (8) ── 

362 RemoteSkill( 

363 name="calendar", 

364 path="skills/calendar", 

365 description="Calendar management: events, reminders, scheduling", 

366 tags=["productivity", "time"], 

367 ), 

368 RemoteSkill( 

369 name="task-manager", 

370 path="skills/task-manager", 

371 description="Task and to-do list management with priorities", 

372 tags=["productivity", "tasks"], 

373 ), 

374 RemoteSkill( 

375 name="notes", 

376 path="skills/notes", 

377 description="Quick note-taking with search and organization", 

378 tags=["productivity", "notes"], 

379 ), 

380 RemoteSkill( 

381 name="apple-notes", 

382 path="skills/apple-notes", 

383 description="Apple Notes app integration", 

384 tags=["productivity", "apple"], 

385 ), 

386 RemoteSkill( 

387 name="apple-reminders", 

388 path="skills/apple-reminders", 

389 description="Apple Reminders app integration", 

390 tags=["productivity", "apple"], 

391 ), 

392 RemoteSkill( 

393 name="things-mac", 

394 path="skills/things-mac", 

395 description="Things 3 task manager integration for macOS", 

396 tags=["productivity", "mac"], 

397 ), 

398 RemoteSkill( 

399 name="trello", 

400 path="skills/trello", 

401 description="Trello board management: cards, lists, boards", 

402 tags=["productivity", "pm"], 

403 ), 

404 RemoteSkill( 

405 name="summarize", 

406 path="skills/summarize", 

407 description="Intelligent text summarization with configurable depth", 

408 tags=["productivity", "text"], 

409 ), 

410 # ── Data & Analysis (5) ── 

411 RemoteSkill( 

412 name="data-analysis", 

413 path="skills/data-analysis", 

414 description="Statistical analysis, visualization, and reporting", 

415 tags=["data", "analytics"], 

416 ), 

417 RemoteSkill( 

418 name="spreadsheet", 

419 path="skills/spreadsheet", 

420 description="Advanced spreadsheet operations and formulas", 

421 tags=["data", "office"], 

422 ), 

423 RemoteSkill( 

424 name="csv-toolkit", 

425 path="skills/csv-toolkit", 

426 description="CSV parsing, transformation, and export toolkit", 

427 tags=["data", "csv"], 

428 ), 

429 RemoteSkill( 

430 name="json-toolkit", 

431 path="skills/json-toolkit", 

432 description="JSON manipulation, validation, and transformation", 

433 tags=["data", "json"], 

434 ), 

435 RemoteSkill( 

436 name="markdown-toolkit", 

437 path="skills/markdown-toolkit", 

438 description="Markdown rendering, conversion, and templating", 

439 tags=["writing", "markdown"], 

440 ), 

441 # ── Media & Multimedia (5) ── 

442 RemoteSkill( 

443 name="video-frames", 

444 path="skills/video-frames", 

445 description="Extract and analyze frames from video files", 

446 tags=["media", "video"], 

447 ), 

448 RemoteSkill( 

449 name="audio-transcribe", 

450 path="skills/audio-transcribe", 

451 description="Speech-to-text transcription with Whisper API", 

452 tags=["media", "audio"], 

453 ), 

454 RemoteSkill( 

455 name="openai-whisper", 

456 path="skills/openai-whisper", 

457 description="OpenAI Whisper speech recognition integration", 

458 tags=["media", "audio"], 

459 ), 

460 RemoteSkill( 

461 name="openai-whisper-api", 

462 path="skills/openai-whisper-api", 

463 description="OpenAI Whisper API with batch processing", 

464 tags=["media", "audio"], 

465 ), 

466 RemoteSkill( 

467 name="sherpa-onnx-tts", 

468 path="skills/sherpa-onnx-tts", 

469 description="Text-to-speech with Sherpa-ONNX engine", 

470 tags=["media", "tts"], 

471 ), 

472 # ── System & Automation (5) ── 

473 RemoteSkill( 

474 name="automation", 

475 path="skills/automation", 

476 description="Workflow automation: triggers, actions, scheduling", 

477 tags=["automation", "workflow"], 

478 ), 

479 RemoteSkill( 

480 name="file-organizer", 

481 path="skills/file-organizer", 

482 description="Smart file organization: sort, rename, deduplicate", 

483 tags=["system", "files"], 

484 ), 

485 RemoteSkill( 

486 name="backup", 

487 path="skills/backup", 

488 description="Automated backup and restore for files and configs", 

489 tags=["system", "backup"], 

490 ), 

491 RemoteSkill( 

492 name="weather", 

493 path="skills/weather", 

494 description="Weather forecasts, alerts, and historical data", 

495 tags=["utility", "weather"], 

496 ), 

497 RemoteSkill( 

498 name="healthcheck", 

499 path="skills/healthcheck", 

500 description="System health monitoring and diagnostics", 

501 tags=["system", "monitoring"], 

502 ), 

503 # ── Security & Privacy (3) ── 

504 RemoteSkill( 

505 name="1password", 

506 path="skills/1password", 

507 description="1Password vault integration for secrets management", 

508 tags=["security", "password"], 

509 ), 

510 RemoteSkill( 

511 name="encryption", 

512 path="skills/encryption", 

513 description="File encryption/decryption with multiple algorithms", 

514 tags=["security", "crypto"], 

515 ), 

516 RemoteSkill( 

517 name="session-logs", 

518 path="skills/session-logs", 

519 description="Audit and analyze agent session logs", 

520 tags=["security", "audit"], 

521 ), 

522 ] 

523 self._catalog = known_skills 

524 return known_skills 

525 

526 async def _fetch_skill_meta(self, session, skill_path: str) -> dict: 

527 """Fetch skill.yaml metadata for a single skill.""" 

528 url = f"{OPENCLAW_API}/contents/{skill_path}/skill.yaml" 

529 try: 

530 async with session.get( 

531 url, headers={"Accept": "application/vnd.github.v3.raw"} 

532 ) as resp: 

533 if resp.status == 200: 

534 text = await resp.text() 

535 import yaml 

536 

537 return yaml.safe_load(text) or {} 

538 except Exception: 

539 pass 

540 return {} 

541 

542 # ── Import ── 

543 

544 async def import_skill(self, name: str, force: bool = False) -> InstallResult | None: 

545 """Import a single skill from OpenClaw by name. 

546 

547 Pipeline: 

548 1. Find in catalog 

549 2. Fetch skill.yaml from raw GitHub 

550 3. Parse as OpenClaw format → SkillManifest 

551 4. Register in SkillRegistry 

552 """ 

553 # Ensure catalog is loaded 

554 if not self._catalog: 

555 await self.list_available() 

556 

557 # Find skill 

558 skill_ref = None 

559 for s in self._catalog: 

560 if s.name == name: 

561 skill_ref = s 

562 break 

563 

564 if not skill_ref: 

565 return None 

566 

567 # Fetch skill.yaml 

568 yaml_url = f"{OPENCLAW_RAW_BASE}/{skill_ref.path}/skill.yaml" 

569 yaml_text = await self._fetch_url(yaml_url) 

570 

571 if not yaml_text: 

572 return None 

573 

574 # Parse as OpenClaw format 

575 import yaml 

576 

577 try: 

578 raw = yaml.safe_load(yaml_text) 

579 except yaml.YAMLError: 

580 return None 

581 

582 if not raw: 

583 return None 

584 

585 # Convert to SkillManifest 

586 raw["format"] = "openclaw" 

587 manifest = SkillManifest.from_dict( 

588 raw, 

589 source=f"openclaw:{name}", 

590 install_path=str(self._cache_dir / name), 

591 ) 

592 

593 # Register 

594 return self._registry.register(manifest, force=force) 

595 

596 async def import_all(self, max_skills: int = 50) -> list[InstallResult]: 

597 """Import all available OpenClaw skills.""" 

598 if not self._catalog: 

599 await self.list_available() 

600 

601 results = [] 

602 semaphore = asyncio.Semaphore(5) # Limit concurrent fetches 

603 

604 async def _import_one(skill: RemoteSkill): 

605 async with semaphore: 

606 return await self.import_skill(skill.name) 

607 

608 tasks = [_import_one(s) for s in self._catalog[:max_skills]] 

609 raw_results = await asyncio.gather(*tasks, return_exceptions=True) 

610 

611 for r in raw_results: 

612 if isinstance(r, Exception): 

613 pass 

614 elif r is not None: 

615 results.append(r) 

616 

617 return results 

618 

619 async def search(self, query: str) -> list[RemoteSkill]: 

620 """Search the catalog by name/description/tag.""" 

621 if not self._catalog: 

622 await self.list_available() 

623 

624 q = query.lower() 

625 results = [] 

626 for s in self._catalog: 

627 if ( 

628 q in s.name.lower() 

629 or q in s.description.lower() 

630 or any(q in t.lower() for t in s.tags) 

631 ): 

632 results.append(s) 

633 return results 

634 

635 # ── Internal ── 

636 

637 async def _fetch_url(self, url: str) -> str: 

638 """Fetch URL content (supports GitHub API + raw).""" 

639 if self._fetch_fn: 

640 return self._fetch_fn(url) 

641 

642 try: 

643 import aiohttp 

644 

645 async with aiohttp.ClientSession() as session: 

646 async with session.get( 

647 url, headers={"Accept": "application/vnd.github.v3.raw"} 

648 ) as resp: 

649 if resp.status == 200: 

650 return await resp.text() 

651 except Exception: 

652 pass 

653 

654 # Fallback: urllib 

655 try: 

656 import urllib.request 

657 

658 req = urllib.request.Request(url, headers={"Accept": "application/vnd.github.v3.raw"}) 

659 with urllib.request.urlopen(req, timeout=10) as resp: 

660 return resp.read().decode("utf-8") 

661 except Exception: 

662 pass 

663 

664 return "" 

665 

666 

667# ── HuggingFace Importer ── 

668 

669 

670class HuggingFaceImporter: 

671 """Import skills from HuggingFace.co skill repositories. 

672 

673 Flow: 

674 hf://username/skill-repo → download → parse skill.yaml → register 

675 """ 

676 

677 def __init__(self, registry: SkillRegistry, cache_dir: str = ""): 

678 self._registry = registry 

679 self._cache_dir = ( 

680 Path(cache_dir) 

681 if cache_dir 

682 else Path.home() / ".agentos" / "marketplace" / "huggingface" 

683 ) 

684 self._cache_dir.mkdir(parents=True, exist_ok=True) 

685 

686 async def import_from_hf(self, repo_id: str, force: bool = False) -> InstallResult | None: 

687 """Import a skill from HuggingFace repo. 

688 

689 Args: 

690 repo_id: e.g. 'username/agentos-skill-translator' 

691 """ 

692 import aiohttp 

693 

694 # Try fetching skill.yaml from main branch 

695 yaml_url = f"https://huggingface.co/{repo_id}/resolve/main/skill.yaml" 

696 yaml_text = "" 

697 try: 

698 async with aiohttp.ClientSession() as session: 

699 async with session.get(yaml_url, timeout=10) as resp: 

700 if resp.status == 200: 

701 yaml_text = await resp.text() 

702 except Exception: 

703 pass 

704 

705 if not yaml_text: 

706 yaml_url = f"https://huggingface.co/{repo_id}/resolve/main/agentos.yaml" 

707 try: 

708 async with aiohttp.ClientSession() as session: 

709 async with session.get(yaml_url, timeout=10) as resp: 

710 if resp.status == 200: 

711 yaml_text = await resp.text() 

712 except Exception: 

713 pass 

714 

715 if not yaml_text: 

716 return None 

717 

718 import yaml 

719 

720 try: 

721 raw = yaml.safe_load(yaml_text) 

722 except yaml.YAMLError: 

723 return None 

724 

725 manifest = SkillManifest.from_dict( 

726 raw, 

727 source=f"huggingface:{repo_id}", 

728 install_path=str(self._cache_dir / repo_id.replace("/", "_")), 

729 ) 

730 

731 return self._registry.register(manifest, force=force) 

732 

733 

734# ── GitHub Importer ── 

735 

736 

737class GitHubImporter: 

738 """Import skills from arbitrary GitHub repositories. 

739 

740 Flow: 

741 github://user/repo/path → download skill.yaml → parse → register 

742 """ 

743 

744 def __init__(self, registry: SkillRegistry, cache_dir: str = ""): 

745 self._registry = registry 

746 self._cache_dir = ( 

747 Path(cache_dir) if cache_dir else Path.home() / ".agentos" / "marketplace" / "github" 

748 ) 

749 self._cache_dir.mkdir(parents=True, exist_ok=True) 

750 

751 async def import_from_github( 

752 self, 

753 repo: str, 

754 path: str = "", 

755 ref: str = "main", 

756 force: bool = False, 

757 ) -> InstallResult | None: 

758 """Import a skill from a GitHub repo. 

759 

760 Args: 

761 repo: 'user/repo' 

762 path: subdirectory containing skill.yaml (e.g. 'skills/my-skill') 

763 ref: branch/tag (default 'main') 

764 """ 

765 raw_base = f"https://raw.githubusercontent.com/{repo}/{ref}" 

766 manifest_path = f"{raw_base}/{path}/skill.yaml" if path else f"{raw_base}/skill.yaml" 

767 

768 yaml_text = "" 

769 import aiohttp 

770 

771 try: 

772 async with aiohttp.ClientSession() as session: 

773 async with session.get(manifest_path, timeout=10) as resp: 

774 if resp.status == 200: 

775 yaml_text = await resp.text() 

776 except Exception: 

777 pass 

778 

779 if not yaml_text: 

780 return None 

781 

782 import yaml 

783 

784 try: 

785 raw = yaml.safe_load(yaml_text) 

786 except yaml.YAMLError: 

787 return None 

788 

789 safe_name = repo.replace("/", "_") 

790 manifest = SkillManifest.from_dict( 

791 raw, 

792 source=f"github:{repo}/{path}" if path else f"github:{repo}", 

793 install_path=str(self._cache_dir / safe_name), 

794 ) 

795 

796 return self._registry.register(manifest, force=force) 

797 

798 async def import_release( 

799 self, 

800 repo: str, 

801 tag: str = "latest", 

802 force: bool = False, 

803 ) -> InstallResult | None: 

804 """Import from a tagged GitHub release.""" 

805 if tag == "latest": 

806 import aiohttp 

807 

808 url = f"https://api.github.com/repos/{repo}/releases/latest" 

809 try: 

810 async with aiohttp.ClientSession() as session: 

811 async with session.get(url, timeout=10) as resp: 

812 if resp.status == 200: 

813 data = await resp.json() 

814 tag = data.get("tag_name", "main") 

815 except Exception: 

816 tag = "main" 

817 

818 return await self.import_from_github(repo, ref=tag, force=force) 

819 

820 

821# ── Unified Importer ── 

822 

823 

824class UnifiedImporter: 

825 """Single entry point for importing skills from any supported source. 

826 

827 Usage: 

828 importer = UnifiedImporter(registry) 

829 

830 # From OpenClaw community 

831 skill = await importer.import_from("openclaw:pdf-tools") 

832 

833 # From HuggingFace 

834 skill = await importer.import_from("hf://username/repo") 

835 

836 # From arbitrary GitHub 

837 skill = await importer.import_from("github://user/repo/skills/my-skill") 

838 """ 

839 

840 _PROTOCOLS = { 

841 "openclaw": "openclaw", 

842 "hf": "huggingface", 

843 "huggingface": "huggingface", 

844 "github": "github", 

845 "gh": "github", 

846 } 

847 

848 def __init__(self, registry: SkillRegistry, cache_dir: str = ""): 

849 self._registry = registry 

850 self._cache_dir = cache_dir 

851 self._openclaw = OpenClawImporter(registry, cache_dir) 

852 self._huggingface = HuggingFaceImporter(registry, cache_dir) 

853 self._github = GitHubImporter(registry, cache_dir) 

854 

855 async def import_from(self, uri: str, force: bool = False) -> InstallResult | None: 

856 """Import a skill from a URI. 

857 

858 URI formats: 

859 - 'openclaw:skill-name' OpenClaw community skill 

860 - 'hf://user/repo' HuggingFace repo 

861 - 'github://user/repo[/path]' GitHub repo 

862 - 'skill-name' Default: try OpenClaw first 

863 """ 

864 # Parse protocol 

865 if "://" in uri: 

866 protocol, rest = uri.split("://", 1) 

867 elif ":" in uri and uri.split(":")[0] in self._PROTOCOLS: 

868 protocol, rest = uri.split(":", 1) 

869 else: 

870 # Default: try OpenClaw 

871 return await self._openclaw.import_skill(uri, force=force) 

872 

873 protocol = self._PROTOCOLS.get(protocol, protocol) 

874 

875 if protocol == "openclaw": 

876 return await self._openclaw.import_skill(rest, force=force) 

877 

878 elif protocol == "huggingface": 

879 return await self._huggingface.import_from_hf(rest, force=force) 

880 

881 elif protocol == "github": 

882 parts = rest.split("/") 

883 if len(parts) >= 2: 

884 repo = f"{parts[0]}/{parts[1]}" 

885 subpath = "/".join(parts[2:]) if len(parts) > 2 else "" 

886 return await self._github.import_from_github(repo, subpath, force=force) 

887 

888 return None 

889 

890 async def list_openclaw(self, refresh: bool = False) -> list[RemoteSkill]: 

891 """List all available OpenClaw community skills.""" 

892 return await self._openclaw.list_available(refresh=refresh)