// Claude Desktop — SQLite Datenbankschicht // Persistiert Guard-Rails, Audit-Log, Memory und Einstellungen use rusqlite::{params, Connection, Result as SqlResult}; use std::path::Path; use std::sync::{Arc, Mutex}; use tauri::{AppHandle, Manager}; use crate::audit::{AuditAction, AuditCategory, AuditEntry, AuditStats}; use crate::guard::{Permission, PermissionAction, PermissionType}; use crate::memory::{ContextCategory, MemoryEntry, Pattern}; /// Eine Claude-Session #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Session { pub id: String, pub claude_session_id: Option, pub title: String, pub working_dir: Option, pub message_count: i64, pub token_input: i64, pub token_output: i64, pub cost_usd: f64, pub status: String, pub created_at: String, pub updated_at: String, pub last_message: Option, } /// Datenbank-Wrapper pub struct Database { conn: Connection, } /// Datenbank-Statistiken #[derive(Debug, serde::Serialize)] pub struct DbStats { pub permissions: usize, pub audit_entries: usize, pub memory_entries: usize, pub patterns: usize, pub db_size_kb: u64, } impl Database { /// Öffnet oder erstellt die Datenbank pub fn open(path: &Path) -> SqlResult { let conn = Connection::open(path)?; // WAL-Modus für bessere Performance conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;")?; let db = Self { conn }; db.create_tables()?; Ok(db) } /// Schema erstellen fn create_tables(&self) -> SqlResult<()> { self.conn.execute_batch( " -- Guard-Rails Permissions CREATE TABLE IF NOT EXISTS permissions ( id TEXT PRIMARY KEY, pattern TEXT NOT NULL, tool TEXT, path_pattern TEXT, action TEXT NOT NULL DEFAULT 'allow', created_at TEXT NOT NULL, use_count INTEGER DEFAULT 0, last_used TEXT ); -- Audit-Log CREATE TABLE IF NOT EXISTS audit_log ( id TEXT PRIMARY KEY, timestamp TEXT NOT NULL, category TEXT NOT NULL, action TEXT NOT NULL, item_id TEXT NOT NULL, item_name TEXT NOT NULL, old_value TEXT, new_value TEXT, reason TEXT, auto_corrected INTEGER DEFAULT 0, session_id TEXT ); CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log(timestamp DESC); CREATE INDEX IF NOT EXISTS idx_audit_category ON audit_log(category); -- Memory-Einträge CREATE TABLE IF NOT EXISTS memory ( id TEXT PRIMARY KEY, category TEXT NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL, sticky INTEGER DEFAULT 0, auto_load INTEGER DEFAULT 0, last_used TEXT, use_count INTEGER DEFAULT 0 ); CREATE INDEX IF NOT EXISTS idx_memory_category ON memory(category); CREATE INDEX IF NOT EXISTS idx_memory_sticky ON memory(sticky) WHERE sticky = 1; -- Patterns (Vorgehensweisen) CREATE TABLE IF NOT EXISTS patterns ( id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT, trigger_text TEXT, old_approach TEXT, new_approach TEXT, reason TEXT, occurrence_count INTEGER DEFAULT 1, auto_corrected INTEGER DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); -- Sessions (Claude-Konversationen) CREATE TABLE IF NOT EXISTS sessions ( id TEXT PRIMARY KEY, claude_session_id TEXT, title TEXT NOT NULL, working_dir TEXT, message_count INTEGER DEFAULT 0, token_input INTEGER DEFAULT 0, token_output INTEGER DEFAULT 0, cost_usd REAL DEFAULT 0, status TEXT NOT NULL DEFAULT 'active', created_at TEXT NOT NULL, updated_at TEXT NOT NULL, last_message TEXT ); CREATE INDEX IF NOT EXISTS idx_sessions_updated ON sessions(updated_at DESC); CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status); -- Einstellungen (Key-Value) CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT NOT NULL ); ", )?; Ok(()) } // ============ Permissions ============ /// Speichert eine Permission pub fn save_permission(&self, perm: &Permission) -> SqlResult<()> { self.conn.execute( "INSERT OR REPLACE INTO permissions (id, pattern, tool, path_pattern, action, created_at, use_count, last_used) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", params![ perm.id, perm.pattern, perm.tool, perm.path_pattern, format!("{:?}", perm.action).to_lowercase(), perm.created_at, perm.use_count, perm.last_used, ], )?; Ok(()) } /// Lädt alle permanenten Permissions pub fn load_permissions(&self) -> SqlResult> { let mut stmt = self.conn.prepare( "SELECT id, pattern, tool, path_pattern, action, created_at, use_count, last_used FROM permissions" )?; let perms = stmt.query_map([], |row| { let action_str: String = row.get(4)?; let action = match action_str.as_str() { "deny" => PermissionAction::Deny, _ => PermissionAction::Allow, }; Ok(Permission { id: row.get(0)?, pattern: row.get(1)?, tool: row.get(2)?, path_pattern: row.get(3)?, permission_type: PermissionType::Permanent, action, created_at: row.get(5)?, use_count: row.get(6)?, last_used: row.get(7)?, }) })?.collect::>>()?; Ok(perms) } /// Löscht eine Permission pub fn delete_permission(&self, id: &str) -> SqlResult<()> { self.conn.execute("DELETE FROM permissions WHERE id = ?1", params![id])?; Ok(()) } // ============ Audit-Log ============ /// Speichert einen Audit-Eintrag pub fn save_audit_entry(&self, entry: &AuditEntry) -> SqlResult<()> { self.conn.execute( "INSERT INTO audit_log (id, timestamp, category, action, item_id, item_name, old_value, new_value, reason, auto_corrected, session_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", params![ entry.id, entry.timestamp, format!("{:?}", entry.category).to_lowercase(), format!("{:?}", entry.action).to_lowercase(), entry.item_id, entry.item_name, entry.old_value.as_ref().map(|v| v.to_string()), entry.new_value.as_ref().map(|v| v.to_string()), entry.reason, entry.auto_corrected as i32, entry.session_id, ], )?; Ok(()) } /// Lädt die letzten N Audit-Einträge pub fn load_audit_log(&self, limit: usize) -> SqlResult> { let mut stmt = self.conn.prepare( "SELECT id, timestamp, category, action, item_id, item_name, old_value, new_value, reason, auto_corrected, session_id FROM audit_log ORDER BY timestamp DESC LIMIT ?1" )?; let entries = stmt.query_map(params![limit as i64], |row| { let cat_str: String = row.get(2)?; let act_str: String = row.get(3)?; let old_val: Option = row.get(6)?; let new_val: Option = row.get(7)?; let auto_corr: i32 = row.get(9)?; Ok(AuditEntry { id: row.get(0)?, timestamp: row.get(1)?, category: parse_audit_category(&cat_str), action: parse_audit_action(&act_str), item_id: row.get(4)?, item_name: row.get(5)?, old_value: old_val.and_then(|s| serde_json::from_str(&s).ok()), new_value: new_val.and_then(|s| serde_json::from_str(&s).ok()), reason: row.get(8)?, auto_corrected: auto_corr != 0, session_id: row.get(10)?, }) })?.collect::>>()?; Ok(entries) } /// Audit-Statistiken pub fn audit_stats(&self) -> SqlResult { let total: usize = self.conn.query_row( "SELECT COUNT(*) FROM audit_log", [], |row| row.get(0) )?; let auto_corrected: usize = self.conn.query_row( "SELECT COUNT(*) FROM audit_log WHERE auto_corrected = 1", [], |row| row.get(0) )?; let today: usize = self.conn.query_row( "SELECT COUNT(*) FROM audit_log WHERE timestamp LIKE ?1 || '%'", params![chrono::Local::now().format("%Y-%m-%d").to_string()], |row| row.get(0), )?; Ok(AuditStats { total, auto_corrected, today }) } // ============ Memory ============ /// Speichert einen Memory-Eintrag pub fn save_memory_entry(&self, entry: &MemoryEntry) -> SqlResult<()> { self.conn.execute( "INSERT OR REPLACE INTO memory (id, category, key, value, sticky, auto_load, last_used, use_count) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", params![ entry.id, format!("{:?}", entry.category), entry.key, entry.value.to_string(), entry.sticky as i32, entry.auto_load as i32, entry.last_used, entry.use_count, ], )?; Ok(()) } /// Lädt alle Memory-Einträge pub fn load_memory_entries(&self) -> SqlResult> { let mut stmt = self.conn.prepare( "SELECT id, category, key, value, sticky, auto_load, last_used, use_count FROM memory" )?; let entries = stmt.query_map([], |row| { let cat_str: String = row.get(1)?; let val_str: String = row.get(3)?; let sticky: i32 = row.get(4)?; let auto_load: i32 = row.get(5)?; Ok(MemoryEntry { id: row.get(0)?, category: parse_context_category(&cat_str), key: row.get(2)?, value: serde_json::from_str(&val_str).unwrap_or(serde_json::Value::String(val_str)), sticky: sticky != 0, auto_load: auto_load != 0, last_used: row.get(6)?, use_count: row.get(7)?, }) })?.collect::>>()?; Ok(entries) } /// Löscht einen Memory-Eintrag pub fn delete_memory_entry(&self, id: &str) -> SqlResult<()> { self.conn.execute("DELETE FROM memory WHERE id = ?1", params![id])?; Ok(()) } // ============ Patterns ============ /// Speichert ein Pattern pub fn save_pattern(&self, pattern: &Pattern) -> SqlResult<()> { self.conn.execute( "INSERT OR REPLACE INTO patterns (id, name, description, trigger_text, old_approach, new_approach, reason, occurrence_count, auto_corrected, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", params![ pattern.id, pattern.name, pattern.description, pattern.trigger, pattern.old_approach, pattern.new_approach, pattern.reason, pattern.occurrence_count, pattern.auto_corrected as i32, pattern.created_at, pattern.updated_at, ], )?; Ok(()) } /// Lädt alle Patterns pub fn load_patterns(&self) -> SqlResult> { let mut stmt = self.conn.prepare( "SELECT id, name, description, trigger_text, old_approach, new_approach, reason, occurrence_count, auto_corrected, created_at, updated_at FROM patterns" )?; let patterns = stmt.query_map([], |row| { let auto_corr: i32 = row.get(8)?; Ok(Pattern { id: row.get(0)?, name: row.get(1)?, description: row.get(2)?, trigger: row.get(3)?, old_approach: row.get(4)?, new_approach: row.get(5)?, reason: row.get(6)?, occurrence_count: row.get(7)?, auto_corrected: auto_corr != 0, created_at: row.get(9)?, updated_at: row.get(10)?, }) })?.collect::>>()?; Ok(patterns) } // ============ Sessions ============ /// Erstellt eine neue Session pub fn create_session(&self, session: &Session) -> SqlResult<()> { self.conn.execute( "INSERT INTO sessions (id, claude_session_id, title, working_dir, message_count, token_input, token_output, cost_usd, status, created_at, updated_at, last_message) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)", params![ session.id, session.claude_session_id, session.title, session.working_dir, session.message_count, session.token_input, session.token_output, session.cost_usd, session.status, session.created_at, session.updated_at, session.last_message, ], )?; Ok(()) } /// Aktualisiert eine Session pub fn update_session(&self, session: &Session) -> SqlResult<()> { self.conn.execute( "UPDATE sessions SET claude_session_id = ?2, title = ?3, message_count = ?4, token_input = ?5, token_output = ?6, cost_usd = ?7, status = ?8, updated_at = ?9, last_message = ?10 WHERE id = ?1", params![ session.id, session.claude_session_id, session.title, session.message_count, session.token_input, session.token_output, session.cost_usd, session.status, chrono::Local::now().to_rfc3339(), session.last_message, ], )?; Ok(()) } /// Lädt alle Sessions (neueste zuerst) pub fn load_sessions(&self, limit: usize) -> SqlResult> { let mut stmt = self.conn.prepare( "SELECT id, claude_session_id, title, working_dir, message_count, token_input, token_output, cost_usd, status, created_at, updated_at, last_message FROM sessions ORDER BY updated_at DESC LIMIT ?1" )?; let sessions = stmt.query_map(params![limit as i64], |row| { Ok(Session { id: row.get(0)?, claude_session_id: row.get(1)?, title: row.get(2)?, working_dir: row.get(3)?, message_count: row.get(4)?, token_input: row.get(5)?, token_output: row.get(6)?, cost_usd: row.get(7)?, status: row.get(8)?, created_at: row.get(9)?, updated_at: row.get(10)?, last_message: row.get(11)?, }) })?.collect::>>()?; Ok(sessions) } /// Holt eine Session nach ID pub fn get_session(&self, id: &str) -> SqlResult> { let result = self.conn.query_row( "SELECT id, claude_session_id, title, working_dir, message_count, token_input, token_output, cost_usd, status, created_at, updated_at, last_message FROM sessions WHERE id = ?1", params![id], |row| { Ok(Session { id: row.get(0)?, claude_session_id: row.get(1)?, title: row.get(2)?, working_dir: row.get(3)?, message_count: row.get(4)?, token_input: row.get(5)?, token_output: row.get(6)?, cost_usd: row.get(7)?, status: row.get(8)?, created_at: row.get(9)?, updated_at: row.get(10)?, last_message: row.get(11)?, }) }, ); match result { Ok(s) => Ok(Some(s)), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), Err(e) => Err(e), } } /// Löscht eine Session pub fn delete_session(&self, id: &str) -> SqlResult<()> { self.conn.execute("DELETE FROM sessions WHERE id = ?1", params![id])?; Ok(()) } // ============ Settings ============ /// Speichert eine Einstellung pub fn set_setting(&self, key: &str, value: &str) -> SqlResult<()> { self.conn.execute( "INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?1, ?2, ?3)", params![key, value, chrono::Local::now().to_rfc3339()], )?; Ok(()) } /// Liest eine Einstellung pub fn get_setting(&self, key: &str) -> SqlResult> { let result = self.conn.query_row( "SELECT value FROM settings WHERE key = ?1", params![key], |row| row.get(0), ); match result { Ok(val) => Ok(Some(val)), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), Err(e) => Err(e), } } /// Lädt alle Einstellungen pub fn get_all_settings(&self) -> SqlResult> { let mut stmt = self.conn.prepare("SELECT key, value FROM settings")?; let settings = stmt.query_map([], |row| { Ok((row.get(0)?, row.get(1)?)) })?.collect::>>()?; Ok(settings) } // ============ Statistiken ============ /// DB-Statistiken pub fn stats(&self) -> SqlResult { let permissions: usize = self.conn.query_row( "SELECT COUNT(*) FROM permissions", [], |row| row.get(0) )?; let audit_entries: usize = self.conn.query_row( "SELECT COUNT(*) FROM audit_log", [], |row| row.get(0) )?; let memory_entries: usize = self.conn.query_row( "SELECT COUNT(*) FROM memory", [], |row| row.get(0) )?; let patterns: usize = self.conn.query_row( "SELECT COUNT(*) FROM patterns", [], |row| row.get(0) )?; // DB-Größe ermitteln let page_count: u64 = self.conn.query_row( "PRAGMA page_count", [], |row| row.get(0) )?; let page_size: u64 = self.conn.query_row( "PRAGMA page_size", [], |row| row.get(0) )?; let db_size_kb = (page_count * page_size) / 1024; Ok(DbStats { permissions, audit_entries, memory_entries, patterns, db_size_kb }) } } // ============ Hilfsfunktionen ============ fn parse_audit_category(s: &str) -> AuditCategory { match s { "guardrail" | "guard_rail" => AuditCategory::GuardRail, "pattern" => AuditCategory::Pattern, "hook" => AuditCategory::Hook, "skill" => AuditCategory::Skill, "setting" => AuditCategory::Setting, "mcp" => AuditCategory::MCP, "memory" => AuditCategory::Memory, _ => AuditCategory::Setting, } } fn parse_audit_action(s: &str) -> AuditAction { match s { "create" => AuditAction::Create, "update" => AuditAction::Update, "delete" => AuditAction::Delete, "enable" => AuditAction::Enable, "disable" => AuditAction::Disable, _ => AuditAction::Update, } } fn parse_context_category(s: &str) -> ContextCategory { match s { "Critical" => ContextCategory::Critical, "Pattern" => ContextCategory::Pattern, "Preference" => ContextCategory::Preference, "GuardRail" => ContextCategory::GuardRail, "Hook" => ContextCategory::Hook, "Skill" => ContextCategory::Skill, _ => ContextCategory::Pattern, } } // ============ Tauri Commands ============ pub type DbState = Arc>; /// DB initialisieren (falls Frontend es auslösen will) #[tauri::command] pub async fn init_database(app: AppHandle) -> Result { let state = app.state::(); let db = state.lock().unwrap(); db.stats().map_err(|e| e.to_string()) } /// DB-Statistiken abrufen #[tauri::command] pub async fn get_db_stats(app: AppHandle) -> Result { let state = app.state::(); let db = state.lock().unwrap(); db.stats().map_err(|e| e.to_string()) } /// Einstellung lesen #[tauri::command] pub async fn get_setting(app: AppHandle, key: String) -> Result, String> { let state = app.state::(); let db = state.lock().unwrap(); db.get_setting(&key).map_err(|e| e.to_string()) } /// Einstellung speichern #[tauri::command] pub async fn set_setting(app: AppHandle, key: String, value: String) -> Result<(), String> { let state = app.state::(); let db = state.lock().unwrap(); db.set_setting(&key, &value).map_err(|e| e.to_string()) } /// Alle Einstellungen laden #[tauri::command] pub async fn get_all_settings(app: AppHandle) -> Result, String> { let state = app.state::(); let db = state.lock().unwrap(); db.get_all_settings().map_err(|e| e.to_string()) }