Coverage for agentos/marketplace/importer.py: 0%
241 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 12:20 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 12:20 +0800
1"""
2Marketplace Importer — Import skills from external sources (OpenClaw, HuggingFace, GitHub).
4OpenClaw Community: https://github.com/openclaw/skills
5 - skill.yaml → SkillManifest (openclaw format)
6 - Auto-detect format, convert, register
8Usage:
9 from agentos.marketplace.importer import OpenClawImporter
10 importer = OpenClawImporter(registry)
11 skill = await importer.import_skill("pdf-tools")
12"""
14from __future__ import annotations
16import asyncio
17from collections.abc import Callable
18from dataclasses import dataclass, field
19from pathlib import Path
21from agentos.marketplace.manifest import SkillManifest
22from agentos.marketplace.registry import InstallResult, SkillRegistry
24# ── OpenClaw Importer ──
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"
31@dataclass
32class RemoteSkill:
33 """Skill metadata discovered from a remote source."""
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"
46class OpenClawImporter:
47 """Import skills from the OpenClaw community skill store.
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 """
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)
62 self._catalog: list[RemoteSkill] = []
63 self._fetch_fn: Callable[[str], str] | None = None # Injection point for testing
65 # ── Catalog ──
67 async def list_available(self, refresh: bool = False) -> list[RemoteSkill]:
68 """List all available OpenClaw community skills.
70 Returns cached catalog unless refresh=True.
71 """
72 if self._catalog and not refresh:
73 return self._catalog
75 skills = []
77 # Try fetching directory listing from the raw GitHub API
78 try:
79 import aiohttp
80 except ImportError:
81 return await self._list_fallback()
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()
93 entries = await resp.json()
94 for entry in entries:
95 if entry.get("type") != "dir":
96 continue
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", "")
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 )
118 except Exception:
119 return await self._list_fallback()
121 self._catalog = skills
122 return skills
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
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
537 return yaml.safe_load(text) or {}
538 except Exception:
539 pass
540 return {}
542 # ── Import ──
544 async def import_skill(self, name: str, force: bool = False) -> InstallResult | None:
545 """Import a single skill from OpenClaw by name.
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()
557 # Find skill
558 skill_ref = None
559 for s in self._catalog:
560 if s.name == name:
561 skill_ref = s
562 break
564 if not skill_ref:
565 return None
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)
571 if not yaml_text:
572 return None
574 # Parse as OpenClaw format
575 import yaml
577 try:
578 raw = yaml.safe_load(yaml_text)
579 except yaml.YAMLError:
580 return None
582 if not raw:
583 return None
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 )
593 # Register
594 return self._registry.register(manifest, force=force)
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()
601 results = []
602 semaphore = asyncio.Semaphore(5) # Limit concurrent fetches
604 async def _import_one(skill: RemoteSkill):
605 async with semaphore:
606 return await self.import_skill(skill.name)
608 tasks = [_import_one(s) for s in self._catalog[:max_skills]]
609 raw_results = await asyncio.gather(*tasks, return_exceptions=True)
611 for r in raw_results:
612 if isinstance(r, Exception):
613 pass
614 elif r is not None:
615 results.append(r)
617 return results
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()
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
635 # ── Internal ──
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)
642 try:
643 import aiohttp
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
654 # Fallback: urllib
655 try:
656 import urllib.request
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
664 return ""
667# ── HuggingFace Importer ──
670class HuggingFaceImporter:
671 """Import skills from HuggingFace.co skill repositories.
673 Flow:
674 hf://username/skill-repo → download → parse skill.yaml → register
675 """
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)
686 async def import_from_hf(self, repo_id: str, force: bool = False) -> InstallResult | None:
687 """Import a skill from HuggingFace repo.
689 Args:
690 repo_id: e.g. 'username/agentos-skill-translator'
691 """
692 import aiohttp
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
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
715 if not yaml_text:
716 return None
718 import yaml
720 try:
721 raw = yaml.safe_load(yaml_text)
722 except yaml.YAMLError:
723 return None
725 manifest = SkillManifest.from_dict(
726 raw,
727 source=f"huggingface:{repo_id}",
728 install_path=str(self._cache_dir / repo_id.replace("/", "_")),
729 )
731 return self._registry.register(manifest, force=force)
734# ── GitHub Importer ──
737class GitHubImporter:
738 """Import skills from arbitrary GitHub repositories.
740 Flow:
741 github://user/repo/path → download skill.yaml → parse → register
742 """
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)
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.
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"
768 yaml_text = ""
769 import aiohttp
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
779 if not yaml_text:
780 return None
782 import yaml
784 try:
785 raw = yaml.safe_load(yaml_text)
786 except yaml.YAMLError:
787 return None
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 )
796 return self._registry.register(manifest, force=force)
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
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"
818 return await self.import_from_github(repo, ref=tag, force=force)
821# ── Unified Importer ──
824class UnifiedImporter:
825 """Single entry point for importing skills from any supported source.
827 Usage:
828 importer = UnifiedImporter(registry)
830 # From OpenClaw community
831 skill = await importer.import_from("openclaw:pdf-tools")
833 # From HuggingFace
834 skill = await importer.import_from("hf://username/repo")
836 # From arbitrary GitHub
837 skill = await importer.import_from("github://user/repo/skills/my-skill")
838 """
840 _PROTOCOLS = {
841 "openclaw": "openclaw",
842 "hf": "huggingface",
843 "huggingface": "huggingface",
844 "github": "github",
845 "gh": "github",
846 }
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)
855 async def import_from(self, uri: str, force: bool = False) -> InstallResult | None:
856 """Import a skill from a URI.
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)
873 protocol = self._PROTOCOLS.get(protocol, protocol)
875 if protocol == "openclaw":
876 return await self._openclaw.import_skill(rest, force=force)
878 elif protocol == "huggingface":
879 return await self._huggingface.import_from_hf(rest, force=force)
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)
888 return None
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)