//! Agent configuration management use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use uuid::Uuid; /// Agent configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { /// Server WebSocket URL (e.g., wss://connect.example.com/ws) pub server_url: String, /// Agent API key for authentication pub api_key: String, /// Unique agent identifier (generated on first run) #[serde(default = "generate_agent_id")] pub agent_id: String, /// Optional hostname override pub hostname_override: Option, /// Support code for one-time support sessions (set via command line) #[serde(skip)] pub support_code: Option, /// Capture settings #[serde(default)] pub capture: CaptureConfig, /// Encoding settings #[serde(default)] pub encoding: EncodingConfig, } fn generate_agent_id() -> String { Uuid::new_v4().to_string() } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CaptureConfig { /// Target frames per second (1-60) #[serde(default = "default_fps")] pub fps: u32, /// Use DXGI Desktop Duplication (recommended) #[serde(default = "default_true")] pub use_dxgi: bool, /// Fall back to GDI if DXGI fails #[serde(default = "default_true")] pub gdi_fallback: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EncodingConfig { /// Preferred codec (auto, raw, vp9, h264) #[serde(default = "default_codec")] pub codec: String, /// Quality (1-100, higher = better quality, more bandwidth) #[serde(default = "default_quality")] pub quality: u32, /// Use hardware encoding if available #[serde(default = "default_true")] pub hardware_encoding: bool, } fn default_fps() -> u32 { 30 } fn default_true() -> bool { true } fn default_codec() -> String { "auto".to_string() } fn default_quality() -> u32 { 75 } impl Default for CaptureConfig { fn default() -> Self { Self { fps: default_fps(), use_dxgi: true, gdi_fallback: true, } } } impl Default for EncodingConfig { fn default() -> Self { Self { codec: default_codec(), quality: default_quality(), hardware_encoding: true, } } } impl Config { /// Load configuration from file or environment pub fn load() -> Result { // Try loading from config file let config_path = Self::config_path(); if config_path.exists() { let contents = std::fs::read_to_string(&config_path) .with_context(|| format!("Failed to read config from {:?}", config_path))?; let mut config: Config = toml::from_str(&contents) .with_context(|| "Failed to parse config file")?; // Ensure agent_id is set and saved if config.agent_id.is_empty() { config.agent_id = generate_agent_id(); let _ = config.save(); } // support_code is always None when loading from file (set via CLI) config.support_code = None; return Ok(config); } // Fall back to environment variables let server_url = std::env::var("GURUCONNECT_SERVER_URL") .unwrap_or_else(|_| "wss://connect.azcomputerguru.com/ws/agent".to_string()); let api_key = std::env::var("GURUCONNECT_API_KEY") .unwrap_or_else(|_| "dev-key".to_string()); let agent_id = std::env::var("GURUCONNECT_AGENT_ID") .unwrap_or_else(|_| generate_agent_id()); let config = Config { server_url, api_key, agent_id, hostname_override: std::env::var("GURUCONNECT_HOSTNAME").ok(), support_code: None, // Set via CLI capture: CaptureConfig::default(), encoding: EncodingConfig::default(), }; // Save config with generated agent_id for persistence let _ = config.save(); Ok(config) } /// Get the configuration file path fn config_path() -> PathBuf { // Check for config in current directory first let local_config = PathBuf::from("guruconnect.toml"); if local_config.exists() { return local_config; } // Check in program data directory (Windows) #[cfg(windows)] { if let Ok(program_data) = std::env::var("ProgramData") { let path = PathBuf::from(program_data) .join("GuruConnect") .join("agent.toml"); if path.exists() { return path; } } } // Default to local config local_config } /// Get the hostname to use pub fn hostname(&self) -> String { self.hostname_override .clone() .unwrap_or_else(|| { hostname::get() .map(|h| h.to_string_lossy().to_string()) .unwrap_or_else(|_| "unknown".to_string()) }) } /// Save current configuration to file pub fn save(&self) -> Result<()> { let config_path = Self::config_path(); // Ensure parent directory exists if let Some(parent) = config_path.parent() { std::fs::create_dir_all(parent)?; } let contents = toml::to_string_pretty(self)?; std::fs::write(&config_path, contents)?; Ok(()) } } /// Example configuration file content pub fn example_config() -> &'static str { r#"# GuruConnect Agent Configuration # Server connection server_url = "wss://connect.example.com/ws" api_key = "your-agent-api-key" agent_id = "auto-generated-uuid" # Optional: override hostname # hostname_override = "custom-hostname" [capture] fps = 30 use_dxgi = true gdi_fallback = true [encoding] codec = "auto" # auto, raw, vp9, h264 quality = 75 # 1-100 hardware_encoding = true "# }