// Slash-Command Registry — scannt ~/.claude/commands/ und ~/.claude/skills/ // und liefert eine kombinierte Liste inkl. Built-in-Commands an das Frontend. use serde::Serialize; use std::fs; use std::path::PathBuf; /// Ein Slash-Command mit Metadaten #[derive(Debug, Clone, Serialize)] pub struct SlashCommand { /// Name des Commands (ohne führenden Slash) pub name: String, /// Beschreibung (erste Zeile der .md-Datei oder fest hinterlegt) pub description: String, /// Kategorie: "builtin", "custom", "skill" pub category: String, /// Herkunft: Dateipfad oder "builtin" pub source: String, } /// Liest die erste nicht-leere Zeile einer Datei als Beschreibung. /// Entfernt dabei führende Markdown-Header-Zeichen (#). fn read_first_line(path: &PathBuf) -> String { match fs::read_to_string(path) { Ok(content) => { content .lines() .find(|line| !line.trim().is_empty()) .map(|line| line.trim().trim_start_matches('#').trim().to_string()) .unwrap_or_else(|| "Keine Beschreibung".into()) } Err(_) => "Datei nicht lesbar".into(), } } /// Scannt alle verfügbaren Slash-Commands aus verschiedenen Quellen fn scan_commands() -> Vec { let mut commands: Vec = Vec::new(); // 1. Built-in-Commands (hardcoded) let builtins = vec![ ("help", "Hilfe und verfügbare Commands anzeigen"), ("clear", "Chat-Verlauf leeren"), ("compact", "Konversation kompaktieren (Token sparen)"), ("model", "KI-Modell wechseln (Sonnet/Opus/Haiku)"), ("cost", "Aktuelle Token-Kosten anzeigen"), ("doctor", "System-Diagnose und Health-Check"), ("review", "Code-Review für aktuelles Projekt"), ]; for (name, desc) in builtins { commands.push(SlashCommand { name: name.to_string(), description: desc.to_string(), category: "builtin".to_string(), source: "builtin".to_string(), }); } // Home-Verzeichnis ermitteln (ohne externe Dependency) let home = match std::env::var("HOME") { Ok(h) => PathBuf::from(h), Err(_) => { eprintln!("⚠️ HOME-Umgebungsvariable nicht gesetzt"); return commands; } }; // 2. Custom Commands aus ~/.claude/commands/*.md let commands_dir = home.join(".claude").join("commands"); if commands_dir.is_dir() { if let Ok(entries) = fs::read_dir(&commands_dir) { for entry in entries.flatten() { let path = entry.path(); if path.extension().map_or(false, |ext| ext == "md") { let name = path .file_stem() .unwrap_or_default() .to_string_lossy() .to_string(); let description = read_first_line(&path); commands.push(SlashCommand { name, description, category: "custom".to_string(), source: path.to_string_lossy().to_string(), }); } } } } // 3. Skills aus ~/.claude/skills/*/SKILL.md let skills_dir = home.join(".claude").join("skills"); if skills_dir.is_dir() { if let Ok(entries) = fs::read_dir(&skills_dir) { for entry in entries.flatten() { let skill_dir = entry.path(); if skill_dir.is_dir() { let skill_md = skill_dir.join("SKILL.md"); if skill_md.exists() { let name = skill_dir .file_name() .unwrap_or_default() .to_string_lossy() .to_string(); let description = read_first_line(&skill_md); commands.push(SlashCommand { name, description, category: "skill".to_string(), source: skill_md.to_string_lossy().to_string(), }); } } } } } // Alphabetisch sortieren (Built-ins zuerst, dann Custom, dann Skills) commands.sort_by(|a, b| { let cat_order = |c: &str| match c { "builtin" => 0, "custom" => 1, "skill" => 2, _ => 3, }; cat_order(&a.category) .cmp(&cat_order(&b.category)) .then_with(|| a.name.cmp(&b.name)) }); commands } /// Tauri-Command: Gibt alle verfügbaren Slash-Commands zurück #[tauri::command] pub fn get_slash_commands() -> Vec { scan_commands() }