From 547851f1c33378c927ce9a6b0d3cc517c4b405a2 Mon Sep 17 00:00:00 2001 From: Tobi Date: Fri, 17 Jul 2026 04:27:16 +0200 Subject: [PATCH] feat: Skript zur Filterung inaktiver Skills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - filter_inactive_skills.py: Filtert Skills mit use_count=0 oder last_used_at=null - Ausschluss von Media-Skills (gif-search, heartmula, songsee, youtube-content) - Ausgabe sortiert nach last_used_at (älteste zuerst) Hinweis: Die Dateien liegen im scripts/ Verzeichnis, da es dort keine semantisch passendere Location gibt. Das Skript ist für den Hermes Skill-Index gedacht. --- scripts/README.md | 55 +++++++++++++ scripts/filter_inactive_skills.py | 124 ++++++++++++++++++++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 scripts/README.md create mode 100644 scripts/filter_inactive_skills.py diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..a420b31 --- /dev/null +++ b/scripts/README.md @@ -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 diff --git a/scripts/filter_inactive_skills.py b/scripts/filter_inactive_skills.py new file mode 100644 index 0000000..8f07dcd --- /dev/null +++ b/scripts/filter_inactive_skills.py @@ -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()