diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 60d8cb3..ec07102 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1360,6 +1360,136 @@ pub async fn get_claude_version() -> Result { } } +// ==================== Auth Commands ==================== + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ClaudeAuthStatus { + pub is_logged_in: bool, + pub email: Option, + pub org_name: Option, + pub api_key_source: Option, + pub api_provider: Option, + pub subscription_type: Option, +} + +#[tauri::command] +pub async fn get_auth_status() -> Result { + tracing::debug!("Getting Claude auth status"); + + let output = create_claude_command() + .args(["auth", "status"]) + .output() + .map_err(|e| format!("Failed to run claude auth status: {}", e))?; + + let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + let raw = if stdout.is_empty() { &stderr } else { &stdout }; + + if let Ok(json) = serde_json::from_str::(raw) { + let is_logged_in = json + .get("loggedIn") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + let email = json + .get("email") + .and_then(|v| v.as_str()) + .map(String::from); + + let org_name = json + .get("orgName") + .and_then(|v| v.as_str()) + .map(String::from); + + let api_key_source = json + .get("apiKeySource") + .and_then(|v| v.as_str()) + .map(String::from); + + let api_provider = json + .get("apiProvider") + .and_then(|v| v.as_str()) + .map(String::from); + + let subscription_type = json + .get("subscriptionType") + .and_then(|v| v.as_str()) + .map(String::from); + + tracing::info!("Claude auth status: logged_in={}", is_logged_in); + Ok(ClaudeAuthStatus { + is_logged_in, + email, + org_name, + api_key_source, + api_provider, + subscription_type, + }) + } else { + // Non-JSON output: fall back to heuristic + let lower = raw.to_lowercase(); + let is_logged_in = output.status.success() + && !lower.contains("not logged in") + && !lower.contains("not authenticated") + && !lower.contains("no account"); + tracing::info!("Claude auth status (non-JSON): logged_in={}", is_logged_in); + Ok(ClaudeAuthStatus { + is_logged_in, + email: None, + org_name: None, + api_key_source: None, + api_provider: None, + subscription_type: None, + }) + } +} + +#[tauri::command] +pub async fn auth_login() -> Result { + tracing::info!("Running claude auth login"); + + let output = create_claude_command() + .args(["auth", "login"]) + .output() + .map_err(|e| format!("Failed to run claude auth login: {}", e))?; + + let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + + if output.status.success() { + let message = if stdout.is_empty() { "Login successful".to_string() } else { stdout }; + tracing::info!("Claude auth login succeeded"); + Ok(message) + } else { + let error = if stderr.is_empty() { stdout } else { stderr }; + tracing::error!("Claude auth login failed: {}", error); + Err(format!("Login failed: {}", error)) + } +} + +#[tauri::command] +pub async fn auth_logout() -> Result { + tracing::info!("Running claude auth logout"); + + let output = create_claude_command() + .args(["auth", "logout"]) + .output() + .map_err(|e| format!("Failed to run claude auth logout: {}", e))?; + + let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + + if output.status.success() { + let message = if stdout.is_empty() { "Logged out successfully".to_string() } else { stdout }; + tracing::info!("Claude auth logout succeeded"); + Ok(message) + } else { + let error = if stderr.is_empty() { stdout } else { stderr }; + tracing::error!("Claude auth logout failed: {}", error); + Err(format!("Logout failed: {}", error)) + } +} + // ==================== Plugin Management Commands ==================== #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6c9c1a8..70aea97 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -195,6 +195,9 @@ pub fn run() { close_application, list_memory_files, get_claude_version, + get_auth_status, + auth_login, + auth_logout, list_plugins, install_plugin, uninstall_plugin, diff --git a/src/lib/components/ConfigSidebar.svelte b/src/lib/components/ConfigSidebar.svelte index 80ed171..014931f 100644 --- a/src/lib/components/ConfigSidebar.svelte +++ b/src/lib/components/ConfigSidebar.svelte @@ -12,6 +12,7 @@ } from "$lib/stores/config"; import { claudeStore } from "$lib/stores/claude"; import { getCurrentWindow } from "@tauri-apps/api/window"; + import { invoke } from "@tauri-apps/api/core"; import CostSummary from "./CostSummary.svelte"; let config: HikariConfig = $state({ @@ -56,6 +57,20 @@ let showCustomThemeEditor = $state(false); + interface AuthStatus { + is_logged_in: boolean; + email: string | null; + org_name: string | null; + api_key_source: string | null; + api_provider: string | null; + subscription_type: string | null; + } + + let authStatus: AuthStatus | null = $state(null); + let authLoading = $state(false); + let authActionLoading = $state(false); + let authError: string | null = $state(null); + let isOpen = $state(false); let isSaving = $state(false); let saveError: string | null = $state(null); @@ -69,6 +84,9 @@ configStore.isSidebarOpen.subscribe((open) => { isOpen = open; + if (open && authStatus === null) { + void refreshAuthStatus(); + } }); configStore.saveError.subscribe((error) => { @@ -111,6 +129,44 @@ "Task", ]; + async function refreshAuthStatus() { + authLoading = true; + authError = null; + try { + authStatus = await invoke("get_auth_status"); + } catch (e) { + authError = String(e); + } finally { + authLoading = false; + } + } + + async function handleAuthLogin() { + authActionLoading = true; + authError = null; + try { + await invoke("auth_login"); + await refreshAuthStatus(); + } catch (e) { + authError = String(e); + } finally { + authActionLoading = false; + } + } + + async function handleAuthLogout() { + authActionLoading = true; + authError = null; + try { + await invoke("auth_logout"); + await refreshAuthStatus(); + } catch (e) { + authError = String(e); + } finally { + authActionLoading = false; + } + } + async function handleSave() { isSaving = true; saveError = null; @@ -228,6 +284,101 @@ {/if} + +
+

+ Account +

+ + {#if authLoading} +
Checking auth status...
+ {:else if authStatus} +
+ + + {authStatus.is_logged_in ? "Logged in" : "Not logged in"} + +
+ {#if authStatus.email || authStatus.org_name || authStatus.api_key_source || config.api_key} +
+ {#if authStatus.email} +
+
Email
+
{authStatus.email}
+
+ {/if} + {#if authStatus.org_name} +
+
Org
+
{authStatus.org_name}
+
+ {/if} + {#if authStatus.api_key_source} +
+
API key
+
{authStatus.api_key_source}
+
+ {/if} + {#if authStatus.subscription_type} +
+
Plan
+
{authStatus.subscription_type}
+
+ {/if} +
+
Override
+
+ {#if config.api_key} + {config.streamer_mode ? "Custom key set 🔒" : "Custom key set"} + {:else} + None + {/if} +
+
+
+ {/if} + {:else} +
Auth status unavailable
+ {/if} + + {#if authError} +
+ {authError} +
+ {/if} + +
+ + {#if authStatus && !authStatus.is_logged_in} + + {:else if authStatus && authStatus.is_logged_in} + + {/if} +
+
+