Auftragsbuch: 'feature/skill-inactive-filter' angenommen (Ein-Klick-Gate)

This commit is contained in:
Auftragsbuch
2026-07-17 12:14:22 +02:00
2 changed files with 179 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
# Skript zur Filterung inaktiver Skills
## Zweck
Das Skript `filter_inactive_skills.py` identifiziert Skills aus dem Hermes Skill-Index, die **nicht** genutzt wurden und **keine** Media-Skills sind.
## Kriterien
Ein Skill gilt als inaktiv, wenn **mindestens eines** der folgenden Kriterien zutrifft:
- `use_count == 0`
- `last_used_at == null` (nie genutzt)
Media-Skills (die im `~/.hermes/skills/media/`-Verzeichnis liegen) werden **ausgeschlossen**:
- gif-search
- heartmula
- songsee
- youtube-content
## Ausgabe
Die Ergebnisse werden als Liste sortiert nach `last_used_at` (älteste Nutzung zuerst) ausgegeben. Skills, die **nie** genutzt wurden (`last_used_at == null`), erscheinen am Ende.
Beispiel-Ausgabe:
```
Inaktive Skills (use_count=0 oder last_used_at=null, ohne Media-Skills): 45
--------------------------------------------------------------------------------
airtable use_count= 0 last_used_at=nie
apple-notes use_count= 0 last_used_at=nie
...
--------------------------------------------------------------------------------
```
## Verwendung
```bash
python3 filter_inactive_skills.py
```
## Dateistruktur
```
t_47fb5aaa/
├── filter_inactive_skills.py # Hauptskript
└── README.md # Diese Dokumentation
```
## Datenquelle
- Skill-Metadaten: `~/.hermes/skills/.usage.json`
- Media-Skills: `~/.hermes/skills/media/`
## Historie
- 2026-07-17: Erstellt im Rahmen von Kanban-Aufgabe t_47fb5aaa
+124
View File
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""
Skript zur Filterung inaktiver Skills.
Filtert Skills aus ~/.hermes/skills/.usage.json, die:
- use_count == 0 ODER last_used_at == null
- NICHT im media/ Verzeichnis liegen (Media-Skills werden ausgeschlossen)
Ausgabe: Liste sortiert nach last_used_at (älteste zuerst), nur Skills mit use_count 0.
"""
import json
from pathlib import Path
from datetime import datetime
def load_usage_data(path: Path) -> dict:
"""Lädt die .usage.json-Datei."""
with open(path, 'r', encoding='utf-8') as f:
return json.load(f)
def get_media_skills(skills_dir: Path) -> set:
"""Ermittelt alle Media-Skills aus dem media/ Verzeichnis."""
media_path = skills_dir / 'media'
if not media_path.is_dir():
return set()
media_skills = set()
for item in media_path.iterdir():
if item.is_dir():
media_skills.add(item.name)
return media_skills
def is_media_skill(skill_name: str, media_skills: set) -> bool:
"""Prüft, ob ein Skill ein Media-Skill ist."""
return skill_name in media_skills
def parse_datetime(dt_str: str | None) -> datetime | None:
"""Parsed einen datetime-string im ISO-Format (mit timezone)."""
if dt_str is None:
return None
# Entferne den timezone-Offset für datetime.fromisoformat (Python 3.11+ supportet +00:00)
# Aber da wir nur sortieren, reicht es, den String direkt zu nutzen
try:
return datetime.fromisoformat(dt_str)
except ValueError:
return None
def filter_inactive_skills(usage: dict, media_skills: set) -> list:
"""
Filtert inaktive Skills.
Kriterien:
- use_count == 0 ODER last_used_at == null
- NICHT in media_skills
Gibt Liste zurück mit (skill_name, use_count, last_used_at)
"""
inactive = []
for skill_name, data in usage.items():
if is_media_skill(skill_name, media_skills):
continue
use_count = data.get('use_count', 0)
last_used_at = data.get('last_used_at')
# Filter: use_count == 0 ODER last_used_at == null
if use_count == 0 or last_used_at is None:
inactive.append({
'name': skill_name,
'use_count': use_count,
'last_used_at': last_used_at
})
# Sortieren nach last_used_at (älteste zuerst, None am Ende)
def sort_key(item):
dt = parse_datetime(item['last_used_at'])
if dt is None:
return (1, '') # None am Ende
return (0, dt.isoformat())
inactive.sort(key=sort_key)
return inactive
def print_results(skills: list) -> None:
"""Gibt die Ergebnisse lesbar aus."""
if not skills:
print("Keine inaktiven Skills gefunden.")
return
print(f"Inaktive Skills (use_count=0 oder last_used_at=null, ohne Media-Skills): {len(skills)}\n")
print("-" * 80)
for skill in skills:
name = skill['name']
use_count = skill['use_count']
last_used = skill['last_used_at'] if skill['last_used_at'] else 'nie'
print(f" {name:30} use_count={use_count:3} last_used_at={last_used}")
print("-" * 80)
def main():
skills_dir = Path.home() / '.hermes' / 'skills'
usage_path = skills_dir / '.usage.json'
if not usage_path.exists():
print(f"Fehler: {usage_path} nicht gefunden.")
return
usage = load_usage_data(usage_path)
media_skills = get_media_skills(skills_dir)
inactive_skills = filter_inactive_skills(usage, media_skills)
print_results(inactive_skills)
if __name__ == '__main__':
main()