mirror of
https://github.com/apify/agent-skills.git
synced 2026-09-14 20:00:06 +08:00
71f4b292b6
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
136 lines
4.2 KiB
Python
136 lines
4.2 KiB
Python
#!/usr/bin/env -S uv run
|
|
# /// script
|
|
# requires-python = ">=3.10"
|
|
# dependencies = []
|
|
# ///
|
|
"""Generate AGENTS.md from AGENTS_TEMPLATE.md and SKILL.md frontmatter.
|
|
|
|
Also validates that marketplace.json is in sync with discovered skills.
|
|
|
|
Usage:
|
|
uv run scripts/generate_agents.py
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
TEMPLATE_PATH = ROOT / "scripts" / "AGENTS_TEMPLATE.md"
|
|
OUTPUT_PATH = ROOT / "agents" / "AGENTS.md"
|
|
MARKETPLACE_PATH = ROOT / ".claude-plugin" / "marketplace.json"
|
|
|
|
|
|
def load_template() -> str:
|
|
return TEMPLATE_PATH.read_text(encoding="utf-8")
|
|
|
|
|
|
def parse_frontmatter(text: str) -> dict[str, str]:
|
|
"""Parse a minimal YAML-ish frontmatter block without external deps."""
|
|
match = re.search(r"^---\s*\n(.*?)\n---\s*", text, re.DOTALL)
|
|
if not match:
|
|
return {}
|
|
data: dict[str, str] = {}
|
|
for line in match.group(1).splitlines():
|
|
if ":" not in line:
|
|
continue
|
|
key, value = line.split(":", 1)
|
|
data[key.strip()] = value.strip()
|
|
return data
|
|
|
|
|
|
def collect_skills() -> list[dict[str, str]]:
|
|
skills: list[dict[str, str]] = []
|
|
for skill_md in ROOT.glob("skills/*/SKILL.md"):
|
|
meta = parse_frontmatter(skill_md.read_text(encoding="utf-8"))
|
|
name = meta.get("name")
|
|
description = meta.get("description")
|
|
if not name or not description:
|
|
continue
|
|
skills.append(
|
|
{
|
|
"name": name,
|
|
"description": description,
|
|
"path": str(skill_md.parent.relative_to(ROOT)),
|
|
}
|
|
)
|
|
# Keep deterministic order for consistent output
|
|
return sorted(skills, key=lambda s: s["name"].lower())
|
|
|
|
|
|
def render(template: str, skills: list[dict[str, str]]) -> str:
|
|
"""Very small Mustache-like renderer that only supports a single skills loop."""
|
|
def repl(match: re.Match[str]) -> str:
|
|
block = match.group(1).strip("\n")
|
|
rendered_blocks = []
|
|
for skill in skills:
|
|
rendered = (
|
|
block.replace("{{name}}", skill["name"])
|
|
.replace("{{description}}", skill["description"])
|
|
.replace("{{path}}", skill["path"])
|
|
)
|
|
rendered_blocks.append(rendered)
|
|
return "\n".join(rendered_blocks)
|
|
|
|
# Render loop blocks
|
|
content = re.sub(r"{{#skills}}(.*?){{/skills}}", repl, template, flags=re.DOTALL)
|
|
return content
|
|
|
|
|
|
def validate_marketplace(skills: list[dict[str, str]]) -> list[str]:
|
|
"""Validate marketplace.json against discovered skills. Returns error messages."""
|
|
if not MARKETPLACE_PATH.exists():
|
|
return [f"marketplace.json not found at {MARKETPLACE_PATH}"]
|
|
|
|
marketplace = json.loads(MARKETPLACE_PATH.read_text(encoding="utf-8"))
|
|
plugins = marketplace.get("plugins", [])
|
|
errors: list[str] = []
|
|
|
|
# Every plugin with skills should have at least one SKILL.md
|
|
for plugin in plugins:
|
|
source = plugin.get("source", "").lstrip("./")
|
|
plugin_skills = [s for s in skills if s["path"].startswith(source)]
|
|
if not plugin_skills:
|
|
errors.append(
|
|
f"Plugin '{plugin['name']}' at '{source}' has no SKILL.md files"
|
|
)
|
|
|
|
# Every discovered skill should be covered by a plugin
|
|
for skill in skills:
|
|
found = any(
|
|
skill["path"].startswith(p.get("source", "").lstrip("./"))
|
|
for p in plugins
|
|
)
|
|
if not found:
|
|
errors.append(
|
|
f"Skill '{skill['name']}' at '{skill['path']}' is not covered by any plugin"
|
|
)
|
|
|
|
return errors
|
|
|
|
|
|
def main() -> None:
|
|
template = load_template()
|
|
skills = collect_skills()
|
|
output = render(template, skills)
|
|
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
OUTPUT_PATH.write_text(output, encoding="utf-8")
|
|
print(f"Wrote {OUTPUT_PATH} with {len(skills)} skills.")
|
|
|
|
# Validate marketplace.json
|
|
errors = validate_marketplace(skills)
|
|
if errors:
|
|
print("\nMarketplace.json validation errors:", file=sys.stderr)
|
|
for error in errors:
|
|
print(f" - {error}", file=sys.stderr)
|
|
sys.exit(1)
|
|
print("Marketplace.json validation passed.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|