diff --git a/ROADMAP.md b/ROADMAP.md index 7d3084b..2edbfdd 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -31,6 +31,7 @@ Stand: 14.04.2026 | **System-Monitor (Phase 16)** | ✅ | adb11fd | | **UI: Code-Copy, Edit, Regenerate (Phase 7)** | ✅ | 9d837ef | | **Session-Management (Phase 6)** | ✅ | abaf4eb | +| **Claude-DB Integration (Phase 8)** | ✅ | (pending) | --- @@ -127,7 +128,9 @@ Stand: 14.04.2026 --- -## Phase 8: Claude-DB Integration +## Phase 8: Claude-DB Integration ✅ ERLEDIGT + +> **Commit:** (pending) ### Problem Die App hat keinen direkten Zugriff auf die zentrale Wissensbasis (`claude` DB auf 192.168.155.1). @@ -137,37 +140,46 @@ Die App hat keinen direkten Zugriff auf die zentrale Wissensbasis (`claude` DB a - Erkenntnisse speichern ("Das merken") - Skills/Hooks/Patterns aus DB laden -### Aufgaben +### Implementiert -- [ ] **src-tauri/src/knowledge.rs** (NEU) - - [ ] MySQL-Verbindung zu claude-db - - [ ] `search_knowledge(query)` — Volltextsuche - - [ ] `save_knowledge(entry)` — Neuer Eintrag - - [ ] `get_sticky_context()` — Fur Claude-Prompt +- ✅ **src-tauri/src/knowledge.rs** (NEU) + - MySQL-Verbindung zu claude-db (192.168.155.1:3306) + - `search_knowledge(query)` — Volltextsuche mit MATCH AGAINST + - `save_knowledge(entry)` — Neuer Eintrag speichern + - `get_knowledge(id)` — Einzelnen Eintrag laden + - `get_knowledge_categories()` — Kategorien mit Count + - `get_recent_knowledge()` — Letzte Einträge + - `test_knowledge_connection()` — Verbindungstest -- [ ] **src-tauri/src/lib.rs** - - [ ] Knowledge-Modul registrieren - - [ ] Tauri-Commands exportieren +- ✅ **src-tauri/Cargo.toml** + - `mysql_async = "0.34"` Dependency hinzugefügt -- [ ] **src/lib/components/KnowledgePanel.svelte** (NEU) - - [ ] Suchfeld fur Wissensbasis - - [ ] Ergebnisliste mit Titel, Tags, Preview - - [ ] Detail-View fur einzelnen Eintrag +- ✅ **src-tauri/src/lib.rs** + - Knowledge-Modul registriert (`mod knowledge`) + - Alle Tauri-Commands exportiert -- [ ] **"Das merken" Feature** - - [ ] Button in Chat bei wichtigen Erkenntnissen - - [ ] Modal: Kategorie, Tags, Titel eingeben - - [ ] Speichert in `knowledge` Tabelle +- ✅ **src/lib/components/KnowledgePanel.svelte** (NEU) + - Suchfeld mit Enter-Trigger + - Kategorie-Filter Chips + - Ergebnisliste mit Relevanz-Score + - Detail-Modal bei Klick + - "Das merken" Dialog mit Formular + - Verbindungsstatus-Anzeige -- [ ] **Sticky Context** - - [ ] Beim Chat-Start: Sticky-Eintrage laden - - [ ] Als System-Prompt an Claude senden +- ✅ **src/routes/+page.svelte** + - Neuer Tab "Wissen" (📚) im mittleren Panel + - KnowledgePanel eingebunden + +### Noch offen (niedrigere Priorität) + +- [ ] **Sticky Context** — Automatisch beim Chat-Start laden +- [ ] **"Das merken" im Chat** — Button direkt bei Nachrichten ### Verifikation ```bash # Wissensbasis durchsuchen → Ergebnisse in App # "Das merken" klicken → Eintrag in claude.knowledge -# Neuer Chat → Sticky Context wird geladen +# Kategorie-Filter → Einträge filtern ``` --- @@ -1235,3 +1247,4 @@ CARGO_TARGET_DIR=/tmp/claude-desktop-target nix-shell --run "npx tauri build" | 14.04.2026 | adb11fd | **Phase 16:** System-Monitor | | 14.04.2026 | 9d837ef | **Phase 7:** UI Code-Copy, Edit, Regenerate | | 14.04.2026 | abaf4eb | **Phase 6:** Session-Management, Auto-Load, Compacting | +| 14.04.2026 | (pending) | **Phase 8:** Claude-DB Integration, KnowledgePanel | diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ba20d61..28d6b36 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -21,6 +21,7 @@ tokio = { version = "1", features = ["full"] } chrono = { version = "0.4", features = ["serde"] } uuid = { version = "1", features = ["v4", "serde"] } rusqlite = { version = "0.31", features = ["bundled"] } +mysql_async = "0.34" [profile.release] panic = "abort" diff --git a/src-tauri/src/knowledge.rs b/src-tauri/src/knowledge.rs new file mode 100644 index 0000000..b4a0abb --- /dev/null +++ b/src-tauri/src/knowledge.rs @@ -0,0 +1,274 @@ +// Claude Desktop — Wissensbasis (claude-db) +// Direkte MySQL-Anbindung zur zentralen Wissensdatenbank + +use mysql_async::{Pool, prelude::*}; +use serde::{Deserialize, Serialize}; + +/// Verbindungskonfiguration +const MYSQL_HOST: &str = "192.168.155.1"; +const MYSQL_PORT: u16 = 3306; +const MYSQL_USER: &str = "claude"; +const MYSQL_PASS: &str = "claude"; +const MYSQL_DB: &str = "claude"; + +/// Wissenseintrag aus der knowledge-Tabelle +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KnowledgeEntry { + pub id: i64, + pub category: String, + pub title: String, + pub content: String, + pub tags: Option, + pub priority: i32, + pub status: String, + pub related_ids: Option, + pub source: Option, + pub created_at: String, + pub updated_at: String, +} + +/// Neuer Wissenseintrag (ohne id, timestamps) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NewKnowledge { + pub category: String, + pub title: String, + pub content: String, + pub tags: Option, + pub priority: Option, + pub source: Option, +} + +/// Suchergebnis mit Relevanz-Score +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SearchResult { + pub entry: KnowledgeEntry, + pub relevance: f64, +} + +/// Verfügbare Kategorien +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CategoryInfo { + pub name: String, + pub count: i64, +} + +// ============ Hilfsfunktionen ============ + +/// Erstellt einen MySQL Connection-Pool +fn create_pool() -> Pool { + let url = format!( + "mysql://{}:{}@{}:{}/{}", + MYSQL_USER, MYSQL_PASS, MYSQL_HOST, MYSQL_PORT, MYSQL_DB + ); + Pool::new(url.as_str()) +} + +// ============ Tauri Commands ============ + +/// Wissensbasis durchsuchen (Volltext) +#[tauri::command] +pub async fn search_knowledge( + query: String, + category: Option, + limit: Option, +) -> Result, String> { + let pool = create_pool(); + let mut conn = pool.get_conn().await.map_err(|e| e.to_string())?; + + let limit = limit.unwrap_or(20); + + // Volltext-Suche mit optionalem Kategorie-Filter + let results: Vec = if let Some(cat) = category { + conn.exec_map( + r#"SELECT + id, category, title, content, tags, priority, status, + related_ids, source, created_at, updated_at, + MATCH(title, content, tags) AGAINST(? IN NATURAL LANGUAGE MODE) as relevance + FROM knowledge + WHERE MATCH(title, content, tags) AGAINST(? IN NATURAL LANGUAGE MODE) + AND category = ? + ORDER BY relevance DESC + LIMIT ?"#, + (&query, &query, &cat, limit), + |(id, category, title, content, tags, priority, status, related_ids, source, created_at, updated_at, relevance): + (i64, String, String, String, Option, i32, String, Option, Option, String, String, f64)| { + SearchResult { + entry: KnowledgeEntry { + id, category, title, content, tags, priority, status, + related_ids, source, created_at, updated_at, + }, + relevance, + } + } + ).await.map_err(|e| e.to_string())? + } else { + conn.exec_map( + r#"SELECT + id, category, title, content, tags, priority, status, + related_ids, source, created_at, updated_at, + MATCH(title, content, tags) AGAINST(? IN NATURAL LANGUAGE MODE) as relevance + FROM knowledge + WHERE MATCH(title, content, tags) AGAINST(? IN NATURAL LANGUAGE MODE) + ORDER BY relevance DESC + LIMIT ?"#, + (&query, &query, limit), + |(id, category, title, content, tags, priority, status, related_ids, source, created_at, updated_at, relevance): + (i64, String, String, String, Option, i32, String, Option, Option, String, String, f64)| { + SearchResult { + entry: KnowledgeEntry { + id, category, title, content, tags, priority, status, + related_ids, source, created_at, updated_at, + }, + relevance, + } + } + ).await.map_err(|e| e.to_string())? + }; + + drop(conn); + pool.disconnect().await.map_err(|e| e.to_string())?; + + println!("🔍 Wissensbasis-Suche '{}': {} Treffer", query, results.len()); + Ok(results) +} + +/// Wissenseintrag nach ID laden +#[tauri::command] +pub async fn get_knowledge(id: i64) -> Result, String> { + let pool = create_pool(); + let mut conn = pool.get_conn().await.map_err(|e| e.to_string())?; + + let result: Option = conn.exec_first( + r#"SELECT id, category, title, content, tags, priority, status, + related_ids, source, created_at, updated_at + FROM knowledge WHERE id = ?"#, + (id,) + ).await.map_err(|e| e.to_string())? + .map(|(id, category, title, content, tags, priority, status, related_ids, source, created_at, updated_at): + (i64, String, String, String, Option, i32, String, Option, Option, String, String)| { + KnowledgeEntry { + id, category, title, content, tags, priority, status, + related_ids, source, created_at, updated_at, + } + }); + + drop(conn); + pool.disconnect().await.map_err(|e| e.to_string())?; + + Ok(result) +} + +/// Neuen Wissenseintrag speichern ("Das merken") +#[tauri::command] +pub async fn save_knowledge(entry: NewKnowledge) -> Result { + let pool = create_pool(); + let mut conn = pool.get_conn().await.map_err(|e| e.to_string())?; + + let priority = entry.priority.unwrap_or(2); + + conn.exec_drop( + r#"INSERT INTO knowledge (category, title, content, tags, priority, status, source, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, 'active', ?, NOW(), NOW())"#, + (&entry.category, &entry.title, &entry.content, &entry.tags, priority, &entry.source) + ).await.map_err(|e| e.to_string())?; + + let id: i64 = conn.last_insert_id().ok_or("Keine Insert-ID")? as i64; + + drop(conn); + pool.disconnect().await.map_err(|e| e.to_string())?; + + println!("💾 Wissen gespeichert: {} (ID: {})", entry.title, id); + Ok(id) +} + +/// Alle Kategorien mit Anzahl der Einträge +#[tauri::command] +pub async fn get_knowledge_categories() -> Result, String> { + let pool = create_pool(); + let mut conn = pool.get_conn().await.map_err(|e| e.to_string())?; + + let categories: Vec = conn.query_map( + r#"SELECT category, COUNT(*) as count + FROM knowledge + WHERE status = 'active' + GROUP BY category + ORDER BY count DESC"#, + |(name, count): (String, i64)| CategoryInfo { name, count } + ).await.map_err(|e| e.to_string())?; + + drop(conn); + pool.disconnect().await.map_err(|e| e.to_string())?; + + Ok(categories) +} + +/// Letzte N Wissenseinträge laden +#[tauri::command] +pub async fn get_recent_knowledge( + limit: Option, + category: Option, +) -> Result, String> { + let pool = create_pool(); + let mut conn = pool.get_conn().await.map_err(|e| e.to_string())?; + + let limit = limit.unwrap_or(10); + + let entries: Vec = if let Some(cat) = category { + conn.exec_map( + r#"SELECT id, category, title, content, tags, priority, status, + related_ids, source, created_at, updated_at + FROM knowledge + WHERE status = 'active' AND category = ? + ORDER BY updated_at DESC + LIMIT ?"#, + (&cat, limit), + |(id, category, title, content, tags, priority, status, related_ids, source, created_at, updated_at): + (i64, String, String, String, Option, i32, String, Option, Option, String, String)| { + KnowledgeEntry { + id, category, title, content, tags, priority, status, + related_ids, source, created_at, updated_at, + } + } + ).await.map_err(|e| e.to_string())? + } else { + conn.exec_map( + r#"SELECT id, category, title, content, tags, priority, status, + related_ids, source, created_at, updated_at + FROM knowledge + WHERE status = 'active' + ORDER BY updated_at DESC + LIMIT ?"#, + (limit,), + |(id, category, title, content, tags, priority, status, related_ids, source, created_at, updated_at): + (i64, String, String, String, Option, i32, String, Option, Option, String, String)| { + KnowledgeEntry { + id, category, title, content, tags, priority, status, + related_ids, source, created_at, updated_at, + } + } + ).await.map_err(|e| e.to_string())? + }; + + drop(conn); + pool.disconnect().await.map_err(|e| e.to_string())?; + + Ok(entries) +} + +/// Verbindung zur Wissensbasis testen +#[tauri::command] +pub async fn test_knowledge_connection() -> Result { + let pool = create_pool(); + let mut conn = pool.get_conn().await.map_err(|e| format!("Verbindungsfehler: {}", e))?; + + let count: i64 = conn.query_first("SELECT COUNT(*) FROM knowledge") + .await + .map_err(|e| e.to_string())? + .unwrap_or(0); + + drop(conn); + pool.disconnect().await.map_err(|e| e.to_string())?; + + println!("✅ Wissensbasis verbunden: {} Einträge", count); + Ok(format!("Verbunden! {} Einträge in der Wissensbasis", count)) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 02d014e..fbfc8f7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -12,6 +12,7 @@ mod audit; mod claude; mod db; mod guard; +mod knowledge; mod memory; mod session; @@ -66,6 +67,13 @@ pub fn run() { db::load_messages, db::clear_messages, db::compact_session, + // Wissensbasis (claude-db) + knowledge::search_knowledge, + knowledge::get_knowledge, + knowledge::save_knowledge, + knowledge::get_knowledge_categories, + knowledge::get_recent_knowledge, + knowledge::test_knowledge_connection, ]) .setup(|app| { let handle = app.handle().clone(); diff --git a/src/lib/components/KnowledgePanel.svelte b/src/lib/components/KnowledgePanel.svelte new file mode 100644 index 0000000..a4ca60d --- /dev/null +++ b/src/lib/components/KnowledgePanel.svelte @@ -0,0 +1,793 @@ + + + + +
+
+

