// Claude Desktop — Claude SDK Integration // Kommunikation mit Claude Code via Node.js Child-Process use serde::{Deserialize, Serialize}; use std::io::{BufRead, BufReader, Write}; use std::process::{Command, Stdio}; use std::sync::{Arc, Mutex}; use tauri::{AppHandle, Emitter, Manager}; use crate::db; /// Status eines Agents #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AgentStatus { pub id: String, pub agent_type: String, pub status: String, pub task: String, pub tool_calls: u32, } /// Tool-Aufruf Event #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolEvent { pub id: String, pub tool: Option, pub input: Option, pub output: Option, pub success: Option, } /// Agent Event #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AgentEvent { pub id: String, #[serde(rename = "type")] pub agent_type: Option, pub task: Option, pub code: Option, } /// Nachricht von der Bridge #[derive(Debug, Clone, Deserialize)] struct BridgeMessage { #[serde(rename = "type")] msg_type: String, event: Option, payload: Option, id: Option, result: Option, error: Option, } /// Globaler State für die Bridge pub struct ClaudeState { pub bridge_process: Option, pub bridge_stdin: Option, pub request_counter: u64, pub agents: Vec, } impl Default for ClaudeState { fn default() -> Self { Self { bridge_process: None, bridge_stdin: None, request_counter: 0, agents: vec![], } } } /// Bridge starten pub fn start_bridge(app: &AppHandle) -> Result<(), String> { // Script-Pfad ermitteln let exe_dir = std::env::current_exe() .map_err(|e| e.to_string())? .parent() .ok_or("Kein Parent-Verzeichnis")? .to_path_buf(); // Script in mehreren Pfaden suchen let candidates = vec![ exe_dir.join("scripts").join("claude-bridge.js"), // Entwicklung: relativ zum Cargo-Manifest std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../scripts/claude-bridge.js"), // Fallback: CWD std::env::current_dir().unwrap_or_default().join("scripts/claude-bridge.js"), ]; let script_path = candidates.iter() .find(|p| p.exists()) .cloned() .ok_or_else(|| format!("claude-bridge.js nicht gefunden. Gesucht in: {:?}", candidates))?; println!("🔌 Starte Claude Bridge: {:?}", script_path); // Arbeitsverzeichnis = Projektroot (wo node_modules liegt) let project_dir = script_path.parent() .and_then(|p| p.parent()) .unwrap_or_else(|| std::path::Path::new(".")); println!("📂 Bridge Arbeitsverzeichnis: {:?}", project_dir); let mut child = Command::new("node") .arg(&script_path) .current_dir(project_dir) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .map_err(|e| format!("Bridge konnte nicht gestartet werden: {}", e))?; let stdin = child.stdin.take().ok_or("Kein stdin verfügbar")?; let stdout = child.stdout.take().ok_or("Kein stdout verfügbar")?; let stderr = child.stderr.take(); // State speichern — child MUSS am Leben bleiben! let state = app.state::>>(); { let mut state = state.lock().unwrap(); state.bridge_process = Some(child); state.bridge_stdin = Some(stdin); } // Stderr in separatem Thread lesen und loggen if let Some(stderr) = stderr { std::thread::spawn(move || { let reader = BufReader::new(stderr); for line in reader.lines().map_while(Result::ok) { println!("🔌 Bridge stderr: {}", line); } }); } // Stdout in separatem Thread lesen let app_handle = app.clone(); std::thread::spawn(move || { let reader = BufReader::new(stdout); for line in reader.lines().map_while(Result::ok) { if let Ok(msg) = serde_json::from_str::(&line) { handle_bridge_message(&app_handle, msg); } } println!("⚠️ Bridge stdout geschlossen"); }); Ok(()) } /// Bridge-Nachrichten verarbeiten fn handle_bridge_message(app: &AppHandle, msg: BridgeMessage) { match msg.msg_type.as_str() { "event" => { if let (Some(event), Some(payload)) = (msg.event, msg.payload) { match event.as_str() { "ready" => { println!("✅ Claude Bridge bereit"); let _ = app.emit("bridge-ready", ()); } "agent-started" | "subagent-start" => { if let Ok(agent) = serde_json::from_value::(payload.clone()) { println!("🤖 Agent gestartet: {}", agent.id); let _ = app.emit("agent-started", &payload); // Agent zur Liste hinzufügen let state = app.state::>>(); let mut state = state.lock().unwrap(); state.agents.push(AgentStatus { id: agent.id, agent_type: agent.agent_type.unwrap_or_else(|| "Main".to_string()), status: "active".to_string(), task: agent.task.unwrap_or_default(), tool_calls: 0, }); } } "agent-stopped" | "subagent-stop" => { if let Ok(agent) = serde_json::from_value::(payload.clone()) { println!("⏹️ Agent gestoppt: {}", agent.id); let _ = app.emit("agent-stopped", &payload); // Agent aus Liste entfernen let state = app.state::>>(); let mut state = state.lock().unwrap(); state.agents.retain(|a| a.id != agent.id); } } "tool-start" => { if let Ok(tool) = serde_json::from_value::(payload.clone()) { println!("🔧 Tool Start: {} - {:?}", tool.id, tool.tool); let _ = app.emit("tool-start", &payload); } } "tool-end" => { if let Ok(tool) = serde_json::from_value::(payload.clone()) { println!( "✅ Tool Ende: {} - {}", tool.id, if tool.success.unwrap_or(true) { "OK" } else { "FEHLER" } ); let _ = app.emit("tool-end", &payload); } } "text" => { let _ = app.emit("claude-text", &payload); } "result" => { // Session-ID aus Result extrahieren und in DB speichern if let Some(sid) = payload.get("session_id").and_then(|v| v.as_str()) { if let Some(db_state) = app.try_state::>>() { let db_lock = db_state.lock().unwrap(); if let Ok(Some(active_id)) = db_lock.get_setting("active_session_id") { if !active_id.is_empty() { if let Ok(Some(mut session)) = db_lock.get_session(&active_id) { session.claude_session_id = Some(sid.to_string()); session.message_count += 1; if let Some(cost) = payload.get("cost").and_then(|v| v.as_f64()) { session.cost_usd += cost; } if let Some(tin) = payload.get("tokens").and_then(|t| t.get("input")).and_then(|v| v.as_i64()) { session.token_input += tin; } if let Some(tout) = payload.get("tokens").and_then(|t| t.get("output")).and_then(|v| v.as_i64()) { session.token_output += tout; } let _ = db_lock.update_session(&session); } } } } } let _ = app.emit("claude-result", &payload); } "all-stopped" => { println!("⏹️ Alle Agents gestoppt"); let _ = app.emit("all-stopped", ()); let state = app.state::>>(); let mut state = state.lock().unwrap(); state.agents.clear(); } _ => { println!("📨 Event: {} = {:?}", event, payload); } } } } "response" => { if let Some(id) = msg.id { println!("📬 Response für {}: {:?}", id, msg.result); // TODO: Über Channel an wartenden Request weitergeben } } _ => {} } } /// Befehl an Bridge senden fn send_to_bridge(app: &AppHandle, command: &str, message: &str) -> Result { let state = app.state::>>(); let mut state = state.lock().unwrap(); state.request_counter += 1; let request_id = format!("req-{}", state.request_counter); let msg = serde_json::json!({ "command": command, "id": request_id, "message": message }); if let Some(stdin) = &mut state.bridge_stdin { writeln!(stdin, "{}", msg.to_string()).map_err(|e| e.to_string())?; stdin.flush().map_err(|e| e.to_string())?; Ok(request_id) } else { Err("Bridge nicht gestartet".to_string()) } } // ============ Tauri Commands ============ /// Nachricht an Claude senden #[tauri::command] pub async fn send_message(app: AppHandle, message: String) -> Result { println!("📨 Nachricht empfangen: {}", &message[..message.len().min(50)]); // Bridge starten falls nicht aktiv let needs_start = { let state = app.state::>>(); let state_guard = state.lock().unwrap(); state_guard.bridge_stdin.is_none() }; if needs_start { start_bridge(&app)?; // Kurz warten bis Bridge bereit tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; } send_to_bridge(&app, "message", &message)?; // Hinweis: Die eigentliche Antwort kommt über Events Ok("Nachricht gesendet. Antwort folgt über Events.".to_string()) } /// Alle Agents stoppen #[tauri::command] pub async fn stop_all_agents(app: AppHandle) -> Result<(), String> { println!("⏹️ STOPP: Alle Agents werden gestoppt"); let _ = send_to_bridge(&app, "stop", ""); app.emit("agents-stopped", ()).map_err(|e| e.to_string())?; Ok(()) } /// Status aller Agents abrufen #[tauri::command] pub async fn get_agent_status(app: AppHandle) -> Result, String> { let state = app.state::>>(); let state = state.lock().unwrap(); Ok(state.agents.clone()) }