Von allein: Kategorie-Skills (2. Ebene) mitzeigen (research/blogwatcher & Co.)

Die mitgelieferten Hermes-Skills sind Kategorie-Ordner (DESCRIPTION.md +
Unter-Skills mit eigener SKILL.md) - der Walker lief nur ueber Ebene 1
und zeigte 12 statt ~40 Skills. Jetzt beide Ebenen, Unter-Skills mit
Kategorie-Badge; Detail-Endpunkt erlaubt Kategorie/Name-Pfad.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-15 10:47:53 +02:00
parent 86bd72838a
commit b0eec715c8
137 changed files with 71 additions and 54 deletions
+27 -14
View File
@@ -66,25 +66,23 @@ def list_skills() -> list[dict]:
usage_raw = {}
usage = {_norm(k): v for k, v in usage_raw.items()}
skills: list[dict] = []
for d in sorted(SKILLS_DIR.iterdir()):
md = d / "SKILL.md"
if not d.is_dir() or not md.is_file():
continue
def eintrag(md: Path, rel: str, kategorie: str | None) -> dict:
try:
fm = _frontmatter(md.read_text(encoding="utf-8", errors="replace"))
except Exception:
fm = {}
if (BUNDLED_DIR / d.name).is_dir():
if (BUNDLED_DIR / rel).is_dir():
herkunft = "bundled"
elif (REPO_DIR / d.name).is_dir():
elif (REPO_DIR / rel).is_dir():
herkunft = "repo"
else:
herkunft = "selbst"
u = usage.get(_norm(d.name)) or usage.get(_norm(fm.get("name", ""))) or {}
skills.append({
"id": d.name,
"name": fm.get("name") or d.name,
kurz = rel.split("/")[-1]
u = usage.get(_norm(kurz)) or usage.get(_norm(fm.get("name", ""))) or {}
return {
"id": rel,
"name": fm.get("name") or kurz,
"kategorie": kategorie,
"beschreibung": fm.get("description") or "",
"version": fm.get("version") or "",
"autor": fm.get("author") or "",
@@ -95,7 +93,22 @@ def list_skills() -> list[dict]:
"erstellt": u.get("created_at") or None,
"status": u.get("state") or "active",
"geaendert": md.stat().st_mtime,
})
}
skills: list[dict] = []
for d in sorted(SKILLS_DIR.iterdir()):
if not d.is_dir():
continue
md = d / "SKILL.md"
if md.is_file():
skills.append(eintrag(md, d.name, None))
continue
# Kategorie-Ordner (mitgelieferte Sammlungen wie research/, apple/): eine Ebene
# tiefer liegen die echten Skills (research/blogwatcher/SKILL.md) — je einzeln zeigen.
for sub in sorted(d.iterdir()):
smd = sub / "SKILL.md"
if sub.is_dir() and smd.is_file():
skills.append(eintrag(smd, f"{d.name}/{sub.name}", d.name))
# Selbst erstellte zuerst — das ist der Star der Ansicht.
order = {"selbst": 0, "repo": 1, "bundled": 2}
skills.sort(key=lambda s: (order[s["herkunft"]], -s["genutzt"], s["name"]))
@@ -103,8 +116,8 @@ def list_skills() -> list[dict]:
def skill_text(skill_id: str) -> str | None:
"""Voller SKILL.md-Text („wie funktioniert der Skill") — nur Namen ohne Pfad-Tricks."""
if not re.fullmatch(r"[\w.-]+", skill_id or ""):
"""Voller SKILL.md-Text („wie funktioniert der Skill") — max. Kategorie/Name, keine Pfad-Tricks."""
if not re.fullmatch(r"[\w.-]+(/[\w.-]+)?", skill_id or ""):
return None
md = SKILLS_DIR / skill_id / "SKILL.md"
if not md.is_file():