📚 Wissensbasis

+ {#if connected} + Verbunden + {:else} + Offline + {/if} +
+ + {#if !connected} +
+

Verbindung zur Wissensbasis fehlgeschlagen.

+

{connectionError}

+ +
+ {:else} + + + + +
+ + {#each categories as cat} + + {/each} +
+ + +
+ {#if results.length === 0} +
+ {#if searchQuery} + Keine Treffer für "{searchQuery}" + {:else} + Keine Einträge vorhanden + {/if} +
+ {:else} + {#each results as { entry, relevance }} + + {/each} + {/if} +
+ + +
+ +
+ {/if} +
+ + +{#if selectedEntry} + +{/if} + + +{#if showSaveDialog} + +{/if} + + diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 6e4f250..7ba9f82 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -5,6 +5,7 @@ import ActivityPanel from '$lib/components/ActivityPanel.svelte'; import AgentView from '$lib/components/AgentView.svelte'; import MemoryPanel from '$lib/components/MemoryPanel.svelte'; + import KnowledgePanel from '$lib/components/KnowledgePanel.svelte'; import AuditLog from '$lib/components/AuditLog.svelte'; import GuardRailsPanel from '$lib/components/GuardRailsPanel.svelte'; import SettingsPanel from '$lib/components/SettingsPanel.svelte'; @@ -16,6 +17,7 @@ const middleTabs = [ { id: 'activity', label: 'Aktivität', icon: '📋' }, { id: 'monitor', label: 'Monitor', icon: '📊' }, + { id: 'knowledge', label: 'Wissen', icon: '📚' }, { id: 'memory', label: 'Gedächtnis', icon: '🧠' }, { id: 'audit', label: 'Historie', icon: '📝' }, ]; @@ -64,6 +66,8 @@ {:else if activeMiddleTab === 'monitor'} + {:else if activeMiddleTab === 'knowledge'} + {:else if activeMiddleTab === 'memory'} {:else if activeMiddleTab === 'audit'}