diff --git a/apps/server/src/providers/provider-factory.ts b/apps/server/src/providers/provider-factory.ts index 0dde03ad..57f590e3 100644 --- a/apps/server/src/providers/provider-factory.ts +++ b/apps/server/src/providers/provider-factory.ts @@ -8,6 +8,25 @@ import { BaseProvider } from './base-provider.js'; import type { InstallationStatus, ModelDefinition } from './types.js'; import { isCursorModel, isCodexModel, type ModelProvider } from '@automaker/types'; +import * as fs from 'fs'; +import * as path from 'path'; + +const DISCONNECTED_MARKERS: Record = { + claude: '.claude-disconnected', + codex: '.codex-disconnected', + cursor: '.cursor-disconnected', +}; + +/** + * Check if a provider CLI is disconnected from the app + */ +export function isProviderDisconnected(providerName: string): boolean { + const markerFile = DISCONNECTED_MARKERS[providerName.toLowerCase()]; + if (!markerFile) return false; + + const markerPath = path.join(process.cwd(), '.automaker', markerFile); + return fs.existsSync(markerPath); +} /** * Provider registration entry @@ -75,10 +94,26 @@ export class ProviderFactory { * Get the appropriate provider for a given model ID * * @param modelId Model identifier (e.g., "claude-opus-4-5-20251101", "cursor-gpt-4o", "cursor-auto") + * @param options Optional settings + * @param options.throwOnDisconnected Throw error if provider is disconnected (default: true) * @returns Provider instance for the model + * @throws Error if provider is disconnected and throwOnDisconnected is true */ - static getProviderForModel(modelId: string): BaseProvider { - const providerName = this.getProviderNameForModel(modelId); + static getProviderForModel( + modelId: string, + options: { throwOnDisconnected?: boolean } = {} + ): BaseProvider { + const { throwOnDisconnected = true } = options; + const providerName = this.getProviderForModelName(modelId); + + // Check if provider is disconnected + if (throwOnDisconnected && isProviderDisconnected(providerName)) { + throw new Error( + `${providerName.charAt(0).toUpperCase() + providerName.slice(1)} CLI is disconnected from the app. ` + + `Please go to Settings > Providers and click "Sign In" to reconnect.` + ); + } + const provider = this.getProviderByName(providerName); if (!provider) { @@ -93,6 +128,35 @@ export class ProviderFactory { return provider; } + /** + * Get the provider name for a given model ID (without creating provider instance) + */ + static getProviderForModelName(modelId: string): string { + const lowerModel = modelId.toLowerCase(); + + // Get all registered providers sorted by priority (descending) + const registrations = Array.from(providerRegistry.entries()).sort( + ([, a], [, b]) => (b.priority ?? 0) - (a.priority ?? 0) + ); + + // Check each provider's canHandleModel function + for (const [name, reg] of registrations) { + if (reg.canHandleModel?.(lowerModel)) { + return name; + } + } + + // Fallback: Check for explicit prefixes + for (const [name] of registrations) { + if (lowerModel.startsWith(`${name}-`)) { + return name; + } + } + + // Default to claude (first registered provider or claude) + return 'claude'; + } + /** * Get all available providers */ diff --git a/apps/server/src/routes/setup/get-claude-status.ts b/apps/server/src/routes/setup/get-claude-status.ts index 3ddd8ed4..4a3ccaf6 100644 --- a/apps/server/src/routes/setup/get-claude-status.ts +++ b/apps/server/src/routes/setup/get-claude-status.ts @@ -6,9 +6,24 @@ import { exec } from 'child_process'; import { promisify } from 'util'; import { getClaudeCliPaths, getClaudeAuthIndicators, systemPathAccess } from '@automaker/platform'; import { getApiKey } from './common.js'; +import * as fs from 'fs'; +import * as path from 'path'; const execAsync = promisify(exec); +const DISCONNECTED_MARKER_FILE = '.claude-disconnected'; + +function isDisconnectedFromApp(): boolean { + try { + // Check if we're in a project directory + const projectRoot = process.cwd(); + const markerPath = path.join(projectRoot, '.automaker', DISCONNECTED_MARKER_FILE); + return fs.existsSync(markerPath); + } catch { + return false; + } +} + export async function getClaudeStatus() { let installed = false; let version = ''; @@ -60,6 +75,30 @@ export async function getClaudeStatus() { } } + // Check if user has manually disconnected from the app + if (isDisconnectedFromApp()) { + return { + status: installed ? 'installed' : 'not_installed', + installed, + method, + version, + path: cliPath, + auth: { + authenticated: false, + method: 'none', + hasCredentialsFile: false, + hasToken: false, + hasStoredOAuthToken: false, + hasStoredApiKey: false, + hasEnvApiKey: false, + oauthTokenValid: false, + apiKeyValid: false, + hasCliAuth: false, + hasRecentActivity: false, + }, + }; + } + // Check authentication - detect all possible auth methods // Note: apiKeys.anthropic_oauth_token stores OAuth tokens from subscription auth // apiKeys.anthropic stores direct API keys for pay-per-use diff --git a/apps/server/src/routes/setup/index.ts b/apps/server/src/routes/setup/index.ts index 3fac6a20..917433b7 100644 --- a/apps/server/src/routes/setup/index.ts +++ b/apps/server/src/routes/setup/index.ts @@ -17,6 +17,10 @@ import { createCursorStatusHandler } from './routes/cursor-status.js'; import { createCodexStatusHandler } from './routes/codex-status.js'; import { createInstallCodexHandler } from './routes/install-codex.js'; import { createAuthCodexHandler } from './routes/auth-codex.js'; +import { createAuthCursorHandler } from './routes/auth-cursor.js'; +import { createDeauthClaudeHandler } from './routes/deauth-claude.js'; +import { createDeauthCodexHandler } from './routes/deauth-codex.js'; +import { createDeauthCursorHandler } from './routes/deauth-cursor.js'; import { createGetCursorConfigHandler, createSetCursorDefaultModelHandler, @@ -34,6 +38,7 @@ export function createSetupRoutes(): Router { router.get('/claude-status', createClaudeStatusHandler()); router.post('/install-claude', createInstallClaudeHandler()); router.post('/auth-claude', createAuthClaudeHandler()); + router.post('/deauth-claude', createDeauthClaudeHandler()); router.post('/store-api-key', createStoreApiKeyHandler()); router.post('/delete-api-key', createDeleteApiKeyHandler()); router.get('/api-keys', createApiKeysHandler()); @@ -44,11 +49,14 @@ export function createSetupRoutes(): Router { // Cursor CLI routes router.get('/cursor-status', createCursorStatusHandler()); + router.post('/auth-cursor', createAuthCursorHandler()); + router.post('/deauth-cursor', createDeauthCursorHandler()); // Codex CLI routes router.get('/codex-status', createCodexStatusHandler()); router.post('/install-codex', createInstallCodexHandler()); router.post('/auth-codex', createAuthCodexHandler()); + router.post('/deauth-codex', createDeauthCodexHandler()); router.get('/cursor-config', createGetCursorConfigHandler()); router.post('/cursor-config/default-model', createSetCursorDefaultModelHandler()); router.post('/cursor-config/models', createSetCursorModelsHandler()); diff --git a/apps/server/src/routes/setup/routes/auth-claude.ts b/apps/server/src/routes/setup/routes/auth-claude.ts index 4531501d..97a170f4 100644 --- a/apps/server/src/routes/setup/routes/auth-claude.ts +++ b/apps/server/src/routes/setup/routes/auth-claude.ts @@ -4,19 +4,54 @@ import type { Request, Response } from 'express'; import { getErrorMessage, logError } from '../common.js'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +import * as fs from 'fs'; +import * as path from 'path'; + +const execAsync = promisify(exec); export function createAuthClaudeHandler() { return async (_req: Request, res: Response): Promise => { try { - res.json({ - success: true, - requiresManualAuth: true, - command: 'claude login', - message: "Please run 'claude login' in your terminal to authenticate", - }); + // Remove the disconnected marker file to reconnect the app to the CLI + const markerPath = path.join(process.cwd(), '.automaker', '.claude-disconnected'); + if (fs.existsSync(markerPath)) { + fs.unlinkSync(markerPath); + } + + // Check if CLI is already authenticated by checking auth indicators + const { getClaudeAuthIndicators } = await import('@automaker/platform'); + const indicators = await getClaudeAuthIndicators(); + const isAlreadyAuthenticated = + indicators.hasStatsCacheWithActivity || + (indicators.hasSettingsFile && indicators.hasProjectsSessions) || + indicators.hasCredentialsFile; + + if (isAlreadyAuthenticated) { + // CLI is already authenticated, just reconnect + res.json({ + success: true, + message: 'Claude CLI is now linked with the app', + wasAlreadyAuthenticated: true, + }); + } else { + // CLI needs authentication - but we can't run claude login here + // because it requires browser OAuth. Just reconnect and let the user authenticate if needed. + res.json({ + success: true, + message: + 'Claude CLI is now linked with the app. If prompted, please authenticate with "claude login" in your terminal.', + requiresManualAuth: true, + }); + } } catch (error) { logError(error, 'Auth Claude failed'); - res.status(500).json({ success: false, error: getErrorMessage(error) }); + res.status(500).json({ + success: false, + error: getErrorMessage(error), + message: 'Failed to link Claude CLI with the app', + }); } }; } diff --git a/apps/server/src/routes/setup/routes/auth-codex.ts b/apps/server/src/routes/setup/routes/auth-codex.ts index c58414d7..79857bd8 100644 --- a/apps/server/src/routes/setup/routes/auth-codex.ts +++ b/apps/server/src/routes/setup/routes/auth-codex.ts @@ -4,27 +4,46 @@ import type { Request, Response } from 'express'; import { logError, getErrorMessage } from '../common.js'; +import * as fs from 'fs'; +import * as path from 'path'; -/** - * Creates handler for POST /api/setup/auth-codex - * Returns instructions for manual Codex CLI authentication - */ export function createAuthCodexHandler() { return async (_req: Request, res: Response): Promise => { try { - const loginCommand = 'codex login'; + // Remove the disconnected marker file to reconnect the app to the CLI + const markerPath = path.join(process.cwd(), '.automaker', '.codex-disconnected'); + if (fs.existsSync(markerPath)) { + fs.unlinkSync(markerPath); + } - res.json({ - success: true, - requiresManualAuth: true, - command: loginCommand, - message: `Please authenticate Codex CLI manually by running: ${loginCommand}`, - }); + // Use the same detection logic as the Codex provider + const { getCodexAuthIndicators } = await import('@automaker/platform'); + const indicators = await getCodexAuthIndicators(); + + const isAlreadyAuthenticated = + indicators.hasApiKey || indicators.hasAuthFile || indicators.hasOAuthToken; + + if (isAlreadyAuthenticated) { + // Already has authentication, just reconnect + res.json({ + success: true, + message: 'Codex CLI is now linked with the app', + wasAlreadyAuthenticated: true, + }); + } else { + res.json({ + success: true, + message: + 'Codex CLI is now linked with the app. If prompted, please authenticate with "codex login" in your terminal.', + requiresManualAuth: true, + }); + } } catch (error) { logError(error, 'Auth Codex failed'); res.status(500).json({ success: false, error: getErrorMessage(error), + message: 'Failed to link Codex CLI with the app', }); } }; diff --git a/apps/server/src/routes/setup/routes/auth-cursor.ts b/apps/server/src/routes/setup/routes/auth-cursor.ts new file mode 100644 index 00000000..fbd6339c --- /dev/null +++ b/apps/server/src/routes/setup/routes/auth-cursor.ts @@ -0,0 +1,73 @@ +/** + * POST /auth-cursor endpoint - Authenticate Cursor CLI + */ + +import type { Request, Response } from 'express'; +import { logError, getErrorMessage } from '../common.js'; +import * as fs from 'fs'; +import * as path from 'path'; +import os from 'os'; + +export function createAuthCursorHandler() { + return async (_req: Request, res: Response): Promise => { + try { + // Remove the disconnected marker file to reconnect the app to the CLI + const markerPath = path.join(process.cwd(), '.automaker', '.cursor-disconnected'); + if (fs.existsSync(markerPath)) { + fs.unlinkSync(markerPath); + } + + // Check if Cursor is already authenticated using the same logic as CursorProvider + const isAlreadyAuthenticated = (): boolean => { + // Check for API key in environment + if (process.env.CURSOR_API_KEY) { + return true; + } + + // Check for credentials files + const credentialPaths = [ + path.join(os.homedir(), '.cursor', 'credentials.json'), + path.join(os.homedir(), '.config', 'cursor', 'credentials.json'), + ]; + + for (const credPath of credentialPaths) { + if (fs.existsSync(credPath)) { + try { + const content = fs.readFileSync(credPath, 'utf8'); + const creds = JSON.parse(content); + if (creds.accessToken || creds.token) { + return true; + } + } catch { + // Invalid credentials file, continue checking + } + } + } + + return false; + }; + + if (isAlreadyAuthenticated()) { + res.json({ + success: true, + message: 'Cursor CLI is now linked with the app', + wasAlreadyAuthenticated: true, + }); + } else { + res.json({ + success: true, + message: + 'Cursor CLI is now linked with the app. If prompted, please authenticate with "cursor auth" in your terminal.', + requiresManualAuth: true, + }); + } + } catch (error) { + logError(error, 'Auth Cursor failed'); + res.status(500).json({ + success: false, + error: getErrorMessage(error), + message: 'Failed to link Cursor CLI with the app', + }); + } + }; +} diff --git a/apps/server/src/routes/setup/routes/codex-status.ts b/apps/server/src/routes/setup/routes/codex-status.ts index 84f2c3f4..6e721e05 100644 --- a/apps/server/src/routes/setup/routes/codex-status.ts +++ b/apps/server/src/routes/setup/routes/codex-status.ts @@ -5,6 +5,20 @@ import type { Request, Response } from 'express'; import { CodexProvider } from '../../../providers/codex-provider.js'; import { getErrorMessage, logError } from '../common.js'; +import * as fs from 'fs'; +import * as path from 'path'; + +const DISCONNECTED_MARKER_FILE = '.codex-disconnected'; + +function isCodexDisconnectedFromApp(): boolean { + try { + const projectRoot = process.cwd(); + const markerPath = path.join(projectRoot, '.automaker', DISCONNECTED_MARKER_FILE); + return fs.existsSync(markerPath); + } catch { + return false; + } +} /** * Creates handler for GET /api/setup/codex-status @@ -16,6 +30,24 @@ export function createCodexStatusHandler() { return async (_req: Request, res: Response): Promise => { try { + // Check if user has manually disconnected from the app + if (isCodexDisconnectedFromApp()) { + res.json({ + success: true, + installed: true, + version: null, + path: null, + auth: { + authenticated: false, + method: 'none', + hasApiKey: false, + }, + installCommand, + loginCommand, + }); + return; + } + const provider = new CodexProvider(); const status = await provider.detectInstallation(); diff --git a/apps/server/src/routes/setup/routes/cursor-status.ts b/apps/server/src/routes/setup/routes/cursor-status.ts index 10cc50d5..f9349aa7 100644 --- a/apps/server/src/routes/setup/routes/cursor-status.ts +++ b/apps/server/src/routes/setup/routes/cursor-status.ts @@ -5,6 +5,20 @@ import type { Request, Response } from 'express'; import { CursorProvider } from '../../../providers/cursor-provider.js'; import { getErrorMessage, logError } from '../common.js'; +import * as fs from 'fs'; +import * as path from 'path'; + +const DISCONNECTED_MARKER_FILE = '.cursor-disconnected'; + +function isCursorDisconnectedFromApp(): boolean { + try { + const projectRoot = process.cwd(); + const markerPath = path.join(projectRoot, '.automaker', DISCONNECTED_MARKER_FILE); + return fs.existsSync(markerPath); + } catch { + return false; + } +} /** * Creates handler for GET /api/setup/cursor-status @@ -16,6 +30,30 @@ export function createCursorStatusHandler() { return async (_req: Request, res: Response): Promise => { try { + // Check if user has manually disconnected from the app + if (isCursorDisconnectedFromApp()) { + const provider = new CursorProvider(); + const [installed, version] = await Promise.all([ + provider.isInstalled(), + provider.getVersion(), + ]); + const cliPath = installed ? provider.getCliPath() : null; + + res.json({ + success: true, + installed, + version: version || null, + path: cliPath, + auth: { + authenticated: false, + method: 'none', + }, + installCommand, + loginCommand, + }); + return; + } + const provider = new CursorProvider(); const [installed, version, auth] = await Promise.all([ diff --git a/apps/server/src/routes/setup/routes/deauth-claude.ts b/apps/server/src/routes/setup/routes/deauth-claude.ts new file mode 100644 index 00000000..8f3c1930 --- /dev/null +++ b/apps/server/src/routes/setup/routes/deauth-claude.ts @@ -0,0 +1,44 @@ +/** + * POST /deauth-claude endpoint - Sign out from Claude CLI + */ + +import type { Request, Response } from 'express'; +import { getErrorMessage, logError } from '../common.js'; +import * as fs from 'fs'; +import * as path from 'path'; + +export function createDeauthClaudeHandler() { + return async (_req: Request, res: Response): Promise => { + try { + // Create a marker file to indicate the CLI is disconnected from the app + const automakerDir = path.join(process.cwd(), '.automaker'); + const markerPath = path.join(automakerDir, '.claude-disconnected'); + + // Ensure .automaker directory exists + if (!fs.existsSync(automakerDir)) { + fs.mkdirSync(automakerDir, { recursive: true }); + } + + // Create the marker file with timestamp + fs.writeFileSync( + markerPath, + JSON.stringify({ + disconnectedAt: new Date().toISOString(), + message: 'Claude CLI is disconnected from the app', + }) + ); + + res.json({ + success: true, + message: 'Claude CLI is now disconnected from the app', + }); + } catch (error) { + logError(error, 'Deauth Claude failed'); + res.status(500).json({ + success: false, + error: getErrorMessage(error), + message: 'Failed to disconnect Claude CLI from the app', + }); + } + }; +} diff --git a/apps/server/src/routes/setup/routes/deauth-codex.ts b/apps/server/src/routes/setup/routes/deauth-codex.ts new file mode 100644 index 00000000..f44a6e15 --- /dev/null +++ b/apps/server/src/routes/setup/routes/deauth-codex.ts @@ -0,0 +1,44 @@ +/** + * POST /deauth-codex endpoint - Sign out from Codex CLI + */ + +import type { Request, Response } from 'express'; +import { logError, getErrorMessage } from '../common.js'; +import * as fs from 'fs'; +import * as path from 'path'; + +export function createDeauthCodexHandler() { + return async (_req: Request, res: Response): Promise => { + try { + // Create a marker file to indicate the CLI is disconnected from the app + const automakerDir = path.join(process.cwd(), '.automaker'); + const markerPath = path.join(automakerDir, '.codex-disconnected'); + + // Ensure .automaker directory exists + if (!fs.existsSync(automakerDir)) { + fs.mkdirSync(automakerDir, { recursive: true }); + } + + // Create the marker file with timestamp + fs.writeFileSync( + markerPath, + JSON.stringify({ + disconnectedAt: new Date().toISOString(), + message: 'Codex CLI is disconnected from the app', + }) + ); + + res.json({ + success: true, + message: 'Codex CLI is now disconnected from the app', + }); + } catch (error) { + logError(error, 'Deauth Codex failed'); + res.status(500).json({ + success: false, + error: getErrorMessage(error), + message: 'Failed to disconnect Codex CLI from the app', + }); + } + }; +} diff --git a/apps/server/src/routes/setup/routes/deauth-cursor.ts b/apps/server/src/routes/setup/routes/deauth-cursor.ts new file mode 100644 index 00000000..303b2006 --- /dev/null +++ b/apps/server/src/routes/setup/routes/deauth-cursor.ts @@ -0,0 +1,44 @@ +/** + * POST /deauth-cursor endpoint - Sign out from Cursor CLI + */ + +import type { Request, Response } from 'express'; +import { logError, getErrorMessage } from '../common.js'; +import * as fs from 'fs'; +import * as path from 'path'; + +export function createDeauthCursorHandler() { + return async (_req: Request, res: Response): Promise => { + try { + // Create a marker file to indicate the CLI is disconnected from the app + const automakerDir = path.join(process.cwd(), '.automaker'); + const markerPath = path.join(automakerDir, '.cursor-disconnected'); + + // Ensure .automaker directory exists + if (!fs.existsSync(automakerDir)) { + fs.mkdirSync(automakerDir, { recursive: true }); + } + + // Create the marker file with timestamp + fs.writeFileSync( + markerPath, + JSON.stringify({ + disconnectedAt: new Date().toISOString(), + message: 'Cursor CLI is disconnected from the app', + }) + ); + + res.json({ + success: true, + message: 'Cursor CLI is now disconnected from the app', + }); + } catch (error) { + logError(error, 'Deauth Cursor failed'); + res.status(500).json({ + success: false, + error: getErrorMessage(error), + message: 'Failed to disconnect Cursor CLI from the app', + }); + } + }; +} diff --git a/apps/ui/src/components/views/settings-view/cli-status/claude-cli-status.tsx b/apps/ui/src/components/views/settings-view/cli-status/claude-cli-status.tsx index a777157e..2457969b 100644 --- a/apps/ui/src/components/views/settings-view/cli-status/claude-cli-status.tsx +++ b/apps/ui/src/components/views/settings-view/cli-status/claude-cli-status.tsx @@ -1,9 +1,12 @@ +import { useState, useCallback } from 'react'; import { Button } from '@/components/ui/button'; import { CheckCircle2, AlertCircle, RefreshCw, XCircle } from 'lucide-react'; import { cn } from '@/lib/utils'; import type { CliStatus } from '../shared/types'; import type { ClaudeAuthStatus } from '@/store/setup-store'; import { AnthropicIcon } from '@/components/ui/provider-icon'; +import { getElectronAPI } from '@/lib/electron'; +import { toast } from 'sonner'; interface CliStatusProps { status: CliStatus | null; @@ -81,6 +84,60 @@ function ClaudeCliStatusSkeleton() { } export function ClaudeCliStatus({ status, authStatus, isChecking, onRefresh }: CliStatusProps) { + const [isAuthenticating, setIsAuthenticating] = useState(false); + const [isDeauthenticating, setIsDeauthenticating] = useState(false); + + const handleSignIn = useCallback(async () => { + setIsAuthenticating(true); + try { + const api = getElectronAPI(); + const result = await api.setup.authClaude(); + + if (result.success) { + toast.success('Signed In', { + description: 'Successfully authenticated Claude CLI', + }); + onRefresh(); + } else if (result.error) { + toast.error('Authentication Failed', { + description: result.error, + }); + } + } catch (error) { + toast.error('Authentication Failed', { + description: error instanceof Error ? error.message : 'Unknown error', + }); + } finally { + setIsAuthenticating(false); + } + }, [onRefresh]); + + const handleSignOut = useCallback(async () => { + setIsDeauthenticating(true); + try { + const api = getElectronAPI(); + const result = await api.setup.deauthClaude(); + + if (result.success) { + toast.success('Signed Out', { + description: 'Successfully signed out from Claude CLI', + }); + // Refresh status after successful logout + onRefresh(); + } else if (result.error) { + toast.error('Sign Out Failed', { + description: result.error, + }); + } + } catch (error) { + toast.error('Sign Out Failed', { + description: error instanceof Error ? error.message : 'Unknown error', + }); + } finally { + setIsDeauthenticating(false); + } + }, [onRefresh]); + if (!status) return ; return ( @@ -153,7 +210,7 @@ export function ClaudeCliStatus({ status, authStatus, isChecking, onRefresh }: C {/* Authentication Status */} {authStatus?.authenticated ? ( -
+
@@ -165,6 +222,15 @@ export function ClaudeCliStatus({ status, authStatus, isChecking, onRefresh }: C {getAuthMethodLabel(authStatus.method)}

+
) : ( @@ -175,9 +241,17 @@ export function ClaudeCliStatus({ status, authStatus, isChecking, onRefresh }: C

Not Authenticated

- Run claude login{' '} - or set an API key to authenticate. + Click Sign In below to get authentication instructions.

+
)} diff --git a/apps/ui/src/components/views/settings-view/cli-status/codex-cli-status.tsx b/apps/ui/src/components/views/settings-view/cli-status/codex-cli-status.tsx index fb7af414..86635264 100644 --- a/apps/ui/src/components/views/settings-view/cli-status/codex-cli-status.tsx +++ b/apps/ui/src/components/views/settings-view/cli-status/codex-cli-status.tsx @@ -1,9 +1,12 @@ +import { useState, useCallback } from 'react'; import { Button } from '@/components/ui/button'; import { CheckCircle2, AlertCircle, RefreshCw, XCircle } from 'lucide-react'; import { cn } from '@/lib/utils'; import type { CliStatus } from '../shared/types'; import type { CodexAuthStatus } from '@/store/setup-store'; import { OpenAIIcon } from '@/components/ui/provider-icon'; +import { getElectronAPI } from '@/lib/electron'; +import { toast } from 'sonner'; interface CliStatusProps { status: CliStatus | null; @@ -76,6 +79,60 @@ function CodexCliStatusSkeleton() { } export function CodexCliStatus({ status, authStatus, isChecking, onRefresh }: CliStatusProps) { + const [isAuthenticating, setIsAuthenticating] = useState(false); + const [isDeauthenticating, setIsDeauthenticating] = useState(false); + + const handleSignIn = useCallback(async () => { + setIsAuthenticating(true); + try { + const api = getElectronAPI(); + const result = await api.setup.authCodex(); + + if (result.success) { + toast.success('Signed In', { + description: 'Successfully authenticated Codex CLI', + }); + onRefresh(); + } else if (result.error) { + toast.error('Authentication Failed', { + description: result.error, + }); + } + } catch (error) { + toast.error('Authentication Failed', { + description: error instanceof Error ? error.message : 'Unknown error', + }); + } finally { + setIsAuthenticating(false); + } + }, [onRefresh]); + + const handleSignOut = useCallback(async () => { + setIsDeauthenticating(true); + try { + const api = getElectronAPI(); + const result = await api.setup.deauthCodex(); + + if (result.success) { + toast.success('Signed Out', { + description: 'Successfully signed out from Codex CLI', + }); + // Refresh status after successful logout + onRefresh(); + } else if (result.error) { + toast.error('Sign Out Failed', { + description: result.error, + }); + } + } catch (error) { + toast.error('Sign Out Failed', { + description: error instanceof Error ? error.message : 'Unknown error', + }); + } finally { + setIsDeauthenticating(false); + } + }, [onRefresh]); + if (!status) return ; return ( @@ -145,7 +202,7 @@ export function CodexCliStatus({ status, authStatus, isChecking, onRefresh }: Cl {/* Authentication Status */} {authStatus?.authenticated ? ( -
+
@@ -157,6 +214,15 @@ export function CodexCliStatus({ status, authStatus, isChecking, onRefresh }: Cl {getAuthMethodLabel(authStatus.method)}

+
) : ( @@ -167,9 +233,17 @@ export function CodexCliStatus({ status, authStatus, isChecking, onRefresh }: Cl

Not Authenticated

- Run codex login{' '} - or set an API key to authenticate. + Click Sign In below to get authentication instructions.

+
)} diff --git a/apps/ui/src/components/views/settings-view/cli-status/cursor-cli-status.tsx b/apps/ui/src/components/views/settings-view/cli-status/cursor-cli-status.tsx index ddc7fd24..bc49270c 100644 --- a/apps/ui/src/components/views/settings-view/cli-status/cursor-cli-status.tsx +++ b/apps/ui/src/components/views/settings-view/cli-status/cursor-cli-status.tsx @@ -1,7 +1,10 @@ +import { useState, useCallback } from 'react'; import { Button } from '@/components/ui/button'; import { CheckCircle2, AlertCircle, RefreshCw, XCircle } from 'lucide-react'; import { cn } from '@/lib/utils'; import { CursorIcon } from '@/components/ui/provider-icon'; +import { getElectronAPI } from '@/lib/electron'; +import { toast } from 'sonner'; interface CursorStatus { installed: boolean; @@ -201,6 +204,60 @@ export function ModelConfigSkeleton() { } export function CursorCliStatus({ status, isChecking, onRefresh }: CursorCliStatusProps) { + const [isAuthenticating, setIsAuthenticating] = useState(false); + const [isDeauthenticating, setIsDeauthenticating] = useState(false); + + const handleSignIn = useCallback(async () => { + setIsAuthenticating(true); + try { + const api = getElectronAPI(); + const result = await api.setup.authCursor(); + + if (result.success) { + toast.success('Signed In', { + description: 'Successfully authenticated Cursor CLI', + }); + onRefresh(); + } else if (result.error) { + toast.error('Authentication Failed', { + description: result.error, + }); + } + } catch (error) { + toast.error('Authentication Failed', { + description: error instanceof Error ? error.message : 'Unknown error', + }); + } finally { + setIsAuthenticating(false); + } + }, [onRefresh]); + + const handleSignOut = useCallback(async () => { + setIsDeauthenticating(true); + try { + const api = getElectronAPI(); + const result = await api.setup.deauthCursor(); + + if (result.success) { + toast.success('Signed Out', { + description: 'Successfully signed out from Cursor CLI', + }); + // Refresh status after successful logout + onRefresh(); + } else if (result.error) { + toast.error('Sign Out Failed', { + description: result.error, + }); + } + } catch (error) { + toast.error('Sign Out Failed', { + description: error instanceof Error ? error.message : 'Unknown error', + }); + } finally { + setIsDeauthenticating(false); + } + }, [onRefresh]); + if (!status) return ; return ( @@ -262,7 +319,7 @@ export function CursorCliStatus({ status, isChecking, onRefresh }: CursorCliStat {/* Authentication Status */} {status.authenticated ? ( -
+
@@ -276,6 +333,15 @@ export function CursorCliStatus({ status, isChecking, onRefresh }: CursorCliStat

+
) : ( @@ -286,9 +352,17 @@ export function CursorCliStatus({ status, isChecking, onRefresh }: CursorCliStat

Not Authenticated

- Run cursor auth{' '} - to authenticate with Cursor. + Click Sign In below to get authentication instructions.

+
)} diff --git a/apps/ui/src/lib/electron.ts b/apps/ui/src/lib/electron.ts index 74ca9cc4..197ff81b 100644 --- a/apps/ui/src/lib/electron.ts +++ b/apps/ui/src/lib/electron.ts @@ -1,6 +1,6 @@ // Type definitions for Electron IPC API import type { SessionListItem, Message } from '@/types/electron'; -import type { ClaudeUsageResponse } from '@/store/app-store'; +import type { ClaudeUsageResponse, CodexUsageResponse } from '@/store/app-store'; import type { IssueValidationVerdict, IssueValidationConfidence, @@ -725,6 +725,9 @@ export interface ElectronAPI { }>; }; ideation?: IdeationAPI; + codex?: { + getUsage: () => Promise; + }; } // Note: Window interface is declared in @/types/electron.d.ts diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts index f41a02ea..835785f7 100644 --- a/apps/ui/src/lib/http-api-client.ts +++ b/apps/ui/src/lib/http-api-client.ts @@ -1073,6 +1073,14 @@ export class HttpApiClient implements ElectronAPI { output?: string; }> => this.post('/api/setup/auth-claude'), + deauthClaude: (): Promise<{ + success: boolean; + requiresManualDeauth?: boolean; + command?: string; + message?: string; + error?: string; + }> => this.post('/api/setup/deauth-claude'), + storeApiKey: ( provider: string, apiKey: string @@ -1139,6 +1147,24 @@ export class HttpApiClient implements ElectronAPI { error?: string; }> => this.get('/api/setup/cursor-status'), + authCursor: (): Promise<{ + success: boolean; + token?: string; + requiresManualAuth?: boolean; + terminalOpened?: boolean; + command?: string; + message?: string; + output?: string; + }> => this.post('/api/setup/auth-cursor'), + + deauthCursor: (): Promise<{ + success: boolean; + requiresManualDeauth?: boolean; + command?: string; + message?: string; + error?: string; + }> => this.post('/api/setup/deauth-cursor'), + getCursorConfig: ( projectPath: string ): Promise<{ @@ -1281,6 +1307,14 @@ export class HttpApiClient implements ElectronAPI { output?: string; }> => this.post('/api/setup/auth-codex'), + deauthCodex: (): Promise<{ + success: boolean; + requiresManualDeauth?: boolean; + command?: string; + message?: string; + error?: string; + }> => this.post('/api/setup/deauth-codex'), + verifyCodexAuth: ( authMethod: 'cli' | 'api_key', apiKey?: string diff --git a/apps/ui/src/types/electron.d.ts b/apps/ui/src/types/electron.d.ts index 068feb61..6388e7a5 100644 --- a/apps/ui/src/types/electron.d.ts +++ b/apps/ui/src/types/electron.d.ts @@ -2,6 +2,8 @@ * Electron API type definitions */ +import type { ClaudeUsageResponse, CodexUsageResponse } from '@/store/app-store'; + export interface ImageAttachment { id?: string; // Optional - may not be present in messages loaded from server data: string; // base64 encoded image data @@ -584,6 +586,16 @@ export interface ElectronAPI { error?: string; }>; + // Claude Usage API + claude: { + getUsage: () => Promise; + }; + + // Codex Usage API + codex: { + getUsage: () => Promise; + }; + // Worktree Management APIs worktree: WorktreeAPI;