diff --git a/CHANGELOG.md b/CHANGELOG.md
index fb2246d..58be43c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Change Log
+## v1.7.08
+1. Add: Google OAuth Authentication | 增加 Google OAuth 認證
+2. Add: GitHub OAuth Authentication | 增加 GitHub OAuth 認證
+3. Add: Lifetime Access for OAuth Users | 增加 OAuth 用戶終身訪問權限
+4. Add: OAuth Authentication Integration | 增加 OAuth 認證集成
+5. Update: Menu System with OAuth Options | 更新菜單系統,添加 OAuth 選項
+6. Add: Multi-language Support for OAuth | 增加 OAuth 多語言支持
+
## v1.7.07
1. Add: Vietnamese Language | 增加越南語言
2. Add: Admin Privilege Management for Windows Executable | 增加 Windows 可執行文件管理員權限
diff --git a/README.md b/README.md
index 4a94ca1..7ffa491 100644
--- a/README.md
+++ b/README.md
@@ -36,6 +36,10 @@ Cursor's configuration.
## ✨ Features | 功能特點
+* 🌟 Google OAuth Authentication with Lifetime Access
使用 Google OAuth 認證(終身訪問)
+
+* ⭐ GitHub OAuth Authentication with Lifetime Access
使用 GitHub OAuth 認證(終身訪問)
+
* Automatically register Cursor membership
自動註冊 Cursor 會員
* Support Windows and macOS systems
支持 Windows 和 macOS 系統
@@ -44,6 +48,8 @@ Cursor's configuration.
* Reset Cursor's configuration
重置 Cursor 的配置
+* Multi-language support (English, 简体中文, 繁體中文, Vietnamese)
多語言支持(英文、简体中文、繁體中文、越南語)
+
## 💻 System Support | 系統支持
| Windows | x64 | ✅ | macOS | Intel | ✅ |
diff --git a/cursor_register_github.py b/cursor_register_github.py
new file mode 100644
index 0000000..b4ea935
--- /dev/null
+++ b/cursor_register_github.py
@@ -0,0 +1,5 @@
+from oauth_auth import main as oauth_main
+
+def main(translator=None):
+ """Handle GitHub OAuth registration"""
+ oauth_main('github', translator)
\ No newline at end of file
diff --git a/cursor_register_google.py b/cursor_register_google.py
new file mode 100644
index 0000000..1da32f2
--- /dev/null
+++ b/cursor_register_google.py
@@ -0,0 +1,5 @@
+from oauth_auth import main as oauth_main
+
+def main(translator=None):
+ """Handle Google OAuth registration"""
+ oauth_main('google', translator)
\ No newline at end of file
diff --git a/locales/en.json b/locales/en.json
index 0b0e3ab..50911b8 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -4,6 +4,8 @@
"exit": "Exit Program",
"reset": "Reset Machine ID",
"register": "Register New Cursor Account",
+ "register_google": "Register with Google Account",
+ "register_github": "Register with GitHub Account",
"register_manual": "Register Cursor with Custom Email",
"quit": "Close Cursor Application",
"select_language": "Change Language",
diff --git a/locales/zh_cn.json b/locales/zh_cn.json
index f7537a5..3fc5bca 100644
--- a/locales/zh_cn.json
+++ b/locales/zh_cn.json
@@ -4,6 +4,8 @@
"exit": "退出程序",
"reset": "重置机器标识",
"register": "注册新 Cursor 账号",
+ "register_google": "使用 Google 账号注册",
+ "register_github": "使用 GitHub 账号注册",
"register_manual": "使用自定义邮箱注册",
"quit": "关闭 Cursor 应用",
"select_language": "更改语言",
diff --git a/main.py b/main.py
index a7bffe9..7d96c1b 100644
--- a/main.py
+++ b/main.py
@@ -219,10 +219,14 @@ def print_menu():
print(f"{Fore.GREEN}0{Style.RESET_ALL}. {EMOJI['ERROR']} {translator.get('menu.exit')}")
print(f"{Fore.GREEN}1{Style.RESET_ALL}. {EMOJI['RESET']} {translator.get('menu.reset')}")
print(f"{Fore.GREEN}2{Style.RESET_ALL}. {EMOJI['SUCCESS']} {translator.get('menu.register')}")
- print(f"{Fore.GREEN}3{Style.RESET_ALL}. {EMOJI['SUCCESS']} {translator.get('menu.register_manual')}")
- print(f"{Fore.GREEN}4{Style.RESET_ALL}. {EMOJI['ERROR']} {translator.get('menu.quit')}")
- print(f"{Fore.GREEN}5{Style.RESET_ALL}. {EMOJI['LANG']} {translator.get('menu.select_language')}")
- print(f"{Fore.GREEN}6{Style.RESET_ALL}. {EMOJI['UPDATE']} {translator.get('menu.disable_auto_update')}")
+ print(f"{Fore.GREEN}3{Style.RESET_ALL}. 🌟 {translator.get('menu.register_google')}")
+ print(f"{Fore.YELLOW} ┗━━ 🔥 LIFETIME ACCESS ENABLED 🔥{Style.RESET_ALL}")
+ print(f"{Fore.GREEN}4{Style.RESET_ALL}. ⭐ {translator.get('menu.register_github')}")
+ print(f"{Fore.YELLOW} ┗━━ 🚀 LIFETIME ACCESS ENABLED 🚀{Style.RESET_ALL}")
+ print(f"{Fore.GREEN}5{Style.RESET_ALL}. {EMOJI['SUCCESS']} {translator.get('menu.register_manual')}")
+ print(f"{Fore.GREEN}6{Style.RESET_ALL}. {EMOJI['ERROR']} {translator.get('menu.quit')}")
+ print(f"{Fore.GREEN}7{Style.RESET_ALL}. {EMOJI['LANG']} {translator.get('menu.select_language')}")
+ print(f"{Fore.GREEN}8{Style.RESET_ALL}. {EMOJI['UPDATE']} {translator.get('menu.disable_auto_update')}")
print(f"{Fore.YELLOW}{'─' * 40}{Style.RESET_ALL}")
def select_language():
@@ -358,7 +362,7 @@ def main():
while True:
try:
- choice = input(f"\n{EMOJI['ARROW']} {Fore.CYAN}{translator.get('menu.input_choice', choices='0-6')}: {Style.RESET_ALL}")
+ choice = input(f"\n{EMOJI['ARROW']} {Fore.CYAN}{translator.get('menu.input_choice', choices='0-8')}: {Style.RESET_ALL}")
if choice == "0":
print(f"\n{Fore.YELLOW}{EMOJI['INFO']} {translator.get('menu.exit')}...{Style.RESET_ALL}")
@@ -373,18 +377,26 @@ def main():
cursor_register.main(translator)
print_menu()
elif choice == "3":
+ import cursor_register_google
+ cursor_register_google.main(translator)
+ print_menu()
+ elif choice == "4":
+ import cursor_register_github
+ cursor_register_github.main(translator)
+ print_menu()
+ elif choice == "5":
import cursor_register_manual
cursor_register_manual.main(translator)
print_menu()
- elif choice == "4":
+ elif choice == "6":
import quit_cursor
quit_cursor.quit_cursor(translator)
print_menu()
- elif choice == "5":
+ elif choice == "7":
if select_language():
print_menu()
continue
- elif choice == "6":
+ elif choice == "8":
import disable_auto_update
disable_auto_update.run(translator)
print_menu()
diff --git a/oauth_auth.py b/oauth_auth.py
new file mode 100644
index 0000000..b917a33
--- /dev/null
+++ b/oauth_auth.py
@@ -0,0 +1,826 @@
+import os
+from colorama import Fore, Style, init
+import time
+import random
+import webbrowser
+import sys
+import json
+from DrissionPage import ChromiumPage, ChromiumOptions
+from cursor_auth import CursorAuth
+from utils import get_random_wait_time, get_default_chrome_path
+from config import get_config
+import platform
+
+# Initialize colorama
+init()
+
+# Define emoji constants
+EMOJI = {
+ 'START': '🚀',
+ 'OAUTH': '🔑',
+ 'SUCCESS': '✅',
+ 'ERROR': '❌',
+ 'WAIT': '⏳',
+ 'INFO': 'ℹ️'
+}
+
+class OAuthHandler:
+ def __init__(self, translator=None):
+ self.translator = translator
+ self.config = get_config(translator)
+ os.environ['BROWSER_HEADLESS'] = 'False'
+ self.browser = None
+
+ def _get_active_profile(self, user_data_dir):
+ """Find the existing default/active Chrome profile"""
+ try:
+ # List all profile directories
+ profiles = []
+ for item in os.listdir(user_data_dir):
+ if item == 'Default' or (item.startswith('Profile ') and os.path.isdir(os.path.join(user_data_dir, item))):
+ profiles.append(item)
+
+ if not profiles:
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} No Chrome profiles found, using Default{Style.RESET_ALL}")
+ return 'Default'
+
+ # First check if Default profile exists
+ if 'Default' in profiles:
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Found Default Chrome profile{Style.RESET_ALL}")
+ return 'Default'
+
+ # If no Default profile, check Local State for last used profile
+ local_state_path = os.path.join(user_data_dir, 'Local State')
+ if os.path.exists(local_state_path):
+ with open(local_state_path, 'r', encoding='utf-8') as f:
+ local_state = json.load(f)
+
+ # Get info about last used profile
+ profile_info = local_state.get('profile', {})
+ last_used = profile_info.get('last_used', '')
+ info_cache = profile_info.get('info_cache', {})
+
+ # Try to find an active profile
+ for profile in profiles:
+ profile_path = profile.replace('\\', '/')
+ if profile_path in info_cache:
+ #print(f"{Fore.CYAN}{EMOJI['INFO']} Using existing Chrome profile: {profile}{Style.RESET_ALL}")
+ return profile
+
+ # If no profile found in Local State, use the first available profile
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Using first available Chrome profile: {profiles[0]}{Style.RESET_ALL}")
+ return profiles[0]
+
+ except Exception as e:
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} Error finding Chrome profile, using Default: {str(e)}{Style.RESET_ALL}")
+ return 'Default'
+
+ def setup_browser(self):
+ """Setup browser for OAuth flow using active profile"""
+ try:
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Initializing browser setup...{Style.RESET_ALL}")
+
+ # Platform-specific initialization
+ platform_name = platform.system().lower()
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Detected platform: {platform_name}{Style.RESET_ALL}")
+
+ # Kill existing browser processes
+ self._kill_browser_processes()
+
+ # Get browser paths and user data directory
+ user_data_dir = self._get_user_data_directory()
+ chrome_path = self._get_browser_path()
+
+ if not chrome_path:
+ raise Exception(f"No compatible browser found. Please install Google Chrome or Chromium.\nSupported browsers for {platform_name}:\n" +
+ "- Windows: Google Chrome, Chromium\n" +
+ "- macOS: Google Chrome, Chromium\n" +
+ "- Linux: Google Chrome, Chromium, chromium-browser")
+
+ # Get active profile
+ active_profile = self._get_active_profile(user_data_dir)
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Using browser profile: {active_profile}{Style.RESET_ALL}")
+
+ # Configure browser options
+ co = self._configure_browser_options(chrome_path, user_data_dir, active_profile)
+
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Starting browser at: {chrome_path}{Style.RESET_ALL}")
+ self.browser = ChromiumPage(co)
+
+ # Verify browser launched successfully
+ if not self.browser:
+ raise Exception("Failed to initialize browser instance")
+
+ print(f"{Fore.GREEN}{EMOJI['SUCCESS']} Browser setup completed successfully{Style.RESET_ALL}")
+ return True
+
+ except Exception as e:
+ print(f"{Fore.RED}{EMOJI['ERROR']} Browser setup failed: {str(e)}{Style.RESET_ALL}")
+ if "DevToolsActivePort file doesn't exist" in str(e):
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} Try running with administrator/root privileges{Style.RESET_ALL}")
+ elif "Chrome failed to start" in str(e):
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} Make sure Chrome/Chromium is properly installed{Style.RESET_ALL}")
+ return False
+
+ def _kill_browser_processes(self):
+ """Kill existing browser processes based on platform"""
+ try:
+ if os.name == 'nt': # Windows
+ processes = ['chrome.exe', 'chromium.exe']
+ for proc in processes:
+ os.system(f'taskkill /f /im {proc} >nul 2>&1')
+ else: # Linux/Mac
+ processes = ['chrome', 'chromium', 'chromium-browser']
+ for proc in processes:
+ os.system(f'pkill -f {proc} >/dev/null 2>&1')
+
+ time.sleep(1) # Wait for processes to close
+ except Exception as e:
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} Warning: Could not kill existing browser processes: {e}{Style.RESET_ALL}")
+
+ def _get_user_data_directory(self):
+ """Get the appropriate user data directory based on platform"""
+ try:
+ if os.name == 'nt': # Windows
+ possible_paths = [
+ os.path.expandvars(r'%LOCALAPPDATA%\Google\Chrome\User Data'),
+ os.path.expandvars(r'%LOCALAPPDATA%\Chromium\User Data')
+ ]
+ elif sys.platform == 'darwin': # macOS
+ possible_paths = [
+ os.path.expanduser('~/Library/Application Support/Google/Chrome'),
+ os.path.expanduser('~/Library/Application Support/Chromium')
+ ]
+ else: # Linux
+ possible_paths = [
+ os.path.expanduser('~/.config/google-chrome'),
+ os.path.expanduser('~/.config/chromium'),
+ '/usr/bin/google-chrome',
+ '/usr/bin/chromium-browser'
+ ]
+
+ # Try each possible path
+ for path in possible_paths:
+ if os.path.exists(path):
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Found browser data directory: {path}{Style.RESET_ALL}")
+ return path
+
+ # Create temporary profile if no existing profile found
+ temp_profile = os.path.join(os.path.expanduser('~'), '.cursor_temp_profile')
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} Creating temporary profile at: {temp_profile}{Style.RESET_ALL}")
+ os.makedirs(temp_profile, exist_ok=True)
+ return temp_profile
+
+ except Exception as e:
+ print(f"{Fore.RED}{EMOJI['ERROR']} Error getting user data directory: {e}{Style.RESET_ALL}")
+ raise
+
+ def _get_browser_path(self):
+ """Get the browser executable path based on platform"""
+ try:
+ # Try default path first
+ chrome_path = get_default_chrome_path()
+ if chrome_path and os.path.exists(chrome_path):
+ return chrome_path
+
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} Searching for alternative browser installations...{Style.RESET_ALL}")
+
+ # Platform-specific paths
+ if os.name == 'nt': # Windows
+ alt_paths = [
+ r'C:\Program Files\Google\Chrome\Application\chrome.exe',
+ r'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe',
+ r'C:\Program Files\Chromium\Application\chrome.exe',
+ os.path.expandvars(r'%ProgramFiles%\Google\Chrome\Application\chrome.exe'),
+ os.path.expandvars(r'%ProgramFiles(x86)%\Google\Chrome\Application\chrome.exe')
+ ]
+ elif sys.platform == 'darwin': # macOS
+ alt_paths = [
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
+ '~/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
+ '~/Applications/Chromium.app/Contents/MacOS/Chromium'
+ ]
+ else: # Linux
+ alt_paths = [
+ '/usr/bin/google-chrome',
+ '/usr/bin/chromium-browser',
+ '/usr/bin/chromium',
+ '/snap/bin/chromium',
+ '/usr/local/bin/chrome',
+ '/usr/local/bin/chromium'
+ ]
+
+ # Try each alternative path
+ for path in alt_paths:
+ expanded_path = os.path.expanduser(path)
+ if os.path.exists(expanded_path):
+ print(f"{Fore.GREEN}{EMOJI['SUCCESS']} Found browser at: {expanded_path}{Style.RESET_ALL}")
+ return expanded_path
+
+ return None
+
+ except Exception as e:
+ print(f"{Fore.RED}{EMOJI['ERROR']} Error finding browser path: {e}{Style.RESET_ALL}")
+ return None
+
+ def _configure_browser_options(self, chrome_path, user_data_dir, active_profile):
+ """Configure browser options based on platform"""
+ try:
+ co = ChromiumOptions()
+ co.set_paths(browser_path=chrome_path, user_data_path=user_data_dir)
+ co.set_argument(f'--profile-directory={active_profile}')
+
+ # Basic options
+ co.set_argument('--no-first-run')
+ co.set_argument('--no-default-browser-check')
+ co.set_argument('--disable-gpu')
+
+ # Platform-specific options
+ if sys.platform.startswith('linux'):
+ co.set_argument('--no-sandbox')
+ co.set_argument('--disable-dev-shm-usage')
+ co.set_argument('--disable-setuid-sandbox')
+ elif sys.platform == 'darwin':
+ co.set_argument('--disable-gpu-compositing')
+ elif os.name == 'nt':
+ co.set_argument('--disable-features=TranslateUI')
+ co.set_argument('--disable-features=RendererCodeIntegrity')
+
+ return co
+
+ except Exception as e:
+ print(f"{Fore.RED}{EMOJI['ERROR']} Error configuring browser options: {e}{Style.RESET_ALL}")
+ raise
+
+ def handle_google_auth(self):
+ """Handle Google OAuth authentication"""
+ try:
+ print(f"{Fore.CYAN}{EMOJI['INFO']} {self.translator.get('oauth.google_start')}{Style.RESET_ALL}")
+
+ # Setup browser
+ if not self.setup_browser():
+ print(f"{Fore.RED}{EMOJI['ERROR']} {self.translator.get('oauth.browser_failed')}{Style.RESET_ALL}")
+ return False, None
+
+ # Navigate to auth URL
+ try:
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Navigating to authentication page...{Style.RESET_ALL}")
+ self.browser.get("https://authenticator.cursor.sh/sign-up")
+ time.sleep(get_random_wait_time(self.config, 'page_load_wait'))
+
+ # Look for Google auth button
+ selectors = [
+ "//a[contains(@href,'GoogleOAuth')]",
+ "//a[contains(@class,'auth-method-button') and contains(@href,'GoogleOAuth')]",
+ "(//a[contains(@class,'auth-method-button')])[1]" # First auth button as fallback
+ ]
+
+ auth_btn = None
+ for selector in selectors:
+ try:
+ auth_btn = self.browser.ele(f"xpath:{selector}", timeout=2)
+ if auth_btn and auth_btn.is_displayed():
+ break
+ except:
+ continue
+
+ if not auth_btn:
+ raise Exception("Could not find Google authentication button")
+
+ # Click the button and wait for page load
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Starting Google authentication...{Style.RESET_ALL}")
+ auth_btn.click()
+ time.sleep(get_random_wait_time(self.config, 'page_load_wait'))
+
+ # Check if we're on account selection page
+ if "accounts.google.com" in self.browser.url:
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Please select your Google account to continue...{Style.RESET_ALL}")
+ try:
+ self.browser.run_js("""
+ alert('Please select your Google account to continue with Cursor authentication');
+ """)
+ except:
+ pass # Alert is optional
+
+ # Wait for authentication to complete
+ auth_info = self._wait_for_auth()
+ if not auth_info:
+ print(f"{Fore.RED}{EMOJI['ERROR']} {self.translator.get('oauth.timeout')}{Style.RESET_ALL}")
+ return False, None
+
+ print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {self.translator.get('oauth.success')}{Style.RESET_ALL}")
+ return True, auth_info
+
+ except Exception as e:
+ print(f"{Fore.RED}{EMOJI['ERROR']} Authentication error: {str(e)}{Style.RESET_ALL}")
+ return False, None
+ finally:
+ try:
+ if self.browser:
+ self.browser.quit()
+ except:
+ pass
+
+ except Exception as e:
+ print(f"{Fore.RED}{EMOJI['ERROR']} {self.translator.get('oauth.failed', error=str(e))}{Style.RESET_ALL}")
+ return False, None
+
+ def _wait_for_auth(self):
+ """Wait for authentication to complete and extract auth info"""
+ try:
+ max_wait = 300 # 5 minutes
+ start_time = time.time()
+ check_interval = 2 # Check every 2 seconds
+
+ print(f"{Fore.CYAN}{EMOJI['WAIT']} Waiting for authentication (timeout: 5 minutes)...{Style.RESET_ALL}")
+
+ while time.time() - start_time < max_wait:
+ try:
+ # Check for authentication cookies
+ cookies = self.browser.cookies()
+
+ for cookie in cookies:
+ if cookie.get("name") == "WorkosCursorSessionToken":
+ value = cookie.get("value", "")
+ token = None
+
+ if "::" in value:
+ token = value.split("::")[-1]
+ elif "%3A%3A" in value:
+ token = value.split("%3A%3A")[-1]
+
+ if token:
+ # Get email from settings page
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Authentication successful, getting account info...{Style.RESET_ALL}")
+ self.browser.get("https://www.cursor.com/settings")
+ time.sleep(3)
+
+ email = None
+ try:
+ email_element = self.browser.ele("css:div[class='flex w-full flex-col gap-2'] div:nth-child(2) p:nth-child(2)")
+ if email_element:
+ email = email_element.text
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Found email: {email}{Style.RESET_ALL}")
+ except:
+ email = "user@cursor.sh" # Fallback email
+
+ # Check usage count
+ try:
+ usage_element = self.browser.ele("css:div[class='flex flex-col gap-4 lg:flex-row'] div:nth-child(1) div:nth-child(1) span:nth-child(2)")
+ if usage_element:
+ usage_text = usage_element.text
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Usage count: {usage_text}{Style.RESET_ALL}")
+
+ # Check if account is expired
+ if usage_text.strip() == "150 / 150":
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} Account has reached maximum usage, creating new account...{Style.RESET_ALL}")
+
+ # Delete current account
+ if self._delete_current_account():
+ # Start new authentication
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Starting new authentication process...{Style.RESET_ALL}")
+ return self.handle_google_auth()
+ else:
+ print(f"{Fore.RED}{EMOJI['ERROR']} Failed to delete expired account{Style.RESET_ALL}")
+
+ except Exception as e:
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} Could not check usage count: {str(e)}{Style.RESET_ALL}")
+
+ return {"email": email, "token": token}
+
+ # Also check URL as backup
+ if "cursor.com/settings" in self.browser.url:
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Detected successful login{Style.RESET_ALL}")
+
+ except Exception as e:
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} Waiting for authentication... ({str(e)}){Style.RESET_ALL}")
+
+ time.sleep(check_interval)
+
+ print(f"{Fore.RED}{EMOJI['ERROR']} Authentication timeout{Style.RESET_ALL}")
+ return None
+
+ except Exception as e:
+ print(f"{Fore.RED}{EMOJI['ERROR']} Error while waiting for authentication: {str(e)}{Style.RESET_ALL}")
+ return None
+
+ def handle_github_auth(self):
+ """Handle GitHub OAuth authentication"""
+ try:
+ print(f"{Fore.CYAN}{EMOJI['INFO']} {self.translator.get('oauth.github_start')}{Style.RESET_ALL}")
+
+ # Setup browser
+ if not self.setup_browser():
+ print(f"{Fore.RED}{EMOJI['ERROR']} {self.translator.get('oauth.browser_failed')}{Style.RESET_ALL}")
+ return False, None
+
+ # Navigate to auth URL
+ try:
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Navigating to authentication page...{Style.RESET_ALL}")
+ self.browser.get("https://authenticator.cursor.sh/sign-up")
+ time.sleep(get_random_wait_time(self.config, 'page_load_wait'))
+
+ # Look for GitHub auth button
+ selectors = [
+ "//a[contains(@href,'GitHubOAuth')]",
+ "//a[contains(@class,'auth-method-button') and contains(@href,'GitHubOAuth')]",
+ "(//a[contains(@class,'auth-method-button')])[2]" # Second auth button as fallback
+ ]
+
+ auth_btn = None
+ for selector in selectors:
+ try:
+ auth_btn = self.browser.ele(f"xpath:{selector}", timeout=2)
+ if auth_btn and auth_btn.is_displayed():
+ break
+ except:
+ continue
+
+ if not auth_btn:
+ raise Exception("Could not find GitHub authentication button")
+
+ # Click the button and wait for page load
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Starting GitHub authentication...{Style.RESET_ALL}")
+ auth_btn.click()
+ time.sleep(get_random_wait_time(self.config, 'page_load_wait'))
+
+ # Wait for authentication to complete
+ auth_info = self._wait_for_auth()
+ if not auth_info:
+ print(f"{Fore.RED}{EMOJI['ERROR']} {self.translator.get('oauth.timeout')}{Style.RESET_ALL}")
+ return False, None
+
+ print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {self.translator.get('oauth.success')}{Style.RESET_ALL}")
+ return True, auth_info
+
+ except Exception as e:
+ print(f"{Fore.RED}{EMOJI['ERROR']} Authentication error: {str(e)}{Style.RESET_ALL}")
+ return False, None
+ finally:
+ try:
+ if self.browser:
+ self.browser.quit()
+ except:
+ pass
+
+ except Exception as e:
+ print(f"{Fore.RED}{EMOJI['ERROR']} {self.translator.get('oauth.failed', error=str(e))}{Style.RESET_ALL}")
+ return False, None
+
+ def _handle_oauth(self, auth_type):
+ """Handle OAuth authentication for both Google and GitHub
+
+ Args:
+ auth_type (str): Type of authentication ('google' or 'github')
+ """
+ try:
+ if not self.setup_browser():
+ return False, None
+
+ # Navigate to auth URL
+ self.browser.get("https://authenticator.cursor.sh/sign-up")
+ time.sleep(get_random_wait_time(self.config, 'page_load_wait'))
+
+ # Set selectors based on auth type
+ if auth_type == "google":
+ selectors = [
+ "//a[@class='rt-reset rt-BaseButton rt-r-size-3 rt-variant-surface rt-high-contrast rt-Button auth-method-button_AuthMethodButton__irESX'][contains(@href,'GoogleOAuth')]",
+ "(//a[@class='rt-reset rt-BaseButton rt-r-size-3 rt-variant-surface rt-high-contrast rt-Button auth-method-button_AuthMethodButton__irESX'])[1]"
+ ]
+ else: # github
+ selectors = [
+ "(//a[@class='rt-reset rt-BaseButton rt-r-size-3 rt-variant-surface rt-high-contrast rt-Button auth-method-button_AuthMethodButton__irESX'])[2]"
+ ]
+
+ # Wait for the button to be available
+ auth_btn = None
+ max_button_wait = 30 # 30 seconds
+ button_start_time = time.time()
+
+ while time.time() - button_start_time < max_button_wait:
+ for selector in selectors:
+ try:
+ auth_btn = self.browser.ele(f"xpath:{selector}", timeout=1)
+ if auth_btn and auth_btn.is_displayed():
+ break
+ except:
+ continue
+ if auth_btn:
+ break
+ time.sleep(1)
+
+ if auth_btn:
+ # Click the button and wait for page load
+ auth_btn.click()
+ time.sleep(get_random_wait_time(self.config, 'page_load_wait'))
+
+ # Check if we're on account selection page
+ if auth_type == "google" and "accounts.google.com" in self.browser.url:
+ alert_js = """
+ alert('Please select your Google account manually to continue with Cursor authentication');
+ """
+ try:
+ self.browser.run_js(alert_js)
+ except Exception as e:
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} Alert display failed: {str(e)}{Style.RESET_ALL}")
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Please select your Google account manually to continue with Cursor authentication...{Style.RESET_ALL}")
+
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Waiting for authentication to complete...{Style.RESET_ALL}")
+
+ # Wait for authentication to complete
+ max_wait = 300 # 5 minutes
+ start_time = time.time()
+ last_url = self.browser.url
+
+ print(f"{Fore.CYAN}{EMOJI['WAIT']} Checking authentication status...{Style.RESET_ALL}")
+
+ while time.time() - start_time < max_wait:
+ try:
+ # Check for authentication cookies
+ cookies = self.browser.cookies()
+
+ for cookie in cookies:
+ if cookie.get("name") == "WorkosCursorSessionToken":
+ value = cookie.get("value", "")
+ if "::" in value:
+ token = value.split("::")[-1]
+ elif "%3A%3A" in value:
+ token = value.split("%3A%3A")[-1]
+
+ if token:
+ print(f"{Fore.GREEN}{EMOJI['SUCCESS']} Authentication successful!{Style.RESET_ALL}")
+ # Navigate to settings page
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Navigating to settings page...{Style.RESET_ALL}")
+ self.browser.get("https://www.cursor.com/settings")
+ time.sleep(3) # Wait for settings page to load
+
+ # Get email from settings page
+ try:
+ email_element = self.browser.ele("css:div[class='flex w-full flex-col gap-2'] div:nth-child(2) p:nth-child(2)")
+ if email_element:
+ actual_email = email_element.text
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Found email: {actual_email}{Style.RESET_ALL}")
+ except Exception as e:
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} Could not find email: {str(e)}{Style.RESET_ALL}")
+ actual_email = "user@cursor.sh"
+
+ # Check usage count
+ try:
+ usage_element = self.browser.ele("css:div[class='flex flex-col gap-4 lg:flex-row'] div:nth-child(1) div:nth-child(1) span:nth-child(2)")
+ if usage_element:
+ usage_text = usage_element.text
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Usage count: {usage_text}{Style.RESET_ALL}")
+
+ # Check if account is expired
+ if usage_text.strip() == "150 / 150": # Changed back to actual condition
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} Account has reached maximum usage, deleting...{Style.RESET_ALL}")
+
+ delete_js = """
+ function deleteAccount() {
+ return new Promise((resolve, reject) => {
+ fetch('https://www.cursor.com/api/dashboard/delete-account', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ credentials: 'include'
+ })
+ .then(response => {
+ if (response.status === 200) {
+ resolve('Account deleted successfully');
+ } else {
+ reject('Failed to delete account: ' + response.status);
+ }
+ })
+ .catch(error => {
+ reject('Error: ' + error);
+ });
+ });
+ }
+ return deleteAccount();
+ """
+
+ try:
+ result = self.browser.run_js(delete_js)
+ print(f"{Fore.GREEN}{EMOJI['SUCCESS']} Delete account result: {result}{Style.RESET_ALL}")
+
+ # Navigate back to auth page and repeat authentication
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Starting re-authentication process...{Style.RESET_ALL}")
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Redirecting to authenticator.cursor.sh...{Style.RESET_ALL}")
+
+ # Explicitly navigate to the authentication page
+ #self.browser.get("https://authenticator.cursor.sh/sign-up")
+ # time.sleep(get_random_wait_time(self.config, 'page_load_wait'))
+
+ # Call handle_google_auth again to repeat the entire process
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Starting new Google authentication...{Style.RESET_ALL}")
+ return self.handle_google_auth()
+
+ except Exception as e:
+ print(f"{Fore.RED}{EMOJI['ERROR']} Failed to delete account or re-authenticate: {str(e)}{Style.RESET_ALL}")
+ else:
+ print(f"{Fore.GREEN}{EMOJI['SUCCESS']} Account is still valid (Usage: {usage_text}){Style.RESET_ALL}")
+ except Exception as e:
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} Could not find usage count: {str(e)}{Style.RESET_ALL}")
+
+ # Remove the browser stay open prompt and input wait
+ return True, {"email": actual_email, "token": token}
+
+ # Also check URL as backup
+ current_url = self.browser.url
+ if "cursor.com/settings" in current_url:
+ print(f"{Fore.GREEN}{EMOJI['SUCCESS']} Already on settings page!{Style.RESET_ALL}")
+ time.sleep(1)
+ cookies = self.browser.cookies()
+ for cookie in cookies:
+ if cookie.get("name") == "WorkosCursorSessionToken":
+ value = cookie.get("value", "")
+ if "::" in value:
+ token = value.split("::")[-1]
+ elif "%3A%3A" in value:
+ token = value.split("%3A%3A")[-1]
+ if token:
+ # Get email and check usage here too
+ try:
+ email_element = self.browser.ele("css:div[class='flex w-full flex-col gap-2'] div:nth-child(2) p:nth-child(2)")
+ if email_element:
+ actual_email = email_element.text
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Found email: {actual_email}{Style.RESET_ALL}")
+ except Exception as e:
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} Could not find email: {str(e)}{Style.RESET_ALL}")
+ actual_email = "user@cursor.sh"
+
+ # Check usage count
+ try:
+ usage_element = self.browser.ele("css:div[class='flex flex-col gap-4 lg:flex-row'] div:nth-child(1) div:nth-child(1) span:nth-child(2)")
+ if usage_element:
+ usage_text = usage_element.text
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Usage count: {usage_text}{Style.RESET_ALL}")
+
+ # Check if account is expired
+ if usage_text.strip() == "150 / 150": # Changed back to actual condition
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} Account has reached maximum usage, deleting...{Style.RESET_ALL}")
+
+ delete_js = """
+ function deleteAccount() {
+ return new Promise((resolve, reject) => {
+ fetch('https://www.cursor.com/api/dashboard/delete-account', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ credentials: 'include'
+ })
+ .then(response => {
+ if (response.status === 200) {
+ resolve('Account deleted successfully');
+ } else {
+ reject('Failed to delete account: ' + response.status);
+ }
+ })
+ .catch(error => {
+ reject('Error: ' + error);
+ });
+ });
+ }
+ return deleteAccount();
+ """
+
+ try:
+ result = self.browser.run_js(delete_js)
+ print(f"{Fore.GREEN}{EMOJI['SUCCESS']} Delete account result: {result}{Style.RESET_ALL}")
+
+ # Navigate back to auth page and repeat authentication
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Starting re-authentication process...{Style.RESET_ALL}")
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Redirecting to authenticator.cursor.sh...{Style.RESET_ALL}")
+
+ # Explicitly navigate to the authentication page
+ self.browser.get("https://authenticator.cursor.sh/sign-up")
+ time.sleep(get_random_wait_time(self.config, 'page_load_wait'))
+
+ # Call handle_google_auth again to repeat the entire process
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Starting new Google authentication...{Style.RESET_ALL}")
+ return self.handle_google_auth()
+
+ except Exception as e:
+ print(f"{Fore.RED}{EMOJI['ERROR']} Failed to delete account or re-authenticate: {str(e)}{Style.RESET_ALL}")
+ else:
+ print(f"{Fore.GREEN}{EMOJI['SUCCESS']} Account is still valid (Usage: {usage_text}){Style.RESET_ALL}")
+ except Exception as e:
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} Could not find usage count: {str(e)}{Style.RESET_ALL}")
+
+ # Remove the browser stay open prompt and input wait
+ return True, {"email": actual_email, "token": token}
+ elif current_url != last_url:
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Page changed, checking auth...{Style.RESET_ALL}")
+ last_url = current_url
+ time.sleep(get_random_wait_time(self.config, 'page_load_wait'))
+ except Exception as e:
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} Status check error: {str(e)}{Style.RESET_ALL}")
+ time.sleep(1)
+ continue
+ time.sleep(1)
+
+ print(f"{Fore.RED}{EMOJI['ERROR']} Authentication timeout{Style.RESET_ALL}")
+ return False, None
+
+ print(f"{Fore.RED}{EMOJI['ERROR']} Authentication button not found{Style.RESET_ALL}")
+ return False, None
+
+ except Exception as e:
+ print(f"{Fore.RED}{EMOJI['ERROR']} Authentication failed: {str(e)}{Style.RESET_ALL}")
+ return False, None
+ finally:
+ if self.browser:
+ self.browser.quit()
+
+ def _extract_auth_info(self):
+ """Extract authentication information after successful OAuth"""
+ try:
+ # Get cookies with retry
+ max_retries = 3
+ for attempt in range(max_retries):
+ try:
+ cookies = self.browser.cookies()
+ if cookies:
+ break
+ time.sleep(1)
+ except:
+ if attempt == max_retries - 1:
+ raise
+ time.sleep(1)
+
+ # Debug cookie information
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Found {len(cookies)} cookies{Style.RESET_ALL}")
+
+ email = None
+ token = None
+
+ for cookie in cookies:
+ name = cookie.get("name", "")
+ if name == "WorkosCursorSessionToken":
+ try:
+ value = cookie.get("value", "")
+ if "::" in value:
+ token = value.split("::")[-1]
+ elif "%3A%3A" in value:
+ token = value.split("%3A%3A")[-1]
+ except Exception as e:
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} Token extraction error: {str(e)}{Style.RESET_ALL}")
+ elif name == "cursor_email":
+ email = cookie.get("value")
+
+ if email and token:
+ print(f"{Fore.GREEN}{EMOJI['SUCCESS']} Authentication successful - Email: {email}{Style.RESET_ALL}")
+ return True, {"email": email, "token": token}
+ else:
+ missing = []
+ if not email:
+ missing.append("email")
+ if not token:
+ missing.append("token")
+ print(f"{Fore.RED}{EMOJI['ERROR']} Missing authentication data: {', '.join(missing)}{Style.RESET_ALL}")
+ return False, None
+
+ except Exception as e:
+ print(f"{Fore.RED}{EMOJI['ERROR']} Failed to extract auth info: {str(e)}{Style.RESET_ALL}")
+ return False, None
+
+def main(auth_type, translator=None):
+ """Main function to handle OAuth authentication
+
+ Args:
+ auth_type (str): Type of authentication ('google' or 'github')
+ translator: Translator instance for internationalization
+ """
+ handler = OAuthHandler(translator)
+
+ if auth_type.lower() == 'google':
+ print(f"{Fore.CYAN}{EMOJI['INFO']} {translator.get('oauth.google_start')}{Style.RESET_ALL}")
+ success, auth_info = handler.handle_google_auth()
+ elif auth_type.lower() == 'github':
+ print(f"{Fore.CYAN}{EMOJI['INFO']} {translator.get('oauth.github_start')}{Style.RESET_ALL}")
+ success, auth_info = handler.handle_github_auth()
+ else:
+ print(f"{Fore.RED}{EMOJI['ERROR']} Invalid authentication type{Style.RESET_ALL}")
+ return False
+
+ if success and auth_info:
+ # Update Cursor authentication
+ auth_manager = CursorAuth(translator)
+ if auth_manager.update_auth(
+ email=auth_info["email"],
+ access_token=auth_info["token"],
+ refresh_token=auth_info["token"]
+ ):
+ print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {translator.get('oauth.auth_update_success')}{Style.RESET_ALL}")
+ # Close the browser after successful authentication
+ if handler.browser:
+ handler.browser.quit()
+ print(f"{Fore.CYAN}{EMOJI['INFO']} Browser closed{Style.RESET_ALL}")
+ return True
+ else:
+ print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('oauth.auth_update_failed')}{Style.RESET_ALL}")
+
+ return False
\ No newline at end of file
diff --git a/utils.py b/utils.py
index 7a491e4..ca9b2d1 100644
--- a/utils.py
+++ b/utils.py
@@ -1,6 +1,7 @@
import os
import sys
import platform
+import random
def get_user_documents_path():
"""Get user documents path"""
@@ -29,4 +30,40 @@ def get_linux_cursor_path():
]
# return the first path that exists
- return next((path for path in possible_paths if os.path.exists(path)), possible_paths[0])
\ No newline at end of file
+ return next((path for path in possible_paths if os.path.exists(path)), possible_paths[0])
+
+def get_random_wait_time(config, timing_key):
+ """Get random wait time based on configuration timing settings
+
+ Args:
+ config (dict): Configuration dictionary containing timing settings
+ timing_key (str): Key to look up in the timing settings
+
+ Returns:
+ float: Random wait time in seconds
+ """
+ try:
+ # Get timing value from config
+ timing = config.get('Timing', {}).get(timing_key)
+ if not timing:
+ # Default to 0.5-1.5 seconds if timing not found
+ return random.uniform(0.5, 1.5)
+
+ # Check if timing is a range (e.g., "0.5-1.5" or "0.5,1.5")
+ if isinstance(timing, str):
+ if '-' in timing:
+ min_time, max_time = map(float, timing.split('-'))
+ elif ',' in timing:
+ min_time, max_time = map(float, timing.split(','))
+ else:
+ # Single value, use it as both min and max
+ min_time = max_time = float(timing)
+ else:
+ # If timing is a number, use it as both min and max
+ min_time = max_time = float(timing)
+
+ return random.uniform(min_time, max_time)
+
+ except (ValueError, TypeError, AttributeError):
+ # Return default value if any error occurs
+ return random.uniform(0.5, 1.5)
\ No newline at end of file