use std::sync::Arc; use std::time::Duration; use tauri::menu::{MenuBuilder, MenuItemBuilder}; use tauri::tray::TrayIconBuilder; use tauri::Manager; use tauri_plugin_autostart::MacosLauncher; use tokio::sync::Mutex; const OLLAMA_PORT: u16 = 11434; const JARVIS_PORT: u16 = 8222; /// Small, fast model pulled at startup so the app opens quickly. const STARTUP_MODEL: &str = "qwen3.5:4b"; /// Tiny fallback model if even the startup model can't be pulled. const FALLBACK_MODEL: &str = "qwen3:0.6b"; /// Qwen3.5 model variants, ordered smallest to largest. /// Each entry is (ollama_tag, approximate_download_size_gb, min_ram_gb). const QWEN35_MODELS: &[(&str, f64, f64)] = &[ ("qwen3.5:0.8b", 1.0, 4.0), ("qwen3.5:2b", 2.7, 6.0), ("qwen3.5:4b", 3.4, 8.0), ("qwen3.5:9b", 6.6, 12.0), ("qwen3.5:27b", 17.0, 24.0), ("qwen3.5:35b", 24.0, 32.0), ("qwen3.5:122b", 81.0, 96.0), ]; /// Get total system RAM in GB. fn total_ram_gb() -> f64 { #[cfg(target_os = "macos")] { use std::process::Command; if let Ok(output) = Command::new("sysctl").args(["-n", "hw.memsize"]).output() { if let Ok(s) = String::from_utf8(output.stdout) { if let Ok(bytes) = s.trim().parse::() { return bytes as f64 / (1024.0 * 1024.0 * 1024.0); } } } } #[cfg(target_os = "linux")] { if let Ok(contents) = std::fs::read_to_string("/proc/meminfo") { for line in contents.lines() { if line.starts_with("MemTotal:") { if let Some(kb_str) = line.split_whitespace().nth(1) { if let Ok(kb) = kb_str.parse::() { return kb as f64 / (1024.0 * 1024.0); } } } } } } #[cfg(target_os = "windows")] { use std::process::Command; // wmic returns TotalVisibleMemorySize in KB if let Ok(output) = Command::new("wmic") .args(["OS", "get", "TotalVisibleMemorySize", "/value"]) .output() { if let Ok(s) = String::from_utf8(output.stdout) { for line in s.lines() { if let Some(val) = line.strip_prefix("TotalVisibleMemorySize=") { if let Ok(kb) = val.trim().parse::() { return kb as f64 / (1024.0 * 1024.0); } } } } } } 8.0 } /// Return the list of Qwen3.5 models that fit on this machine, smallest first. fn models_that_fit() -> Vec<&'static str> { let ram = total_ram_gb(); QWEN35_MODELS .iter() .filter(|(_, _, min_ram)| ram >= *min_ram) .map(|(tag, _, _)| *tag) .collect() } /// Pick the default model — prefers STARTUP_MODEL if it fits, otherwise /// falls back to the third-largest model that fits on this machine. fn preferred_model() -> &'static str { let fitting = models_that_fit(); // Prefer STARTUP_MODEL when it fits (fast, good quality) if fitting.contains(&STARTUP_MODEL) { return STARTUP_MODEL; } match fitting.len() { 0 => FALLBACK_MODEL, 1 => fitting[0], 2 => fitting[0], n => fitting[n - 3], // third-largest } } /// Get the user home directory, handling both Unix (HOME) and Windows (USERPROFILE). fn home_dir() -> String { std::env::var("HOME") .or_else(|_| std::env::var("USERPROFILE")) .unwrap_or_default() } /// Resolve full path to a binary by checking common locations. /// macOS .app bundles don't inherit the shell PATH, so we probe manually. fn resolve_bin(name: &str) -> String { let home = home_dir(); #[cfg(not(target_os = "windows"))] let candidates = vec![ format!("/opt/homebrew/bin/{name}"), format!("{home}/.local/bin/{name}"), format!("{home}/.cargo/bin/{name}"), format!("/usr/local/bin/{name}"), format!("/usr/bin/{name}"), ]; #[cfg(target_os = "windows")] let candidates = { let localappdata = std::env::var("LOCALAPPDATA").unwrap_or_default(); let programfiles = std::env::var("ProgramFiles").unwrap_or_default(); let programfiles_x86 = std::env::var("ProgramFiles(x86)").unwrap_or_default(); vec![ // Git for Windows — standard install paths format!("{programfiles}\\Git\\cmd\\{name}.exe"), format!("{programfiles_x86}\\Git\\cmd\\{name}.exe"), format!("{localappdata}\\Programs\\Git\\cmd\\{name}.exe"), // Scoop package manager format!("{home}\\scoop\\shims\\{name}.exe"), // Cargo, local bin format!("{home}\\.cargo\\bin\\{name}.exe"), format!("{home}\\.local\\bin\\{name}.exe"), // Generic program locations format!("{localappdata}\\Programs\\{name}\\{name}.exe"), format!("{programfiles}\\{name}\\{name}.exe"), // Ollama installs to LOCALAPPDATA on Windows format!("{localappdata}\\Programs\\Ollama\\{name}.exe"), // uv installs via pip/pipx format!("{home}\\AppData\\Roaming\\Python\\Scripts\\{name}.exe"), ] }; for path in &candidates { if std::path::Path::new(path).exists() { return path.clone(); } } // Fallback: ask the OS to find it on PATH. // On Windows this uses `where.exe`, on Unix `which`. #[cfg(target_os = "windows")] { if let Ok(output) = std::process::Command::new("where") .arg(format!("{name}.exe")) .output() { if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); if let Some(first_line) = stdout.lines().next() { let p = first_line.trim(); if !p.is_empty() && std::path::Path::new(p).exists() { return p.to_string(); } } } } } #[cfg(not(target_os = "windows"))] { if let Ok(output) = std::process::Command::new("which").arg(name).output() { if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); if let Some(first_line) = stdout.lines().next() { let p = first_line.trim(); if !p.is_empty() && std::path::Path::new(p).exists() { return p.to_string(); } } } } } name.to_string() } /// Find the OpenJarvis project root (contains pyproject.toml). /// Checks OPENJARVIS_ROOT env var, walks up from the executable, then /// probes common clone locations. fn find_project_root() -> Option { // 1. Explicit env var override if let Ok(root) = std::env::var("OPENJARVIS_ROOT") { let path = std::path::PathBuf::from(&root); if path.join("pyproject.toml").exists() { return Some(path); } } // 2. Walk up from the running executable (works in dev and .app bundle) if let Ok(exe) = std::env::current_exe() { let mut dir = exe.parent().map(|p| p.to_path_buf()); for _ in 0..8 { if let Some(ref d) = dir { if d.join("pyproject.toml").exists() { return Some(d.clone()); } dir = d.parent().map(|p| p.to_path_buf()); } } } // 3. Fallback: well-known direct paths let home = home_dir(); let direct = [ format!("{home}/OpenJarvis"), format!("{home}/projects/hazy/OpenJarvis"), format!("{home}/projects/OpenJarvis"), format!("{home}/src/OpenJarvis"), format!("{home}/Documents/OpenJarvis"), format!("{home}/Desktop/OpenJarvis"), format!("{home}/Developer/OpenJarvis"), format!("{home}/dev/OpenJarvis"), format!("{home}/Code/OpenJarvis"), format!("{home}/code/OpenJarvis"), format!("{home}/repos/OpenJarvis"), format!("{home}/github/OpenJarvis"), ]; for p in &direct { let path = std::path::PathBuf::from(p); if path.join("pyproject.toml").exists() { return Some(path); } } // 4. Shallow scan: look for OpenJarvis one level inside common parent dirs. // This catches clones like ~/Documents/my-stuff/OpenJarvis without // needing to enumerate every possible intermediate folder. let scan_parents = [ format!("{home}/Documents"), format!("{home}/Desktop"), format!("{home}/Developer"), format!("{home}/projects"), format!("{home}/repos"), format!("{home}/src"), format!("{home}/Code"), format!("{home}/code"), format!("{home}/dev"), format!("{home}/github"), ]; for parent in &scan_parents { let parent_path = std::path::PathBuf::from(parent); if let Ok(entries) = std::fs::read_dir(&parent_path) { for entry in entries.flatten() { let candidate = entry.path().join("OpenJarvis"); if candidate.join("pyproject.toml").exists() { return Some(candidate); } // Also check if the entry itself is OpenJarvis (case-insensitive match) if let Some(name) = entry.file_name().to_str() { if name.eq_ignore_ascii_case("openjarvis") && entry.path().join("pyproject.toml").exists() { return Some(entry.path()); } } } } } None } // --------------------------------------------------------------------------- // BackendManager — owns the Ollama + Jarvis server child processes // --------------------------------------------------------------------------- struct ChildHandle { child: tokio::process::Child, } impl ChildHandle { async fn kill(&mut self) { let _ = self.child.kill().await; } } #[derive(Default)] struct BackendManager { ollama: Option, jarvis: Option, } impl BackendManager { async fn stop_all(&mut self) { if let Some(ref mut h) = self.jarvis { h.kill().await; } self.jarvis = None; if let Some(ref mut h) = self.ollama { h.kill().await; } self.ollama = None; } } type SharedBackend = Arc>; // --------------------------------------------------------------------------- // Setup status (reported to frontend) // --------------------------------------------------------------------------- #[derive(serde::Serialize, Clone)] struct SetupStatus { phase: String, detail: String, ollama_ready: bool, server_ready: bool, model_ready: bool, error: Option, } impl Default for SetupStatus { fn default() -> Self { Self { phase: "starting".into(), detail: "Initializing...".into(), ollama_ready: false, server_ready: false, model_ready: false, error: None, } } } type SharedStatus = Arc>; // --------------------------------------------------------------------------- // Health-check helpers // --------------------------------------------------------------------------- async fn wait_for_url(url: &str, timeout: Duration) -> bool { let client = reqwest::Client::builder() .timeout(Duration::from_secs(2)) .build() .unwrap(); let deadline = tokio::time::Instant::now() + timeout; while tokio::time::Instant::now() < deadline { if let Ok(resp) = client.get(url).send().await { if resp.status().is_success() { return true; } } tokio::time::sleep(Duration::from_millis(500)).await; } false } async fn ollama_has_model(model: &str) -> bool { let url = format!("http://127.0.0.1:{}/api/tags", OLLAMA_PORT); let client = reqwest::Client::builder() .timeout(Duration::from_secs(5)) .build() .unwrap(); if let Ok(resp) = client.get(&url).send().await { if let Ok(body) = resp.json::().await { if let Some(models) = body.get("models").and_then(|m| m.as_array()) { return models.iter().any(|m| { m.get("name") .and_then(|n| n.as_str()) .map(|n| { n == model || n.strip_suffix(":latest") == Some(model) || model.strip_suffix(":latest") == Some(n) }) .unwrap_or(false) }); } } } false } async fn pull_model(model: &str) -> Result<(), String> { let url = format!("http://127.0.0.1:{}/api/pull", OLLAMA_PORT); let client = reqwest::Client::builder() .timeout(Duration::from_secs(600)) .build() .map_err(|e| e.to_string())?; let resp = client .post(&url) .json(&serde_json::json!({"name": model, "stream": false})) .send() .await .map_err(|e| format!("Pull request failed: {}", e))?; if !resp.status().is_success() { return Err(format!("Pull returned status {}", resp.status())); } Ok(()) } // --------------------------------------------------------------------------- // Backend boot sequence (runs in background after app launch) // --------------------------------------------------------------------------- async fn boot_backend(backend: SharedBackend, status: SharedStatus) { // Phase 1: Start Ollama { let mut s = status.lock().await; s.phase = "ollama".into(); s.detail = "Starting inference engine...".into(); } // Try the bundled sidecar first, fall back to system ollama let ollama_child = { let ollama_bin = resolve_bin("ollama"); let sidecar = tokio::process::Command::new(&ollama_bin) .arg("serve") .env("OLLAMA_HOST", format!("127.0.0.1:{}", OLLAMA_PORT)) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .spawn(); match sidecar { Ok(child) => Some(child), Err(_) => None, } }; if let Some(child) = ollama_child { backend.lock().await.ollama = Some(ChildHandle { child }); } let ollama_url = format!("http://127.0.0.1:{}/api/tags", OLLAMA_PORT); let ollama_ok = wait_for_url(&ollama_url, Duration::from_secs(30)).await; if !ollama_ok { let mut s = status.lock().await; s.error = Some("Could not start Ollama. Install it from https://ollama.com".into()); return; } { let mut s = status.lock().await; s.ollama_ready = true; s.detail = "Inference engine ready.".into(); } // Phase 2: Pull one small model (qwen3.5:2b) so the app can open fast. // Remaining models are pulled in the background after the server starts. { let mut s = status.lock().await; s.phase = "model".into(); s.detail = format!("Checking for {}...", STARTUP_MODEL); } if !ollama_has_model(STARTUP_MODEL).await { { let mut s = status.lock().await; s.detail = format!("Downloading {}... (this may take a minute)", STARTUP_MODEL); } if let Err(e) = pull_model(STARTUP_MODEL).await { // If the startup model fails, try the tiny fallback eprintln!("Warning: failed to pull {}: {}", STARTUP_MODEL, e); if !ollama_has_model(FALLBACK_MODEL).await { let mut s = status.lock().await; s.detail = format!("Downloading {}...", FALLBACK_MODEL); drop(s); if let Err(e2) = pull_model(FALLBACK_MODEL).await { let mut s = status.lock().await; s.error = Some(format!("Failed to download model: {}", e2)); return; } } } } { let mut s = status.lock().await; s.model_ready = true; s.detail = "Model ready.".into(); } // Phase 3: Start jarvis serve { let mut s = status.lock().await; s.phase = "server".into(); s.detail = "Starting API server...".into(); } let uv_bin = resolve_bin("uv"); // Verify uv is actually installed if !std::path::Path::new(&uv_bin).exists() && uv_bin == "uv" { let mut s = status.lock().await; s.error = Some( "Could not find 'uv' (Python package manager). \ Install it from https://astral.sh/uv then relaunch." .into(), ); return; } let mut project_root = find_project_root(); if project_root.is_none() { // Auto-clone on first launch let git_bin = resolve_bin("git"); // Check that git is installed if !std::path::Path::new(&git_bin).exists() && git_bin == "git" { let mut s = status.lock().await; s.error = Some( "Could not find 'git'. \ Install it from https://git-scm.com then relaunch." .into(), ); return; } let target_path = std::path::PathBuf::from(home_dir()).join("OpenJarvis"); let clone_target = target_path.display().to_string(); // If the directory exists but is not a valid project, don't overwrite if target_path.exists() && !target_path.join("pyproject.toml").exists() { let mut s = status.lock().await; s.error = Some(format!( "{} exists but is not a valid OpenJarvis project. \ Remove it and relaunch, or set OPENJARVIS_ROOT to the correct path.", clone_target, )); return; } { let mut s = status.lock().await; s.detail = "Downloading OpenJarvis (first launch)...".into(); } let clone_result = tokio::process::Command::new(&git_bin) .args([ "clone", "--depth", "1", "https://github.com/open-jarvis/OpenJarvis.git", &clone_target, ]) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::piped()) .spawn(); match clone_result { Ok(child) => match child.wait_with_output().await { Ok(output) if output.status.success() => { project_root = Some(target_path); } Ok(output) => { let stderr = String::from_utf8_lossy(&output.stderr); let mut s = status.lock().await; s.error = Some(format!( "Failed to download OpenJarvis: {}. \ Clone manually: git clone https://github.com/open-jarvis/OpenJarvis.git {}", stderr.trim(), clone_target, )); return; } Err(e) => { let mut s = status.lock().await; s.error = Some(format!( "Failed to download OpenJarvis: {}. \ Clone manually: git clone https://github.com/open-jarvis/OpenJarvis.git {}", e, clone_target, )); return; } }, Err(e) => { let mut s = status.lock().await; s.error = Some(format!( "Could not run git: {}. \ Install git from https://git-scm.com then relaunch.", e, )); return; } } } // Kill any leftover server on our port from a previous run { let client = reqwest::Client::builder() .timeout(Duration::from_secs(2)) .build() .unwrap(); if client .get(format!("http://127.0.0.1:{}/health", JARVIS_PORT)) .send() .await .is_ok() { // Something is already listening — try to kill it #[cfg(unix)] { let _ = tokio::process::Command::new("fuser") .args(["-k", &format!("{}/tcp", JARVIS_PORT)]) .output() .await; tokio::time::sleep(Duration::from_secs(2)).await; } #[cfg(target_os = "windows")] { // Find the PID holding the port via netstat, then kill it if let Ok(output) = tokio::process::Command::new("cmd") .args(["/C", &format!( "for /f \"tokens=5\" %a in ('netstat -ano ^| findstr :{port} ^| findstr LISTENING') do taskkill /PID %a /F", port = JARVIS_PORT, )]) .output() .await { let _ = output; // best-effort } tokio::time::sleep(Duration::from_secs(2)).await; } } } // Start with STARTUP_MODEL (just pulled) or preferred if already available. let pref = preferred_model(); let startup_model = if ollama_has_model(pref).await { pref } else if ollama_has_model(STARTUP_MODEL).await { STARTUP_MODEL } else { FALLBACK_MODEL }; let root = project_root.as_ref().unwrap(); // Install dependencies automatically (handles fresh clones) { let mut s = status.lock().await; s.detail = "Installing dependencies...".into(); } let _ = tokio::process::Command::new(&uv_bin) .args(["sync", "--extra", "server"]) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .current_dir(root) .status() .await; { let mut s = status.lock().await; s.detail = format!( "Starting server with {} from {}...", startup_model, root.display(), ); } let mut cmd = tokio::process::Command::new(&uv_bin); cmd.args([ "run", "jarvis", "serve", "--port", &JARVIS_PORT.to_string(), "--model", startup_model, "--agent", "simple", ]) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::piped()) .current_dir(root); // Inject cloud API keys from ~/.openjarvis/cloud-keys.env for (key, value) in read_cloud_keys() { cmd.env(&key, &value); } let jarvis_child = cmd.spawn(); match jarvis_child { Ok(child) => { backend.lock().await.jarvis = Some(ChildHandle { child }); } Err(e) => { let mut s = status.lock().await; s.error = Some(format!( "Could not start jarvis server: {}. \ Make sure uv is installed (https://astral.sh/uv) and the OpenJarvis repo is cloned at {}", e, root.display(), )); return; } } let server_url = format!("http://127.0.0.1:{}/health", JARVIS_PORT); let server_ok = wait_for_url(&server_url, Duration::from_secs(600)).await; if !server_ok { // Try to read stderr from the failed process for a useful error let mut stderr_msg = String::new(); { let mut mgr = backend.lock().await; if let Some(ref mut h) = mgr.jarvis { if let Some(ref mut stderr) = h.child.stderr.take() { use tokio::io::AsyncReadExt; let mut buf = vec![0u8; 4096]; if let Ok(n) = stderr.read(&mut buf).await { stderr_msg = String::from_utf8_lossy(&buf[..n]).to_string(); } } } } let detail = if stderr_msg.is_empty() { format!( "Jarvis server did not start. Check that:\n\ 1. uv is installed ({})\n\ 2. The OpenJarvis repo is at {}\n\ 3. Run 'uv sync' in that directory", uv_bin, root.display(), ) } else { format!("Server failed to start: {}", stderr_msg.trim()) }; let mut s = status.lock().await; s.error = Some(detail); return; } { let mut s = status.lock().await; s.server_ready = true; s.phase = "ready".into(); s.detail = "All systems ready.".into(); } // Phase 4: Pull remaining Qwen3.5 models in the background. // The app is already usable with qwen3.5:2b; as each model finishes // it appears in the model list automatically. let fitting = models_that_fit(); tokio::spawn(async move { for model in fitting { if model != STARTUP_MODEL && model != FALLBACK_MODEL { if !ollama_has_model(model).await { let _ = pull_model(model).await; } } } }); } // --------------------------------------------------------------------------- // Tauri commands // --------------------------------------------------------------------------- fn api_base() -> String { format!("http://127.0.0.1:{}", JARVIS_PORT) } #[tauri::command] async fn get_setup_status(state: tauri::State<'_, SharedStatus>) -> Result { Ok(state.lock().await.clone()) } #[tauri::command] fn get_api_base() -> String { api_base() } #[tauri::command] async fn start_backend( backend: tauri::State<'_, SharedBackend>, status: tauri::State<'_, SharedStatus>, ) -> Result<(), String> { let b = backend.inner().clone(); let s = status.inner().clone(); tauri::async_runtime::spawn(boot_backend(b, s)); Ok(()) } #[tauri::command] async fn stop_backend(backend: tauri::State<'_, SharedBackend>) -> Result<(), String> { backend.lock().await.stop_all().await; Ok(()) } #[tauri::command] async fn check_health(api_url: String) -> Result { let url = format!( "{}/health", if api_url.is_empty() { api_base() } else { api_url } ); let resp = reqwest::get(&url) .await .map_err(|e| format!("Connection failed: {}", e))?; resp.json() .await .map_err(|e| format!("Invalid response: {}", e)) } #[tauri::command] async fn fetch_energy(api_url: String) -> Result { let base = if api_url.is_empty() { api_base() } else { api_url }; let resp = reqwest::get(format!("{}/v1/telemetry/energy", base)) .await .map_err(|e| format!("Connection failed: {}", e))?; resp.json() .await .map_err(|e| format!("Invalid response: {}", e)) } #[tauri::command] async fn fetch_telemetry(api_url: String) -> Result { let base = if api_url.is_empty() { api_base() } else { api_url }; let resp = reqwest::get(format!("{}/v1/telemetry/stats", base)) .await .map_err(|e| format!("Connection failed: {}", e))?; resp.json() .await .map_err(|e| format!("Invalid response: {}", e)) } #[tauri::command] async fn fetch_traces(api_url: String, limit: u32) -> Result { let base = if api_url.is_empty() { api_base() } else { api_url }; let resp = reqwest::get(format!("{}/v1/traces?limit={}", base, limit)) .await .map_err(|e| format!("Connection failed: {}", e))?; resp.json() .await .map_err(|e| format!("Invalid response: {}", e)) } #[tauri::command] async fn fetch_trace(api_url: String, trace_id: String) -> Result { let base = if api_url.is_empty() { api_base() } else { api_url }; let resp = reqwest::get(format!("{}/v1/traces/{}", base, trace_id)) .await .map_err(|e| format!("Connection failed: {}", e))?; resp.json() .await .map_err(|e| format!("Invalid response: {}", e)) } #[tauri::command] async fn fetch_learning_stats(api_url: String) -> Result { let base = if api_url.is_empty() { api_base() } else { api_url }; let resp = reqwest::get(format!("{}/v1/learning/stats", base)) .await .map_err(|e| format!("Connection failed: {}", e))?; resp.json() .await .map_err(|e| format!("Invalid response: {}", e)) } #[tauri::command] async fn fetch_learning_policy(api_url: String) -> Result { let base = if api_url.is_empty() { api_base() } else { api_url }; let resp = reqwest::get(format!("{}/v1/learning/policy", base)) .await .map_err(|e| format!("Connection failed: {}", e))?; resp.json() .await .map_err(|e| format!("Invalid response: {}", e)) } #[tauri::command] async fn fetch_memory_stats(api_url: String) -> Result { let base = if api_url.is_empty() { api_base() } else { api_url }; let resp = reqwest::get(format!("{}/v1/memory/stats", base)) .await .map_err(|e| format!("Connection failed: {}", e))?; resp.json() .await .map_err(|e| format!("Invalid response: {}", e)) } #[tauri::command] async fn search_memory( api_url: String, query: String, top_k: u32, ) -> Result { let base = if api_url.is_empty() { api_base() } else { api_url }; let client = reqwest::Client::new(); let resp = client .post(format!("{}/v1/memory/search", base)) .json(&serde_json::json!({"query": query, "top_k": top_k})) .send() .await .map_err(|e| format!("Connection failed: {}", e))?; resp.json() .await .map_err(|e| format!("Invalid response: {}", e)) } #[tauri::command] async fn fetch_agents(api_url: String) -> Result { let base = if api_url.is_empty() { api_base() } else { api_url }; let resp = reqwest::get(format!("{}/v1/agents", base)) .await .map_err(|e| format!("Connection failed: {}", e))?; resp.json() .await .map_err(|e| format!("Invalid response: {}", e)) } #[tauri::command] async fn fetch_models(api_url: String) -> Result { let base = if api_url.is_empty() { api_base() } else { api_url }; let resp = reqwest::get(format!("{}/v1/models", base)) .await .map_err(|e| format!("Connection failed: {}", e))?; resp.json() .await .map_err(|e| format!("Invalid response: {}", e)) } #[tauri::command] async fn run_jarvis_command(args: Vec) -> Result { let mut cmd_args = vec!["run".to_string(), "jarvis".to_string()]; cmd_args.extend(args); let uv_bin = resolve_bin("uv"); let output = tokio::process::Command::new(&uv_bin) .args(&cmd_args) .output() .await .map_err(|e| format!("Failed to launch jarvis: {}", e))?; if output.status.success() { Ok(String::from_utf8_lossy(&output.stdout).to_string()) } else { Err(String::from_utf8_lossy(&output.stderr).to_string()) } } #[tauri::command] async fn fetch_savings(api_url: String) -> Result { let base = if api_url.is_empty() { api_base() } else { api_url }; let resp = reqwest::get(format!("{}/v1/savings", base)) .await .map_err(|e| format!("Connection failed: {}", e))?; resp.json() .await .map_err(|e| format!("Invalid response: {}", e)) } /// Transcribe audio via the speech API endpoint. #[tauri::command] async fn transcribe_audio( api_url: String, audio_data: Vec, filename: String, ) -> Result { let url = format!("{}/v1/speech/transcribe", api_url); let client = reqwest::Client::new(); let part = reqwest::multipart::Part::bytes(audio_data) .file_name(filename) .mime_str("audio/webm") .map_err(|e| format!("Failed to create multipart: {}", e))?; let form = reqwest::multipart::Form::new().part("file", part); let resp = client .post(&url) .multipart(form) .send() .await .map_err(|e| format!("Connection failed: {}", e))?; let body: serde_json::Value = resp .json() .await .map_err(|e| format!("Invalid response: {}", e))?; Ok(body) } /// Submit savings to Supabase leaderboard. #[tauri::command] async fn submit_savings( supabase_url: String, supabase_key: String, payload: serde_json::Value, ) -> Result { if supabase_url.is_empty() || supabase_key.is_empty() { return Ok(false); } let client = reqwest::Client::new(); let resp = client .post(format!( "{}/rest/v1/savings_entries?on_conflict=anon_id", supabase_url )) .header("Content-Type", "application/json") .header("apikey", &supabase_key) .header("Authorization", format!("Bearer {}", supabase_key)) .header("Prefer", "resolution=merge-duplicates") .json(&payload) .send() .await .map_err(|e| format!("Supabase POST failed: {}", e))?; Ok(resp.status().is_success()) } // --------------------------------------------------------------------------- // Cloud API key management // --------------------------------------------------------------------------- /// Path to the cloud keys file (~/.openjarvis/cloud-keys.env). fn cloud_keys_path() -> std::path::PathBuf { let home = home_dir(); std::path::PathBuf::from(home) .join(".openjarvis") .join("cloud-keys.env") } /// Read cloud keys from disk and return as key=value pairs. fn read_cloud_keys() -> Vec<(String, String)> { let path = cloud_keys_path(); let mut keys = Vec::new(); if let Ok(contents) = std::fs::read_to_string(&path) { for line in contents.lines() { let line = line.trim(); if line.is_empty() || line.starts_with('#') { continue; } if let Some((k, v)) = line.split_once('=') { keys.push((k.trim().to_string(), v.trim().to_string())); } } } keys } /// Save a single cloud API key to the keys file. #[tauri::command] async fn save_cloud_key(key_name: String, key_value: String) -> Result<(), String> { let path = cloud_keys_path(); // Ensure directory exists if let Some(parent) = path.parent() { let _ = std::fs::create_dir_all(parent); } // Read existing keys, update/add the one being saved let mut keys: Vec<(String, String)> = read_cloud_keys() .into_iter() .filter(|(k, _)| k != &key_name) .collect(); if !key_value.is_empty() { keys.push((key_name, key_value)); } // Write back let content: String = keys .iter() .map(|(k, v)| format!("{}={}", k, v)) .collect::>() .join("\n"); std::fs::write(&path, content + "\n").map_err(|e| format!("Failed to save key: {}", e))?; // Set permissions to owner-only (chmod 600) #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)); } Ok(()) } /// Get which cloud providers have keys configured (without exposing values). #[tauri::command] async fn get_cloud_key_status() -> Result { let keys = read_cloud_keys(); let status: Vec = keys .iter() .map(|(k, v)| serde_json::json!({ "key": k, "set": !v.is_empty() })) .collect(); Ok(serde_json::json!(status)) } /// Pull a model via Ollama (called from frontend download button). #[tauri::command] async fn pull_ollama_model(model_name: String) -> Result { pull_model(&model_name) .await .map_err(|e| format!("Failed to pull {}: {}", model_name, e))?; Ok(serde_json::json!({"status": "ok", "model": model_name})) } /// Delete a model from Ollama. #[tauri::command] async fn delete_ollama_model(model_name: String) -> Result { let url = format!("http://127.0.0.1:{}/api/delete", OLLAMA_PORT); let client = reqwest::Client::builder() .timeout(Duration::from_secs(30)) .build() .map_err(|e| e.to_string())?; let resp = client .delete(&url) .json(&serde_json::json!({"name": model_name})) .send() .await .map_err(|e| format!("Delete failed: {}", e))?; if !resp.status().is_success() { return Err(format!("Delete returned status {}", resp.status())); } Ok(serde_json::json!({"status": "deleted", "model": model_name})) } /// Check speech backend health. #[tauri::command] async fn speech_health(api_url: String) -> Result { let url = format!("{}/v1/speech/health", api_url); let resp = reqwest::get(&url) .await .map_err(|e| format!("Connection failed: {}", e))?; let body: serde_json::Value = resp .json() .await .map_err(|e| format!("Invalid response: {}", e))?; Ok(body) } // --------------------------------------------------------------------------- // App entry point // --------------------------------------------------------------------------- #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let backend: SharedBackend = Arc::new(Mutex::new(BackendManager::default())); let status: SharedStatus = Arc::new(Mutex::new(SetupStatus::default())); let boot_backend_ref = backend.clone(); let boot_status_ref = status.clone(); tauri::Builder::default() .manage(backend.clone()) .manage(status.clone()) .plugin(tauri_plugin_notification::init()) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_global_shortcut::Builder::new().build()) .plugin(tauri_plugin_autostart::init( MacosLauncher::LaunchAgent, Some(vec!["--hidden"]), )) .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { if let Some(window) = app.get_webview_window("main") { let _ = window.set_focus(); } })) .setup(move |app| { // System tray let show = MenuItemBuilder::with_id("show", "Show / Hide").build(app)?; let health = MenuItemBuilder::with_id("health", "Health: starting...") .enabled(false) .build(app)?; let quit = MenuItemBuilder::with_id("quit", "Quit OpenJarvis").build(app)?; let menu = MenuBuilder::new(app) .item(&show) .separator() .item(&health) .separator() .item(&quit) .build()?; let _tray = TrayIconBuilder::with_id("main") .icon(app.default_window_icon().unwrap().clone()) .tooltip("OpenJarvis") .menu(&menu) .on_menu_event(move |app, event| match event.id().as_ref() { "show" => { if let Some(window) = app.get_webview_window("main") { if window.is_visible().unwrap_or(false) { let _ = window.hide(); } else { let _ = window.show(); let _ = window.set_focus(); } } } "quit" => { app.exit(0); } _ => {} }) .build(app)?; // Auto-start backend services on launch tauri::async_runtime::spawn(boot_backend(boot_backend_ref, boot_status_ref)); Ok(()) }) .invoke_handler(tauri::generate_handler![ get_setup_status, get_api_base, start_backend, stop_backend, check_health, fetch_energy, fetch_telemetry, fetch_traces, fetch_trace, fetch_learning_stats, fetch_learning_policy, fetch_memory_stats, search_memory, fetch_agents, fetch_models, run_jarvis_command, fetch_savings, submit_savings, transcribe_audio, speech_health, pull_ollama_model, delete_ollama_model, save_cloud_key, get_cloud_key_status, ]) .build(tauri::generate_context!()) .expect("error while building OpenJarvis Desktop") .run(move |_app, event| { if let tauri::RunEvent::ExitRequested { .. } = event { let b = backend.clone(); tauri::async_runtime::spawn(async move { b.lock().await.stop_all().await; }); } }); }