#!/usr/bin/env python3 """Generate index.json from the SKILL.md files under skills/. Previously this logic lived as an inline heredoc inside .github/workflows/update-index.yml with a hand-rolled regex YAML parser that truncated 604 of 817 descriptions. It lives here now so it is testable outside CI and shares one PyYAML-backed parser with every other tool. Usage: python tools/generate-index.py # write index.json python tools/generate-index.py --check # verify index.json is current (CI) """ from __future__ import annotations import argparse import json import os import sys from datetime import datetime, timezone sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from skill_frontmatter import description_of, iter_skill_dirs, load_frontmatter, FrontmatterError INDEX_VERSION = "1.1.0" REPOSITORY = "https://github.com/mukul975/Anthropic-Cybersecurity-Skills" DEFAULT_DOMAIN = "cybersecurity" def build_index(skills_dir: str = "skills") -> tuple[dict, list[str]]: """Build the index payload. Returns (index, errors).""" skills = [] errors = [] for slug, skill_dir in iter_skill_dirs(skills_dir): try: frontmatter = load_frontmatter(os.path.join(skill_dir, "SKILL.md")) except FrontmatterError as exc: errors.append(f"{slug}: {exc}") continue description = description_of(frontmatter) if not description: errors.append(f"{slug}: empty description") skills.append({ "name": slug, "description": description, "domain": frontmatter.get("domain") or DEFAULT_DOMAIN, "path": f"{skills_dir}/{slug}", }) index = { "version": INDEX_VERSION, "generated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), "repository": REPOSITORY, "domain": DEFAULT_DOMAIN, "total_skills": len(skills), "skills": skills, } return index, errors def _comparable(index: dict) -> str: """Serialize an index ignoring generated_at, so --check tolerates a re-run.""" return json.dumps({k: v for k, v in index.items() if k != "generated_at"}, sort_keys=True) def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--check", action="store_true", help="verify index.json matches the skills tree; do not write") parser.add_argument("--skills-dir", default="skills") parser.add_argument("--out", default="index.json") args = parser.parse_args() if not os.path.isdir(args.skills_dir): print(f"ERROR: '{args.skills_dir}' not found. Run from the repository root.") return 1 index, errors = build_index(args.skills_dir) for error in errors: print(f"ERROR {error}") if errors: print(f"\n{len(errors)} skill(s) could not be indexed.") return 1 if args.check: if not os.path.isfile(args.out): print(f"ERROR: {args.out} is missing. Run: python tools/generate-index.py") return 1 with open(args.out, encoding="utf-8") as handle: current = json.load(handle) if _comparable(current) != _comparable(index): print(f"ERROR: {args.out} is out of date. Run: python tools/generate-index.py") return 1 print(f"OK: {args.out} is up to date ({index['total_skills']} skills)") return 0 with open(args.out, "w", encoding="utf-8") as handle: json.dump(index, handle, separators=(",", ":")) print(f"Updated {args.out}: {index['total_skills']} skills") return 0 if __name__ == "__main__": sys.exit(main())