236053cd7f
Kategoriserer danske regnskabs-/ERP-skills, ERP-data-afstemnings-
skill og to devops-workflows (gitea-issue-agent, kanban-workflows)
som Radix-medlemmers AI-agenter kan dele.
Struktur:
skills/<kategori>/<skill-navn>/SKILL.md
skills/<kategori>/<skill-navn>/{references,templates,scripts}/
Indekseret af index.json (genereret af scripts/rebuild_index.py).
Kør `python3 scripts/rebuild_index.py --check` i CI for at fange
synkroniseringsfejl.
125 lines
4.0 KiB
Python
125 lines
4.0 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Rebuild index.json from SKILL.md frontmatter in this repository.
|
|
|
|
Usage:
|
|
python3 scripts/rebuild_index.py
|
|
python3 scripts/rebuild_index.py --check # exit 1 if index.json is out of date
|
|
|
|
The index is the canonical machine-readable manifest of all skills in this
|
|
repository. Regenerate it whenever a SKILL.md is added, removed, or has its
|
|
frontmatter changed. Commit index.json together with the SKILL.md change.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
SKILLS_ROOT = REPO_ROOT / "skills"
|
|
INDEX_PATH = REPO_ROOT / "index.json"
|
|
|
|
FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---\n", re.DOTALL)
|
|
KEY_RE = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*):\s*(.+?)(?=\n[A-Za-z_]|\Z)", re.MULTILINE | re.DOTALL)
|
|
|
|
|
|
def _grab(frontmatter: str, key: str) -> str:
|
|
m = re.search(rf"^{key}:\s*(.+?)(?=\n[A-Za-z_]|\Z)", frontmatter, re.MULTILINE | re.DOTALL)
|
|
if not m:
|
|
return ""
|
|
val = m.group(1).strip()
|
|
# strip YAML block indicators and quotes
|
|
val = val.strip(">").strip("|").strip()
|
|
if (val.startswith('"') and val.endswith('"')) or (val.startswith("'") and val.endswith("'")):
|
|
val = val[1:-1]
|
|
return val.strip()
|
|
|
|
|
|
def _grab_tags(frontmatter: str) -> list[str]:
|
|
m = re.search(r"tags:\s*\[(.*?)\]", frontmatter, re.DOTALL)
|
|
if not m:
|
|
return []
|
|
return [t.strip().strip('"').strip("'") for t in m.group(1).split(",") if t.strip()]
|
|
|
|
|
|
def build_index() -> dict:
|
|
skills: list[dict] = []
|
|
for skill_md in sorted(SKILLS_ROOT.rglob("SKILL.md")):
|
|
rel = skill_md.relative_to(SKILLS_ROOT)
|
|
parts = rel.parts
|
|
category = parts[0] if len(parts) > 1 else "root"
|
|
name = parts[-2] if len(parts) > 1 else "root"
|
|
|
|
text = skill_md.read_text(encoding="utf-8")
|
|
fm_match = FRONTMATTER_RE.match(text)
|
|
frontmatter = fm_match.group(1) if fm_match else ""
|
|
|
|
desc = _grab(frontmatter, "description").replace("\n", " ").strip()
|
|
version = _grab(frontmatter, "version")
|
|
author = _grab(frontmatter, "author")
|
|
tags = _grab_tags(frontmatter)
|
|
|
|
all_files = sorted(
|
|
str(p.relative_to(SKILLS_ROOT))
|
|
for p in skill_md.parent.rglob("*")
|
|
if p.is_file()
|
|
)
|
|
|
|
skills.append(
|
|
{
|
|
"category": category,
|
|
"name": name,
|
|
"path": str(rel),
|
|
"description": desc,
|
|
"version": version,
|
|
"author": author,
|
|
"tags": tags,
|
|
"files": all_files,
|
|
}
|
|
)
|
|
|
|
return {
|
|
"repository": "Radix-Skills",
|
|
"version": 1,
|
|
"description": (
|
|
"Delt AI-skill-bibliotek for Radix — bruges af alle medarbejderes "
|
|
"AI-agenter (Hermes, Codex m.fl.) til dansk regnskab/ERP/data/devops."
|
|
),
|
|
"categories": sorted({s["category"] for s in skills}),
|
|
"skill_count": len(skills),
|
|
"skills": skills,
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--check", action="store_true", help="Exit 1 if index.json is stale")
|
|
args = parser.parse_args()
|
|
|
|
new_index = build_index()
|
|
new_json = json.dumps(new_index, ensure_ascii=False, indent=2) + "\n"
|
|
|
|
if args.check:
|
|
if not INDEX_PATH.exists():
|
|
print("index.json does not exist", file=sys.stderr)
|
|
return 1
|
|
current = INDEX_PATH.read_text(encoding="utf-8")
|
|
if current != new_json:
|
|
print("index.json is out of date — run scripts/rebuild_index.py", file=sys.stderr)
|
|
return 1
|
|
return 0
|
|
|
|
INDEX_PATH.write_text(new_json, encoding="utf-8")
|
|
print(
|
|
f"Wrote {INDEX_PATH.relative_to(REPO_ROOT)} with {new_index['skill_count']} skills "
|
|
f"across {len(new_index['categories'])} categories: {', '.join(new_index['categories'])}"
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|