All checks were successful
Build AppImage / build (push) Successful in 7m51s
- KB-Hints werden automatisch in jeden Claude-Prompt injiziert - SQL-Queries berücksichtigen jetzt Priority (DESC) - Voice-zu-Claude-Pipeline: Sprache → Transkription → Claude → TTS - Hook-System feuert echte Events (SessionStart, Pre/PostToolUse) - Pattern-Detektion bei Tool-Fehlern aktiviert - Slash-Command Autocomplete mit CommandPalette - Updater abgesichert: Lock-Datei, Prozess-Guard, Bestätigungs-Dialog - ROADMAP.md und CHANGELOG.md aktualisiert Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
271 lines
8.3 KiB
Rust
271 lines
8.3 KiB
Rust
// 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<Self> {
|
|
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<String, Vec<HookConfig>>,
|
|
pub executions: Vec<HookExecution>,
|
|
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<String> {
|
|
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<HookConfig> {
|
|
self.hooks.values().flatten().cloned().collect()
|
|
}
|
|
|
|
pub fn recent_executions(&self, limit: usize) -> Vec<HookExecution> {
|
|
let start = self.executions.len().saturating_sub(limit);
|
|
self.executions[start..].to_vec()
|
|
}
|
|
}
|
|
|
|
pub type HookState = Arc<Mutex<HookManager>>;
|
|
|
|
// ============ Tauri Commands ============
|
|
|
|
#[tauri::command]
|
|
pub async fn list_hooks(state: tauri::State<'_, HookState>) -> Result<Vec<HookConfig>, 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<bool, String> {
|
|
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<usize>,
|
|
) -> Result<Vec<HookExecution>, 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<Vec<String>, 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)
|
|
}
|