import os import sys import base64 import json import sqlite3 import shutil import time import subprocess import psutil from datetime import datetime import httpx from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cli.proto_utils import create_unified_oauth_token, create_string_field, encode_varint, create_timestamp_field try: import winreg except ImportError: winreg = None # API Constants from GoogleAPIService.ts CLIENT_ID = '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com' CLIENT_SECRET = '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' USER_AGENT = 'antigravity/1.11.3 Darwin/arm64' URL_TOKEN = 'https://oauth2.googleapis.com/token' URL_QUOTA = 'https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels' URL_LOAD_PROJECT = 'https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist' # Config DB_NAME = 'cloud_accounts.db' # Try to find user data path if sys.platform == 'win32': APPDATA = os.getenv('APPDATA') USER_DATA_DIR = os.path.join(APPDATA, 'AntigravityManager') ANTIGRAVITY_APP_DATA = os.path.join(APPDATA, 'Antigravity IDE') # Or whatever the IDE name is else: # Fallback for non-windows (though user is on windows) USER_DATA_DIR = os.path.expanduser('~/.config/AntigravityManager') def find_antigravity_executable(): """Smartly find the Antigravity IDE executable path.""" # 1. Try to find if it's already running (most reliable) for proc in psutil.process_iter(['name', 'exe']): try: if proc.info['name'] and 'antigravity' in proc.info['name'].lower() and not 'manager' in proc.info['name'].lower(): if proc.info['exe'] and os.path.exists(proc.info['exe']): return proc.info['exe'] except: pass # 2. Standard installation paths local_appdata = os.getenv('LOCALAPPDATA', '') prog_files = os.getenv('ProgramFiles', 'C:\\Program Files') candidates = [ # User-specific install os.path.join(local_appdata, 'Programs', 'Antigravity', 'Antigravity.exe'), # System-wide install os.path.join(prog_files, 'Antigravity', 'Antigravity.exe'), # Developer/Custom paths (A: drive etc.) r"A:\Antigravity\Antigravity.exe", r"A:\UnityProjects\ManagerFork\AntigravityManager\out\Antigravity Manager-win32-x64\Antigravity Manager.exe" ] for path in candidates: if path and os.path.exists(path): return path return None # Global path determined at runtime ANTIGRAVITY_EXE_PATH = find_antigravity_executable() CLOUD_DB_PATH = os.path.join(USER_DATA_DIR, DB_NAME) # We need to decrypt safeStorage try: import win32crypt except ImportError: win32crypt = None def decrypt_safe_storage(encrypted_data): if not win32crypt: raise ImportError("win32crypt module is missing. Install pywin32.") try: # data is iv:authTag:ciphertext # Wait, if it's safeStorage, the *key* in .mk is encrypted with DPAPI. # But the DB content is AES-256-GCM encrypted with that key. pass except Exception as e: print(f"Decryption error: {e}") return None def get_data_dirs(): home = os.path.expanduser('~') appdata = os.getenv('APPDATA') return [ os.path.join(home, '.antigravity-agent'), os.path.join(appdata, 'Antigravity Manager'), os.path.join(appdata, 'AntigravityManager'), os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '.config')), os.path.abspath(os.path.join(os.path.dirname(__file__), '..')), ] def find_db_path(): for d in get_data_dirs(): p = os.path.join(d, DB_NAME) if os.path.exists(p): return p return None def get_master_key(): data_dirs = get_data_dirs() # 1. We need the OSCrypt key from "Local State" oscrypt_key = None for d in data_dirs: ls_path = os.path.join(d, 'Local State') if os.path.exists(ls_path): try: with open(ls_path, 'r', encoding='utf-8') as f: ls_data = json.load(f) b64_key = ls_data['os_crypt']['encrypted_key'] encrypted_key = base64.b64decode(b64_key) # Remove "DPAPI" fixed prefix (5 bytes) raw_key = encrypted_key[5:] # Decrypt with DPAPI _, master_key_v10 = win32crypt.CryptUnprotectData(raw_key, None, None, None, 0) oscrypt_key = master_key_v10 # print(f"[DEBUG] Found OSCrypt key in {ls_path}") break except Exception as e: # print(f"[DEBUG] Failed to get OSCrypt key from {ls_path}: {e}") continue # 2. Now decrypt the .mk file using oscrypt_key for d in data_dirs: p = os.path.join(d, '.mk') if os.path.exists(p): with open(p, 'rb') as f: blob = f.read() try: # Check for "v10" prefix if blob.startswith(b'v10'): if not oscrypt_key: # print("[ERROR] Found v10 Master Key but no OSCrypt key to decrypt it.") continue # Format: v10 (3) + nonce (12) + encrypted_data nonce = blob[3:15] ciphertext = blob[15:] aesgcm = AESGCM(oscrypt_key) decrypted = aesgcm.decrypt(nonce, ciphertext, None) key_hex = decrypted.decode('utf-8') return bytes.fromhex(key_hex) else: # Legacy DPAPI-only format _, decrypted_bytes = win32crypt.CryptUnprotectData(blob, None, None, None, 0) key_hex = decrypted_bytes.decode('utf-8') return bytes.fromhex(key_hex) except Exception as e: # print(f"[ERROR] Failed to decrypt .mk at {p}: {e}") continue print("[ERROR] No valid Master Key found (OSCrypt/DPAPI failed).") return None def decrypt_db_value(value, master_key): if not value or not isinstance(value, str): return value if value.startswith('{') or value.startswith('['): return value # Already plain parts = value.split(':') if len(parts) != 3: # print(f"[DEBUG] Encrypted value has unexpected format: {value[:20]}...") return value iv_hex, tag_hex, cipher_hex = parts try: iv = bytes.fromhex(iv_hex) tag = bytes.fromhex(tag_hex) ciphertext = bytes.fromhex(cipher_hex) aesgcm = AESGCM(master_key) # Python's AESGCM.decrypt takes nonce and data (ciphertext + tag). return aesgcm.decrypt(iv, ciphertext + tag, None).decode('utf-8') except Exception as e: # print(f"[DEBUG] AES Decrypt fail: {e}") return None def encrypt_db_value(text: str, master_key: bytes) -> str: import secrets iv = secrets.token_bytes(16) aesgcm = AESGCM(master_key) # Python cryptography AESGCM.encrypt returns ciphertext + tag combined = aesgcm.encrypt(iv, text.encode('utf-8'), None) # Node.js format: iv_hex:auth_tag_hex:ciphertext_hex tag = combined[-16:] ciphertext = combined[:-16] return f"{iv.hex()}:{tag.hex()}:{ciphertext.hex()}" def get_accounts(): db_path = find_db_path() if not db_path: print(f"[ERROR] Could not find {DB_NAME}. Searched standard locations.") # Debug info print(f"APPDATA: {os.getenv('APPDATA')}") return [] # print(f"[DEBUG] Using DB: {db_path}") # Uncomment if needed try: conn = sqlite3.connect(db_path) except sqlite3.OperationalError as e: print(f"[ERROR] Failed to open DB at {db_path}: {e}") return [] conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute("SELECT * FROM accounts ORDER BY last_used DESC") rows = cursor.fetchall() master_key = get_master_key() accounts = [] for row in rows: acc = dict(row) if master_key: token_json = decrypt_db_value(acc['token_json'], master_key) if token_json: acc['token'] = json.loads(token_json) quota_json = decrypt_db_value(acc['quota_json'], master_key) if quota_json: acc['quota'] = json.loads(quota_json) accounts.append(acc) conn.close() return accounts def get_antigravity_db_path(): # Logic from paths.ts # On Windows: %APPDATA%/Antigravity IDE/User/state.vscdb # Or %APPDATA%/Code/User/state.vscdb (if standard VSCode) # The user is likely using a custom build so folder might be 'Antigravity IDE' or similar. # Let's search standard locations. candidates = [ os.path.join(APPDATA, 'Antigravity', 'User', 'globalStorage', 'state.vscdb'), os.path.join(APPDATA, 'Antigravity IDE', 'User', 'globalStorage', 'state.vscdb'), os.path.join(APPDATA, 'Antigravity IDE', 'User', 'state.vscdb'), os.path.join(APPDATA, 'Code', 'User', 'state.vscdb'), os.path.join(APPDATA, 'Antigravity', 'User', 'state.vscdb'), os.path.join(APPDATA, 'Antigravity', 'state.vscdb'), ] for p in candidates: if os.path.exists(p): return p return None def inject_token(account): db_path = get_antigravity_db_path() if not db_path: print("Antigravity DB not found!") return False # Backup try: shutil.copy2(db_path, db_path + ".backup") except Exception as e: print(f"Backup failed: {e}") return False # Inject with transaction for atomicity token_data = account.get('token', {}) access_token = token_data.get('access_token') refresh_token = token_data.get('refresh_token') expiry = token_data.get('expiry_timestamp') if not access_token or not refresh_token: print("Missing token data") return False value_b64 = create_unified_oauth_token(access_token, refresh_token, expiry) # Write to SQLite with transaction try: conn = sqlite3.connect(db_path) cursor = conn.cursor() # Start transaction cursor.execute("BEGIN") try: # Ensure table exists (it should) cursor.execute("CREATE TABLE IF NOT EXISTS ItemTable (key TEXT PRIMARY KEY, value TEXT)") # Upsert key = 'antigravityUnifiedStateSync.oauthToken' cursor.execute("INSERT OR REPLACE INTO ItemTable (key, value) VALUES (?, ?)", (key, value_b64)) # Also clean up auth status auth_status = json.dumps({ "name": account.get('name') or account.get('email'), "email": account['email'], "apiKey": access_token }) cursor.execute("INSERT OR REPLACE INTO ItemTable (key, value) VALUES (?, ?)", ('antigravityAuthStatus', auth_status)) cursor.execute("INSERT OR REPLACE INTO ItemTable (key, value) VALUES (?, ?)", ('antigravityOnboarding', 'true')) cursor.execute("DELETE FROM ItemTable WHERE key = ?", ('google.antigravity',)) # Commit transaction conn.commit() print(f"Injected token for {account['email']}") return True except Exception as e: # Rollback on error conn.rollback() print(f"Injection failed, rolled back: {e}") return False finally: conn.close() except Exception as e: print(f"Database connection failed: {e}") return False def kill_process(name_hints=["Antigravity", "Antigravity Manager"]): # On Windows it might be Antigravity.exe or Code.exe killed = False for proc in psutil.process_iter(['pid', 'name']): try: pname = proc.info['name'] if pname and any(hint.lower() in pname.lower() for hint in name_hints): proc.kill() killed = True except: pass return killed def start_process(): # Re-scan in case it was just closed/installed exe_path = ANTIGRAVITY_EXE_PATH or find_antigravity_executable() if not exe_path or not os.path.exists(exe_path): print(f"Error: Could not locate Antigravity executable.") return False print(f"Starting {exe_path}...") try: # DETACHED_PROCESS = 0x00000008 ensures the app lives after CLI exits subprocess.Popen([exe_path], creationflags=0x00000008, close_fds=True) return True except Exception as e: print(f"Failed to start process: {e}") return False def switch_account(email_pattern): accounts = get_accounts() target = None for acc in accounts: if email_pattern.lower() in acc['email'].lower(): target = acc break if not target: print(f"Account matching '{email_pattern}' not found.") return False print(f"Switching to {target['email']}...") # 1. Kill Process kill_process(["Antigravity.exe", "Antigravity"]) time.sleep(1.0) # Give it a second # 2. Inject if inject_token(target): # 3. Start Process start_process() return True return False async def refresh_access_token(refresh_token: str) -> dict: async with httpx.AsyncClient(timeout=30.0) as client: params = { 'client_id': CLIENT_ID, 'client_secret': CLIENT_SECRET, 'refresh_token': refresh_token, 'grant_type': 'refresh_token', } resp = await client.post(URL_TOKEN, data=params) resp.raise_for_status() return resp.json() async def fetch_project_id(access_token: str) -> str: async with httpx.AsyncClient(timeout=30.0) as client: headers = { 'Authorization': f'Bearer {access_token}', 'User-Agent': USER_AGENT, 'Content-Type': 'application/json', } body = {'metadata': {'ideType': 'ANTIGRAVITY'}} resp = await client.post(URL_LOAD_PROJECT, json=body, headers=headers) if resp.is_success: return resp.json().get('cloudaicompanionProject') return None async def fetch_live_quota(access_token: str) -> dict: project_id = await fetch_project_id(access_token) async with httpx.AsyncClient(timeout=30.0) as client: headers = { 'Authorization': f'Bearer {access_token}', 'User-Agent': USER_AGENT, 'Content-Type': 'application/json', } payload = {} if project_id: payload['project'] = project_id resp = await client.post(URL_QUOTA, json=payload, headers=headers) resp.raise_for_status() raw_data = resp.json() result = {'models': {}} for name, info in raw_data.get('models', {}).items(): q_info = info.get('quotaInfo') if q_info: fraction = q_info.get('remainingFraction', 0) result['models'][name] = { 'percentage': int(fraction * 100), 'resetTime': q_info.get('resetTime', '') } return result async def update_account_quota_live(email: str): accounts = get_accounts() target = next((a for a in accounts if email.lower() in a['email'].lower()), None) if not target: print(f"[ERROR] Account matching '{email}' not found in database.") return if not target.get('token'): print(f"[ERROR] Found account {target['email']}, but could not decrypt its token.") print("Possible reason: Python CLI cannot access the Master Key or DPAPI is locked.") return print(f"Refreshing quota for {target['email']}...") try: # 1. Refresh token tokens = await refresh_access_token(target['token']['refresh_token']) new_access_token = tokens['access_token'] # 2. Fetch quota new_quota = await fetch_live_quota(new_access_token) # 3. Save to DB db_path = find_db_path() master_key = get_master_key() if not master_key: print("Cannot update DB: Master Key missing.") return encrypted_quota = encrypt_db_value(json.dumps(new_quota), master_key) conn = sqlite3.connect(db_path) cursor = conn.cursor() cursor.execute("UPDATE accounts SET quota_json = ? WHERE email = ?", (encrypted_quota, target['email'])) conn.commit() conn.close() print("Quota updated successfully.") except Exception as e: print(f"Failed to refresh quota: {e}") def add_to_windows_path(path_to_add: str) -> bool: if not winreg: print("Registry access not available on this platform.") return False try: # User Environment Path in Registry key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r'Environment', 0, winreg.KEY_ALL_ACCESS) try: current_path, _ = winreg.QueryValueEx(key, 'Path') except FileNotFoundError: current_path = '' if path_to_add in current_path: print(f"Path already contains: {path_to_add}") return True new_path = current_path if new_path and not new_path.endswith(';'): new_path += ';' new_path += path_to_add winreg.SetValueEx(key, 'Path', 0, winreg.REG_EXPAND_SZ, new_path) winreg.CloseKey(key) # Notify system about Environment changes (standard Windows behavior) import ctypes HWND_BROADCAST = 0xFFFF WM_SETTINGCHANGE = 0x001A ctypes.windll.user32.SendMessageTimeoutW(HWND_BROADCAST, WM_SETTINGCHANGE, 0, 'Environment', 0, 1000, 0) return True except Exception as e: print(f"Failed to update PATH: {e}") return False def get_ide_accounts(): """Read accounts from IDE's state.vscdb database.""" db_path = get_antigravity_db_path() if not db_path: print("[ERROR] Could not find IDE database.") return [] try: conn = sqlite3.connect(db_path) conn.row_factory = sqlite3.Row cursor = conn.cursor() # Read the storage JSON that contains account info cursor.execute("SELECT value FROM ItemTable WHERE key = 'antigravityAuthStatus'") row = cursor.fetchone() conn.close() if not row: return [] auth_data = json.loads(row['value']) # IDE stores current active account, not a full list # We'll return it as a single-item list for consistency return [{ 'email': auth_data.get('email', ''), 'name': auth_data.get('name', ''), 'source': 'ide' }] except Exception as e: print(f"[ERROR] Failed to read IDE accounts: {e}") return [] def sync_accounts_from_ide(): """Import accounts from IDE to CLI database.""" ide_accounts = get_ide_accounts() cli_accounts = get_accounts() if not ide_accounts: print("No accounts found in IDE.") return cli_emails = {acc['email'] for acc in cli_accounts} new_accounts = [acc for acc in ide_accounts if acc['email'] not in cli_emails] if not new_accounts: print("All IDE accounts are already in CLI database.") return print(f"Found {len(new_accounts)} new account(s) in IDE:") for acc in new_accounts: print(f" - {acc['email']}") # Note: We can't import tokens from IDE as they're in a different format # This is more of a "discovery" feature print("\n[INFO] To use these accounts in CLI, you need to add them via the Manager GUI first.") print("The CLI will then be able to read their tokens from the encrypted database.") def compare_accounts(): """Show differences between CLI and IDE accounts.""" cli_accounts = get_accounts() ide_accounts = get_ide_accounts() cli_emails = {acc['email'] for acc in cli_accounts} ide_emails = {acc['email'] for acc in ide_accounts} only_in_cli = cli_emails - ide_emails only_in_ide = ide_emails - cli_emails in_both = cli_emails & ide_emails return { 'cli_only': only_in_cli, 'ide_only': only_in_ide, 'both': in_both } def is_token_expired(token_data: dict) -> bool: """Check if access token has expired based on expiry_timestamp.""" if not token_data: return True expiry = token_data.get('expiry_timestamp') if not expiry: return True # expiry_timestamp is in milliseconds now_ms = int(time.time() * 1000) return now_ms >= expiry async def validate_and_refresh_account(account: dict) -> dict: """Check token validity and refresh if needed. Returns status dict.""" email = account['email'] token_data = account.get('token') result = { 'email': email, 'valid': False, 'refreshed': False, 'error': None } if not token_data: result['error'] = 'No token data' return result if not is_token_expired(token_data): result['valid'] = True return result # Token expired, try to refresh try: refresh_token = token_data.get('refresh_token') if not refresh_token: result['error'] = 'No refresh token' return result print(f"[{email}] Token expired, refreshing...") new_tokens = await refresh_access_token(refresh_token) # Update in database db_path = find_db_path() master_key = get_master_key() if not master_key: result['error'] = 'Cannot access master key' return result # Merge new tokens with existing data updated_token = token_data.copy() updated_token['access_token'] = new_tokens['access_token'] updated_token['expiry_timestamp'] = int(time.time() * 1000) + (new_tokens.get('expires_in', 3600) * 1000) encrypted_token = encrypt_db_value(json.dumps(updated_token), master_key) conn = sqlite3.connect(db_path) cursor = conn.cursor() cursor.execute("UPDATE accounts SET token_json = ? WHERE email = ?", (encrypted_token, email)) conn.commit() conn.close() result['valid'] = True result['refreshed'] = True return result except Exception as e: result['error'] = str(e) return result async def validate_all_accounts(): """Validate and refresh all accounts. Returns summary.""" accounts = get_accounts() results = [] for acc in accounts: result = await validate_and_refresh_account(acc) results.append(result) return results def remove_account(email_pattern: str) -> bool: """Remove an account from the database.""" db_path = find_db_path() if not db_path: print("Database not found.") return False accounts = get_accounts() target = None for acc in accounts: if email_pattern.lower() in acc['email'].lower(): target = acc break if not target: print(f"Account matching '{email_pattern}' not found.") return False try: conn = sqlite3.connect(db_path) cursor = conn.cursor() cursor.execute("DELETE FROM accounts WHERE email = ?", (target['email'],)) conn.commit() conn.close() return True except Exception as e: print(f"Failed to remove account: {e}") return False def get_alias_path(): """Get path to alias storage file.""" return os.path.join(USER_DATA_DIR, 'aliases.json') def get_aliases() -> dict: """Load account aliases from file.""" alias_path = get_alias_path() if not os.path.exists(alias_path): return {} try: with open(alias_path, 'r') as f: return json.load(f) except: return {} def set_alias(alias: str, email: str) -> bool: """Set an alias for an account.""" aliases = get_aliases() aliases[alias] = email alias_path = get_alias_path() os.makedirs(os.path.dirname(alias_path), exist_ok=True) try: with open(alias_path, 'w') as f: json.dump(aliases, f, indent=2) return True except Exception as e: print(f"Failed to save alias: {e}") return False def remove_alias(alias: str) -> bool: """Remove an alias.""" aliases = get_aliases() if alias not in aliases: return False del aliases[alias] alias_path = get_alias_path() try: with open(alias_path, 'w') as f: json.dump(aliases, f, indent=2) return True except Exception as e: print(f"Failed to remove alias: {e}") return False def resolve_email_or_alias(pattern: str) -> str: """Resolve alias to email or return pattern as-is.""" aliases = get_aliases() return aliases.get(pattern, pattern) def export_accounts(output_path: str) -> bool: """Export all accounts to an encrypted JSON file.""" accounts = get_accounts() # Remove sensitive runtime data but keep encrypted tokens db_path = find_db_path() if not db_path: return False try: conn = sqlite3.connect(db_path) conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute("SELECT * FROM accounts") rows = cursor.fetchall() conn.close() export_data = [dict(row) for row in rows] with open(output_path, 'w') as f: json.dump(export_data, f, indent=2) return True except Exception as e: print(f"Export failed: {e}") return False def import_accounts(input_path: str) -> bool: """Import accounts from a backup file.""" if not os.path.exists(input_path): print(f"File not found: {input_path}") return False db_path = find_db_path() if not db_path: print("Database not found.") return False try: with open(input_path, 'r') as f: import_data = json.load(f) # Validate structure if not isinstance(import_data, list): print("Invalid backup format: expected list of accounts") return False conn = sqlite3.connect(db_path) cursor = conn.cursor() # Start transaction cursor.execute("BEGIN") imported = 0 try: for acc in import_data: # Validate required fields if not isinstance(acc, dict): print(f"Skipping invalid entry (not a dict)") continue if 'email' not in acc: print(f"Skipping entry without email") continue # Sanitize email email = str(acc['email']).strip() if not email or '@' not in email: print(f"Skipping invalid email: {email}") continue # Check if account already exists cursor.execute("SELECT email FROM accounts WHERE email = ?", (email,)) if cursor.fetchone(): print(f"Skipping {email} (already exists)") continue # Insert account with validated data cursor.execute( "INSERT INTO accounts (email, token_json, quota_json, name, avatar_url, last_used, is_active) VALUES (?, ?, ?, ?, ?, ?, ?)", ( email, acc.get('token_json', ''), acc.get('quota_json', ''), acc.get('name', ''), acc.get('avatar_url', ''), int(acc.get('last_used', 0)), 0 # Never set imported accounts as active ) ) imported += 1 # Commit transaction conn.commit() print(f"Imported {imported} account(s).") return True except Exception as e: # Rollback on error conn.rollback() print(f"Import failed during transaction, rolled back: {e}") return False finally: conn.close() except json.JSONDecodeError as e: print(f"Invalid JSON in backup file: {e}") return False except Exception as e: print(f"Import failed: {e}") return False def auto_select_best_account(min_quota: int = 50, prefer_model: str = None) -> dict: """Automatically select the best account based on quota.""" accounts = get_accounts() best = None best_score = -1 for acc in accounts: quota = acc.get('quota', {}).get('models', {}) if not quota: continue # Calculate score if prefer_model: # Filter for specific model family relevant = [m['percentage'] for n, m in quota.items() if prefer_model.lower() in n.lower()] if not relevant: continue score = min(relevant) else: # Overall best: minimum of all quotas all_quotas = [m['percentage'] for m in quota.values()] score = min(all_quotas) if all_quotas else 0 if score >= min_quota and score > best_score: best = acc best_score = score return best def run_diagnostics() -> dict: """Run system diagnostics and return results.""" results = { 'python': {'status': 'ok', 'version': sys.version}, 'database': {'status': 'error', 'path': None}, 'master_key': {'status': 'error'}, 'ide_db': {'status': 'error', 'path': None}, 'exe': {'status': 'error', 'path': None}, 'accounts': {'status': 'error', 'count': 0}, } # Check database db_path = find_db_path() if db_path: results['database'] = {'status': 'ok', 'path': db_path} # Check master key master_key = get_master_key() if master_key: results['master_key'] = {'status': 'ok'} # Check IDE database ide_db = get_antigravity_db_path() if ide_db: results['ide_db'] = {'status': 'ok', 'path': ide_db} # Check executable exe = ANTIGRAVITY_EXE_PATH if exe and os.path.exists(exe): results['exe'] = {'status': 'ok', 'path': exe} # Check accounts accounts = get_accounts() results['accounts'] = {'status': 'ok' if accounts else 'warning', 'count': len(accounts)} return results