// Claude Desktop — Hook-System // Zentraler Dispatcher + Audit-Log fuer automatische Aktionen // (SessionStart, PreToolUse, PostToolUse, BeforeCompacting, AfterCompacting) use serde::{Deserialize, Serialize}; use std::sync::{Arc, Mutex}; use std::collections::HashMap; use tauri::{AppHandle, Emitter}; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum HookEvent { SessionStart, PreToolUse, PostToolUse, BeforeCompacting, AfterCompacting, ContextFailure, AgentStarted, } impl HookEvent { pub fn as_str(&self) -> &'static str { match self { HookEvent::SessionStart => "SessionStart", HookEvent::PreToolUse => "PreToolUse", HookEvent::PostToolUse => "PostToolUse", HookEvent::BeforeCompacting => "BeforeCompacting", HookEvent::AfterCompacting => "AfterCompacting", HookEvent::ContextFailure => "ContextFailure", HookEvent::AgentStarted => "AgentStarted", } } pub fn from_str(s: &str) -> Option { match s { "SessionStart" => Some(HookEvent::SessionStart), "PreToolUse" => Some(HookEvent::PreToolUse), "PostToolUse" => Some(HookEvent::PostToolUse), "BeforeCompacting" => Some(HookEvent::BeforeCompacting), "AfterCompacting" => Some(HookEvent::AfterCompacting), "ContextFailure" => Some(HookEvent::ContextFailure), "AgentStarted" => Some(HookEvent::AgentStarted), _ => None, } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HookConfig { pub name: String, pub event: String, pub enabled: bool, pub description: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HookExecution { pub timestamp: String, pub event: String, pub hook_name: String, pub duration_ms: u64, pub success: bool, pub summary: String, } /// Hook-Manager — haelt Registry und Ausfuehrungs-Log pub struct HookManager { pub hooks: HashMap>, pub executions: Vec, pub max_log_size: usize, } impl Default for HookManager { fn default() -> Self { let mut mgr = HookManager { hooks: HashMap::new(), executions: Vec::new(), max_log_size: 500, }; mgr.register_builtin_hooks(); mgr } } impl HookManager { /// Eingebaute Hooks registrieren fn register_builtin_hooks(&mut self) { let builtins = vec![ HookConfig { name: "load-sticky-context".into(), event: "SessionStart".into(), enabled: true, description: "Laedt Sticky-Context bei Session-Start".into(), }, HookConfig { name: "inject-knowledge-hints".into(), event: "PreToolUse".into(), enabled: true, description: "Injiziert relevante KB-Eintraege vor Tool-Ausfuehrung".into(), }, HookConfig { name: "save-failure-pattern".into(), event: "PostToolUse".into(), enabled: true, description: "Speichert Fehler-Pattern nach fehlgeschlagenen Tools".into(), }, HookConfig { name: "extract-critical-context".into(), event: "BeforeCompacting".into(), enabled: true, description: "Extrahiert kritischen Kontext vor Compacting".into(), }, HookConfig { name: "reinject-context".into(), event: "AfterCompacting".into(), enabled: true, description: "Injiziert Sticky+Project-Context nach Compacting".into(), }, ]; for hook in builtins { self.hooks .entry(hook.event.clone()) .or_default() .push(hook); } } pub fn fire(&mut self, event: &HookEvent, summary: String) -> Vec { let event_name = event.as_str().to_string(); let mut fired_names = Vec::new(); if let Some(hooks) = self.hooks.get(&event_name) { for hook in hooks.iter().filter(|h| h.enabled) { fired_names.push(hook.name.clone()); let execution = HookExecution { timestamp: chrono::Utc::now().to_rfc3339(), event: event_name.clone(), hook_name: hook.name.clone(), duration_ms: 0, success: true, summary: summary.clone(), }; self.executions.push(execution); if self.executions.len() > self.max_log_size { self.executions.remove(0); } } } fired_names } pub fn set_enabled(&mut self, event: &str, hook_name: &str, enabled: bool) -> bool { if let Some(hooks) = self.hooks.get_mut(event) { for hook in hooks.iter_mut() { if hook.name == hook_name { hook.enabled = enabled; return true; } } } false } pub fn list_all(&self) -> Vec { self.hooks.values().flatten().cloned().collect() } pub fn recent_executions(&self, limit: usize) -> Vec { let start = self.executions.len().saturating_sub(limit); self.executions[start..].to_vec() } } pub type HookState = Arc>; // ============ Tauri Commands ============ #[tauri::command] pub async fn list_hooks(state: tauri::State<'_, HookState>) -> Result, String> { let mgr = state.lock().map_err(|e| e.to_string())?; Ok(mgr.list_all()) } #[tauri::command] pub async fn set_hook_enabled( state: tauri::State<'_, HookState>, event: String, hook_name: String, enabled: bool, ) -> Result { let mut mgr = state.lock().map_err(|e| e.to_string())?; Ok(mgr.set_enabled(&event, &hook_name, enabled)) } #[tauri::command] pub async fn get_hook_executions( state: tauri::State<'_, HookState>, limit: Option, ) -> Result, String> { let mgr = state.lock().map_err(|e| e.to_string())?; Ok(mgr.recent_executions(limit.unwrap_or(100))) } #[tauri::command] pub async fn fire_hook( app: AppHandle, state: tauri::State<'_, HookState>, event: String, summary: String, ) -> Result, String> { let hook_event = HookEvent::from_str(&event) .ok_or_else(|| format!("Unbekanntes Hook-Event: {}", event))?; let fired = { let mut mgr = state.lock().map_err(|e| e.to_string())?; mgr.fire(&hook_event, summary.clone()) }; // Event ans Frontend senden (fuer Live-Log im UI) let _ = app.emit( "hook-fired", serde_json::json!({ "event": event, "hooks": fired, "summary": summary, }), ); // Spezifische Hook-Events dispatchen — Frontend kann gezielt darauf reagieren // Summary wird als JSON-Payload durchgereicht (Tool-Name, Argumente, Ergebnis etc.) match hook_event { HookEvent::SessionStart => { let _ = app.emit("hook-session-start", serde_json::json!({ "hooks": fired, "payload": summary, })); } HookEvent::PreToolUse => { let _ = app.emit("hook-pre-tool-use", serde_json::json!({ "hooks": fired, "payload": summary, })); } HookEvent::PostToolUse => { let _ = app.emit("hook-post-tool-use", serde_json::json!({ "hooks": fired, "payload": summary, })); } HookEvent::BeforeCompacting => { let _ = app.emit("hook-before-compacting", serde_json::json!({ "hooks": fired, "payload": summary, })); } HookEvent::AfterCompacting => { let _ = app.emit("hook-after-compacting", serde_json::json!({ "hooks": fired, "payload": summary, })); } // ContextFailure + AgentStarted: aktuell kein eigenes Frontend-Event noetig _ => {} } Ok(fired) }