import os
import yaml
from typing import List, Dict
def load_skills_from_directory(directory: str) -> List[Dict]:
skills = []
if not os.path.exists(directory):
return skills
for item in os.listdir(directory):
item_path = os.path.join(directory, item)
if os.path.isdir(item_path):
skill_md_path = os.path.join(item_path, "SKILL.md")
if os.path.exists(skill_md_path):
with open(skill_md_path, "r") as f:
content = f.read()
# Simple frontmatter parser
if content.startswith("---"):
parts = content.split("---", 2)
if len(parts) >= 3:
try:
frontmatter = yaml.safe_load(parts[1])
body = parts[2].strip()
skill_def = frontmatter
# Split body into Documentation and AI Instructions
# Convention: Use common headers or tags to denote the start of AI instructions
human_docs = body
ai_instructions = ""
separators = ["# AI Instructions", "# Intelligence Protocol", "<!-- ai-instructions -->"]
for sep in separators:
if sep in body:
split_parts = body.split(sep, 1)
human_docs = split_parts[0].strip()
ai_instructions = split_parts[1].strip()
break
skill_def["preview_markdown"] = human_docs
skill_def["system_prompt"] = ai_instructions
# Ensure metadata exists
if "extra_metadata" not in skill_def:
skill_def["extra_metadata"] = {}
# Handle common OpenClaw metadata locations
if "emoji" in frontmatter and "emoji" not in skill_def["extra_metadata"]:
skill_def["extra_metadata"]["emoji"] = frontmatter["emoji"]
# Validation: Ensure features match platform profiles
from app.core.orchestration.profiles import get_allowed_features
allowed = get_allowed_features()
skill_features = skill_def.get("features", [])
if not all(f in allowed for f in skill_features):
invalid = [f for f in skill_features if f not in allowed]
# Only log warning, don't crash, to remain developer-friendly
print(f"⚠️ Warning in {skill_def.get('name')}: features {invalid} are not registered in profiles.py. They will use the default 'chat' behavior.")
skills.append(skill_def)
except Exception as e:
print(f"Error parsing {skill_md_path}: {e}")
return skills