centaurus-cli 3.1.1 → 3.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/services/api-client.ts"],"sourcesContent":["/**\r\n * API Client Service for Centaurus CLI\r\n * \r\n * Handles all communication with the backend REST API including:\r\n * - Authentication and session management\r\n * - Conversation and message operations\r\n * - User settings management\r\n * - API key storage and retrieval\r\n */\r\n\r\nimport axios, { AxiosInstance, AxiosError } from 'axios';\r\nimport { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'fs';\r\nimport { join, dirname } from 'path';\r\nimport { homedir } from 'os';\r\nimport { IS_DEV_BUILD, DEV_BACKEND_URL, PRODUCTION_BACKEND_URL } from '../config/build-config.js';\r\nimport { logError } from '../utils/logger.js';\r\n\r\n/**\r\n * Type definitions for API requests and responses\r\n */\r\n\r\n// Authentication types\r\ninterface GoogleAuthInitResponse {\r\n authUrl: string;\r\n state: string;\r\n}\r\n\r\ninterface AuthResponse {\r\n sessionToken: string;\r\n expiresAt: string;\r\n user: {\r\n id: string;\r\n email: string;\r\n fullName: string;\r\n avatarUrl?: string;\r\n };\r\n}\r\n\r\ninterface UserProfile {\r\n id: string;\r\n email: string;\r\n fullName: string;\r\n avatarUrl?: string;\r\n createdAt: string;\r\n}\r\n\r\n// Conversation types\r\ninterface CreateConversationRequest {\r\n title: string;\r\n modelUsed: string;\r\n provider: string;\r\n workingDirectory?: string;\r\n tags?: string[];\r\n metadata?: Record<string, any>;\r\n}\r\n\r\ninterface Conversation {\r\n id: string;\r\n userId: string;\r\n title: string;\r\n modelUsed: string;\r\n provider: string;\r\n workingDirectory?: string;\r\n tags: string[];\r\n isPinned: boolean;\r\n metadata: Record<string, any>;\r\n createdAt: string;\r\n updatedAt: string;\r\n archivedAt?: string;\r\n}\r\n\r\ninterface UpdateConversationRequest {\r\n title?: string;\r\n tags?: string[];\r\n isPinned?: boolean;\r\n metadata?: Record<string, any>;\r\n}\r\n\r\n// Message types\r\ninterface CreateMessageRequest {\r\n role: 'user' | 'assistant' | 'system' | 'tool';\r\n content: string;\r\n contentType?: 'text' | 'code' | 'markdown' | 'file' | 'image';\r\n tokensUsed?: number;\r\n toolCalls?: any[];\r\n attachments?: any[];\r\n metadata?: Record<string, any>;\r\n}\r\n\r\ninterface Message {\r\n id: string;\r\n conversationId: string;\r\n role: 'user' | 'assistant' | 'system' | 'tool';\r\n content: string;\r\n contentType: 'text' | 'code' | 'markdown' | 'file' | 'image';\r\n tokensUsed?: number;\r\n toolCalls?: any[];\r\n attachments?: any[];\r\n metadata: Record<string, any>;\r\n createdAt: string;\r\n editedAt?: string;\r\n}\r\n\r\n// Settings types\r\ninterface UserSettings {\r\n defaultModel?: string;\r\n defaultProvider?: string;\r\n theme?: string;\r\n autoSave?: boolean;\r\n planMode?: boolean;\r\n [key: string]: any;\r\n}\r\n\r\n// Models configuration types\r\nexport interface ModelConfig {\r\n id: string;\r\n name: string;\r\n description: string;\r\n provider: string;\r\n contextWindow: number;\r\n region: string;\r\n supportsThinking: boolean;\r\n thinkingConfig?: Record<string, any>;\r\n generationConfig?: {\r\n temperature?: number;\r\n topP?: number;\r\n topK?: number;\r\n maxOutputTokens?: number;\r\n };\r\n allowFrontendDisplay?: boolean; // If false, model is hidden from frontend model picker but valid for API use\r\n}\r\n\r\nexport interface ModelsConfig {\r\n models: ModelConfig[];\r\n defaultModel: string;\r\n}\r\n\r\n// API Response wrapper\r\ninterface ApiResponse<T = any> {\r\n success: boolean;\r\n data?: T;\r\n error?: {\r\n code: string;\r\n message: string;\r\n details?: any;\r\n };\r\n meta?: {\r\n page?: number;\r\n limit?: number;\r\n total?: number;\r\n };\r\n}\r\n\r\n/**\r\n * API Client class for communicating with the backend service\r\n */\r\nclass ApiClient {\r\n private client: AxiosInstance | null = null;\r\n private sessionToken: string | null = null;\r\n private configPath: string;\r\n private configDir: string;\r\n private cachedUser: UserProfile | null = null;\r\n\r\n constructor() {\r\n // Set up session storage path: ~/.centaurus/session.json\r\n this.configDir = join(homedir(), '.centaurus');\r\n this.configPath = join(this.configDir, 'session.json');\r\n\r\n // Load existing session if available\r\n this.loadSession();\r\n\r\n // Don't create axios client yet - wait until first use\r\n // This allows environment variables to be loaded first\r\n }\r\n\r\n /**\r\n * Helper to extract data from API response safely\r\n */\r\n private extractData<T>(response: { data: ApiResponse<T> }): T {\r\n if (response.data.data === undefined || response.data.data === null) {\r\n throw new Error('API response missing expected data payload');\r\n }\r\n return response.data.data as T;\r\n }\r\n\r\n /**\r\n * Get or create the axios client instance\r\n * This is lazy-loaded to ensure environment variables are loaded first\r\n */\r\n private getClient(): AxiosInstance {\r\n if (!this.client) {\r\n // Import build config - values frozen at compile time for security\r\n // SECURITY: This prevents malicious .env files from overriding production URLs\r\n\r\n // Use production URL in production builds, localhost only in dev builds\r\n const baseURL = IS_DEV_BUILD ? DEV_BACKEND_URL : PRODUCTION_BACKEND_URL;\r\n\r\n this.client = axios.create({\r\n baseURL,\r\n timeout: 30000,\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n },\r\n });\r\n\r\n // Request interceptor: Add Authorization header if session token exists\r\n this.getClient().interceptors.request.use(\r\n (config) => {\r\n if (this.sessionToken) {\r\n config.headers.Authorization = `Bearer ${this.sessionToken}`;\r\n }\r\n return config;\r\n },\r\n (error) => {\r\n return Promise.reject(error);\r\n }\r\n );\r\n\r\n // Response interceptor: Handle 401 errors (expired/invalid session)\r\n this.getClient().interceptors.response.use(\r\n (response) => response,\r\n async (error: AxiosError) => {\r\n if (error.response?.status === 401) {\r\n // Clear invalid session\r\n this.clearSession();\r\n\r\n // Create a more user-friendly error\r\n const authError = new Error('Session expired. Please sign in again.');\r\n authError.name = 'AuthenticationError';\r\n throw authError;\r\n }\r\n\r\n // For other errors, extract message from API response if available\r\n if (error.response?.data) {\r\n const apiError = error.response.data as ApiResponse;\r\n if (apiError.error) {\r\n const customError = new Error(apiError.error.message);\r\n customError.name = apiError.error.code;\r\n throw customError;\r\n }\r\n }\r\n\r\n throw error;\r\n }\r\n );\r\n }\r\n\r\n return this.client;\r\n }\r\n\r\n /**\r\n * Load session token from local config file\r\n */\r\n private loadSession(): void {\r\n try {\r\n if (existsSync(this.configPath)) {\r\n const data = readFileSync(this.configPath, 'utf-8');\r\n const session = JSON.parse(data);\r\n this.sessionToken = session.sessionToken || null;\r\n }\r\n } catch (error) {\r\n // If there's any error reading the session, just start fresh\r\n this.sessionToken = null;\r\n }\r\n }\r\n\r\n /**\r\n * Save session token to local config file\r\n */\r\n private saveSession(token: string, expiresAt?: string): void {\r\n try {\r\n // Ensure config directory exists\r\n if (!existsSync(this.configDir)) {\r\n mkdirSync(this.configDir, { recursive: true, mode: 0o700 });\r\n }\r\n\r\n // Save session data\r\n const sessionData = {\r\n sessionToken: token,\r\n expiresAt: expiresAt || null,\r\n savedAt: new Date().toISOString(),\r\n };\r\n\r\n writeFileSync(this.configPath, JSON.stringify(sessionData, null, 2), { encoding: 'utf-8', mode: 0o600 });\r\n this.sessionToken = token;\r\n } catch (error) {\r\n logError('Failed to save session', error as Error);\r\n throw new Error('Failed to save session locally');\r\n }\r\n }\r\n\r\n /**\r\n * Clear session token from memory and local storage\r\n */\r\n private clearSession(): void {\r\n this.sessionToken = null;\r\n this.cachedUser = null;\r\n try {\r\n if (existsSync(this.configPath)) {\r\n unlinkSync(this.configPath);\r\n }\r\n } catch (error) {\r\n // Ignore errors when clearing session\r\n }\r\n }\r\n\r\n /**\r\n * Check if user is authenticated\r\n */\r\n public isAuthenticated(): boolean {\r\n return this.sessionToken !== null;\r\n }\r\n\r\n // ==================== Authentication Methods ====================\r\n\r\n /**\r\n * Initialize Google OAuth flow\r\n * @param redirectUri - The URI to redirect to after OAuth\r\n * @returns OAuth URL and state parameter\r\n */\r\n async initGoogleAuth(redirectUri: string): Promise<GoogleAuthInitResponse> {\r\n const response = await this.getClient().post<ApiResponse<GoogleAuthInitResponse>>(\r\n '/auth/google/init',\r\n { redirectUri }\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Complete Google OAuth authentication\r\n * @param code - Authorization code from Google\r\n * @param state - State parameter for CSRF protection\r\n * @returns Session token and user information\r\n */\r\n async authenticate(code: string, state: string): Promise<AuthResponse> {\r\n const response = await this.getClient().post<ApiResponse<AuthResponse>>(\r\n '/auth/google/callback',\r\n { code, state }\r\n );\r\n\r\n const authData = this.extractData(response);\r\n this.saveSession(authData.sessionToken, authData.expiresAt);\r\n\r\n return authData;\r\n }\r\n\r\n /**\r\n * Set session token directly (used when receiving token from web app)\r\n * @param sessionToken - The session token to save\r\n * @param user - User information\r\n */\r\n setSessionToken(sessionToken: string, user?: any): void {\r\n // Calculate expiration (30 days from now)\r\n const expiresAt = new Date();\r\n expiresAt.setDate(expiresAt.getDate() + 30);\r\n\r\n this.saveSession(sessionToken, expiresAt.toISOString());\r\n }\r\n\r\n /**\r\n * Refresh the current session token\r\n * @returns New session token and expiration\r\n */\r\n async refreshSession(): Promise<{ sessionToken: string; expiresAt: string }> {\r\n const response = await this.getClient().post<ApiResponse<{ sessionToken: string; expiresAt: string }>>(\r\n '/auth/refresh'\r\n );\r\n\r\n const refreshData = this.extractData(response);\r\n this.saveSession(refreshData.sessionToken, refreshData.expiresAt);\r\n\r\n return refreshData;\r\n }\r\n\r\n /**\r\n * Logout and invalidate current session\r\n */\r\n async logout(): Promise<void> {\r\n try {\r\n await this.getClient().post('/auth/logout');\r\n } finally {\r\n // Always clear local session, even if API call fails\r\n this.clearSession();\r\n }\r\n }\r\n\r\n /**\r\n * Get current authenticated user profile\r\n * @returns User profile information\r\n */\r\n async getCurrentUser(): Promise<UserProfile> {\r\n const response = await this.getClient().get<ApiResponse<UserProfile>>('/auth/me');\r\n const user = this.extractData(response);\r\n this.cachedUser = user;\r\n return user;\r\n }\r\n\r\n getCachedUser(): UserProfile | null {\r\n return this.cachedUser;\r\n }\r\n\r\n // ==================== Conversation Methods ====================\r\n\r\n /**\r\n * Create a new conversation\r\n * @param data - Conversation creation parameters\r\n * @returns Created conversation\r\n */\r\n async createConversation(data: CreateConversationRequest): Promise<Conversation> {\r\n const response = await this.getClient().post<ApiResponse<Conversation>>(\r\n '/threads',\r\n data\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Get all conversations for the authenticated user\r\n * @param params - Pagination and filter parameters\r\n * @returns List of conversations with pagination metadata\r\n */\r\n async getConversations(params?: {\r\n page?: number;\r\n limit?: number;\r\n includeArchived?: boolean;\r\n tags?: string[];\r\n }): Promise<ApiResponse<Conversation[]>> {\r\n const queryParams: any = {\r\n page: params?.page || 1,\r\n limit: params?.limit || 20,\r\n };\r\n\r\n if (params?.includeArchived !== undefined) {\r\n queryParams.includeArchived = params.includeArchived;\r\n }\r\n\r\n if (params?.tags && params.tags.length > 0) {\r\n queryParams.tags = params.tags.join(',');\r\n }\r\n\r\n const response = await this.getClient().get<ApiResponse<Conversation[]>>(\r\n '/threads',\r\n { params: queryParams }\r\n );\r\n\r\n return response.data;\r\n }\r\n\r\n /**\r\n * Get a specific conversation by ID\r\n * @param conversationId - The conversation ID\r\n * @returns Conversation details\r\n */\r\n async getConversation(conversationId: string): Promise<Conversation> {\r\n const response = await this.getClient().get<ApiResponse<Conversation>>(\r\n `/threads/${conversationId}`\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Update a conversation\r\n * @param conversationId - The conversation ID\r\n * @param data - Fields to update\r\n * @returns Updated conversation\r\n */\r\n async updateConversation(\r\n conversationId: string,\r\n data: UpdateConversationRequest\r\n ): Promise<Conversation> {\r\n const response = await this.getClient().put<ApiResponse<Conversation>>(\r\n `/threads/${conversationId}`,\r\n data\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Delete (archive) a conversation\r\n * @param conversationId - The conversation ID\r\n */\r\n async deleteConversation(conversationId: string): Promise<void> {\r\n await this.getClient().delete(`/threads/${conversationId}`);\r\n }\r\n\r\n // ==================== Message Methods ====================\r\n\r\n /**\r\n * Add a message to a conversation\r\n * @param conversationId - The conversation ID\r\n * @param message - Message data\r\n * @returns Created message\r\n */\r\n async addMessage(\r\n conversationId: string,\r\n message: CreateMessageRequest\r\n ): Promise<Message> {\r\n const response = await this.getClient().post<ApiResponse<Message>>(\r\n `/threads/${conversationId}/messages`,\r\n message\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Get all messages for a conversation\r\n * @param conversationId - The conversation ID\r\n * @param params - Pagination parameters\r\n * @returns List of messages with pagination metadata\r\n */\r\n async getMessages(\r\n conversationId: string,\r\n params?: { page?: number; limit?: number }\r\n ): Promise<ApiResponse<Message[]>> {\r\n const queryParams = {\r\n page: params?.page || 1,\r\n limit: params?.limit || 50,\r\n };\r\n\r\n const response = await this.getClient().get<ApiResponse<Message[]>>(\r\n `/threads/${conversationId}/messages`,\r\n { params: queryParams }\r\n );\r\n\r\n return response.data;\r\n }\r\n\r\n // ==================== Settings Methods ====================\r\n\r\n /**\r\n * Get user settings\r\n * @returns User settings object\r\n */\r\n async getSettings(): Promise<UserSettings> {\r\n const response = await this.getClient().get<ApiResponse<UserSettings>>('/settings');\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Update user settings\r\n * @param settings - Settings to update (partial update supported)\r\n * @returns Updated settings\r\n */\r\n async updateSettings(settings: Partial<UserSettings>): Promise<UserSettings> {\r\n const response = await this.getClient().put<ApiResponse<UserSettings>>(\r\n '/settings',\r\n settings\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n // ==================== Models Configuration Methods ====================\r\n\r\n /**\r\n * Get available AI models configuration from backend\r\n * This is a public endpoint (no auth required)\r\n * @returns Models configuration including all model variants and default model\r\n */\r\n async getModelsConfig(): Promise<ModelsConfig> {\r\n const response = await this.getClient().get<ApiResponse<ModelsConfig>>('/models');\r\n return this.extractData(response);\r\n }\r\n\r\n // ==================== Rate Limits Configuration Methods ====================\r\n\r\n /**\r\n * Get rate limits configuration from backend\r\n * This is a public endpoint (no auth required)\r\n * @returns Rate limits configuration including session quotas\r\n */\r\n async getRateLimitsConfig(): Promise<{\r\n maxMessagesPerSession: number;\r\n sessionDurationHours: number;\r\n warningThreshold: number;\r\n defaultPlan: string;\r\n }> {\r\n const response = await this.getClient().get<ApiResponse<{\r\n maxMessagesPerSession: number;\r\n sessionDurationHours: number;\r\n warningThreshold: number;\r\n defaultPlan: string;\r\n }>>('/rate-limits/session');\r\n return this.extractData(response);\r\n }\r\n\r\n // ==================== Classification Methods ====================\r\n\r\n /**\r\n * Classify user input to determine if it's a terminal command or AI message\r\n * @param text - Input text to classify\r\n * @returns Mode prediction: 'terminal' or 'ai'\r\n */\r\n async classifyInput(text: string): Promise<'terminal' | 'ai'> {\r\n try {\r\n const response = await this.getClient().post<ApiResponse<{ mode: 'terminal' | 'ai' }>>(\r\n '/classify',\r\n { text }\r\n );\r\n return response.data.data?.mode || 'ai';\r\n } catch (error) {\r\n // On error, return default 'ai' mode - silent fallback\r\n return 'ai';\r\n }\r\n }\r\n\r\n // ==================== File Upload Methods ====================\r\n\r\n /**\r\n * Upload a file to the backend for AI processing\r\n * @param conversationId - The conversation ID\r\n * @param fileName - Original file name\r\n * @param fileType - MIME type\r\n * @param fileData - Base64 encoded file data\r\n * @returns Upload result with gcsUri for Vertex AI\r\n */\r\n async uploadFile(\r\n conversationId: string,\r\n fileName: string,\r\n fileType: string,\r\n fileData: string\r\n ): Promise<{\r\n id: string;\r\n storagePath: string;\r\n publicUrl: string;\r\n fileName: string;\r\n fileType: string;\r\n fileSize: number;\r\n gcsUri?: string;\r\n gcsPath?: string;\r\n }> {\r\n const response = await this.getClient().post<ApiResponse<{\r\n id: string;\r\n storagePath: string;\r\n publicUrl: string;\r\n fileName: string;\r\n fileType: string;\r\n fileSize: number;\r\n gcsUri?: string;\r\n gcsPath?: string;\r\n }>>('/files', { conversationId, fileName, fileType, mimeType: fileType, fileData, clientType: 'cli' });\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Delete all files for a conversation from storage (Supabase and GCS)\r\n * Call this when deleting a conversation to clean up associated images\r\n * @param conversationId - The conversation ID\r\n */\r\n async deleteConversationFiles(conversationId: string): Promise<void> {\r\n try {\r\n await this.getClient().delete(`/files/by-thread/${conversationId}`);\r\n } catch (error) {\r\n // Silently fail - files might not exist or user might not be authenticated\r\n // This is a cleanup operation, so we don't want to block chat deletion\r\n }\r\n }\r\n\r\n // ==================== Sync Methods ====================\r\n\r\n /**\r\n * Upload sync data (combined chat history and config)\r\n * @param syncData - The combined data to sync\r\n * @returns Upload result with version info\r\n */\r\n async uploadSyncData(syncData: any): Promise<{\r\n id: string;\r\n version: number;\r\n updatedAt: string;\r\n }> {\r\n const response = await this.getClient().post<ApiResponse<{\r\n id: string;\r\n version: number;\r\n updatedAt: string;\r\n }>>('/sync', { syncData });\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Get sync data for the current user\r\n * @returns Sync data or null if not found\r\n */\r\n async getSyncData(): Promise<{\r\n id: string;\r\n syncData: any;\r\n version: number;\r\n createdAt: string;\r\n updatedAt: string;\r\n } | null> {\r\n try {\r\n const response = await this.getClient().get<ApiResponse<{\r\n id: string;\r\n syncData: any;\r\n version: number;\r\n createdAt: string;\r\n updatedAt: string;\r\n }>>('/sync');\r\n return this.extractData(response);\r\n } catch (error: any) {\r\n if (error.response?.status === 404) {\r\n return null;\r\n }\r\n throw error;\r\n }\r\n }\r\n\r\n /**\r\n * Delete sync data for the current user\r\n */\r\n async deleteSyncData(): Promise<void> {\r\n await this.getClient().delete('/sync');\r\n }\r\n\r\n // ==================== Token Counting Methods ====================\r\n\r\n /**\r\n * Count tokens for a given model and messages\r\n * Uses backend's accurate token counting (Vertex AI countTokens API)\r\n * @param model - Model name (e.g., gemini-2.5-flash)\r\n * @param messages - Array of conversation messages\r\n * @returns Total token count including system prompt\r\n */\r\n async countTokens(model: string, messages: any[]): Promise<number> {\r\n try {\r\n const response = await this.getClient().post<ApiResponse<{ tokenCount: number; model: string }>>(\r\n '/chat/token-count',\r\n { model, messages }\r\n );\r\n return response.data.data?.tokenCount || 0;\r\n } catch (error) {\r\n logError('Failed to count tokens via API', error as Error);\r\n\r\n // Fallback to character-based estimation\r\n const totalCharacters = messages.reduce((sum, msg) => {\r\n const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);\r\n return sum + content.length;\r\n }, 0);\r\n\r\n // Add system prompt estimate (roughly 14000 characters)\r\n // Use 1 token ≈ 4 characters for Gemini models\r\n return Math.ceil((totalCharacters + 14000) / 4);\r\n }\r\n }\r\n\r\n // ==================== Health Check ====================\r\n\r\n /**\r\n * Check backend service health\r\n * @returns Health status information\r\n */\r\n async healthCheck(): Promise<{\r\n status: string;\r\n timestamp: string;\r\n database: string;\r\n version: string;\r\n }> {\r\n // Health endpoint is at root level, not under /api\r\n // So we need to construct the full URL manually\r\n // Use build config for URL (frozen at compile time)\r\n const baseURL = IS_DEV_BUILD ? DEV_BACKEND_URL : PRODUCTION_BACKEND_URL;\r\n const healthURL = baseURL.replace('/v1', '/health');\r\n\r\n const response = await axios.get<ApiResponse<any>>(healthURL);\r\n return this.extractData(response);\r\n }\r\n}\r\n\r\n// Export singleton instance\r\nexport const apiClient = new ApiClient();\r\n\r\n// Export types for use in other modules\r\nexport type {\r\n GoogleAuthInitResponse,\r\n AuthResponse,\r\n UserProfile,\r\n CreateConversationRequest,\r\n Conversation,\r\n UpdateConversationRequest,\r\n CreateMessageRequest,\r\n Message,\r\n UserSettings,\r\n ApiResponse,\r\n};\r\n\r\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/services/api-client.ts"],"sourcesContent":["/**\r\n * API Client Service for Centaurus CLI\r\n * \r\n * Handles all communication with the backend REST API including:\r\n * - Authentication and session management\r\n * - Conversation and message operations\r\n * - User settings management\r\n * - API key storage and retrieval\r\n */\r\n\r\nimport axios, { AxiosInstance, AxiosError } from 'axios';\r\nimport { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'fs';\r\nimport { join, dirname } from 'path';\r\nimport { homedir } from 'os';\r\nimport { IS_DEV_BUILD, DEV_BACKEND_URL, PRODUCTION_BACKEND_URL } from '../config/build-config.js';\r\nimport { logError } from '../utils/logger.js';\r\n\r\n/**\r\n * Type definitions for API requests and responses\r\n */\r\n\r\n// Authentication types\r\ninterface GoogleAuthInitResponse {\r\n authUrl: string;\r\n state: string;\r\n}\r\n\r\ninterface AuthResponse {\r\n sessionToken: string;\r\n expiresAt: string;\r\n user: {\r\n id: string;\r\n email: string;\r\n fullName: string;\r\n avatarUrl?: string;\r\n };\r\n}\r\n\r\ninterface UserProfile {\r\n id: string;\r\n email: string;\r\n fullName: string;\r\n avatarUrl?: string;\r\n createdAt: string;\r\n}\r\n\r\n// Conversation types\r\ninterface CreateConversationRequest {\r\n title: string;\r\n modelUsed: string;\r\n provider: string;\r\n workingDirectory?: string;\r\n tags?: string[];\r\n metadata?: Record<string, any>;\r\n}\r\n\r\ninterface Conversation {\r\n id: string;\r\n userId: string;\r\n title: string;\r\n modelUsed: string;\r\n provider: string;\r\n workingDirectory?: string;\r\n tags: string[];\r\n isPinned: boolean;\r\n metadata: Record<string, any>;\r\n createdAt: string;\r\n updatedAt: string;\r\n archivedAt?: string;\r\n}\r\n\r\ninterface UpdateConversationRequest {\r\n title?: string;\r\n tags?: string[];\r\n isPinned?: boolean;\r\n metadata?: Record<string, any>;\r\n}\r\n\r\n// Message types\r\ninterface CreateMessageRequest {\r\n role: 'user' | 'assistant' | 'system' | 'tool';\r\n content: string;\r\n contentType?: 'text' | 'code' | 'markdown' | 'file' | 'image';\r\n tokensUsed?: number;\r\n toolCalls?: any[];\r\n attachments?: any[];\r\n metadata?: Record<string, any>;\r\n}\r\n\r\ninterface Message {\r\n id: string;\r\n conversationId: string;\r\n role: 'user' | 'assistant' | 'system' | 'tool';\r\n content: string;\r\n contentType: 'text' | 'code' | 'markdown' | 'file' | 'image';\r\n tokensUsed?: number;\r\n toolCalls?: any[];\r\n attachments?: any[];\r\n metadata: Record<string, any>;\r\n createdAt: string;\r\n editedAt?: string;\r\n}\r\n\r\n// Settings types\r\ninterface UserSettings {\r\n defaultModel?: string;\r\n defaultProvider?: string;\r\n theme?: string;\r\n autoSave?: boolean;\r\n planMode?: boolean;\r\n [key: string]: any;\r\n}\r\n\r\n// Models configuration types\r\nexport interface ModelConfig {\r\n uid: string; // Unique identifier per entry (e.g. \"claude-opus-4-6-thinking\")\r\n id: string; // Actual API model identifier (may be shared across entries)\r\n name: string;\r\n description: string;\r\n provider: string;\r\n contextWindow: number;\r\n region: string;\r\n supportsThinking: boolean;\r\n thinkingConfig?: Record<string, any>;\r\n generationConfig?: {\r\n temperature?: number;\r\n topP?: number;\r\n topK?: number;\r\n maxOutputTokens?: number;\r\n };\r\n allowFrontendDisplay?: boolean; // If false, model is hidden from frontend model picker but valid for API use\r\n}\r\n\r\nexport interface ModelsConfig {\r\n models: ModelConfig[];\r\n defaultModel: string;\r\n}\r\n\r\n// API Response wrapper\r\ninterface ApiResponse<T = any> {\r\n success: boolean;\r\n data?: T;\r\n error?: {\r\n code: string;\r\n message: string;\r\n details?: any;\r\n };\r\n meta?: {\r\n page?: number;\r\n limit?: number;\r\n total?: number;\r\n };\r\n}\r\n\r\n/**\r\n * API Client class for communicating with the backend service\r\n */\r\nclass ApiClient {\r\n private client: AxiosInstance | null = null;\r\n private sessionToken: string | null = null;\r\n private configPath: string;\r\n private configDir: string;\r\n private cachedUser: UserProfile | null = null;\r\n\r\n constructor() {\r\n // Set up session storage path: ~/.centaurus/session.json\r\n this.configDir = join(homedir(), '.centaurus');\r\n this.configPath = join(this.configDir, 'session.json');\r\n\r\n // Load existing session if available\r\n this.loadSession();\r\n\r\n // Don't create axios client yet - wait until first use\r\n // This allows environment variables to be loaded first\r\n }\r\n\r\n /**\r\n * Helper to extract data from API response safely\r\n */\r\n private extractData<T>(response: { data: ApiResponse<T> }): T {\r\n if (response.data.data === undefined || response.data.data === null) {\r\n throw new Error('API response missing expected data payload');\r\n }\r\n return response.data.data as T;\r\n }\r\n\r\n /**\r\n * Get or create the axios client instance\r\n * This is lazy-loaded to ensure environment variables are loaded first\r\n */\r\n private getClient(): AxiosInstance {\r\n if (!this.client) {\r\n // Import build config - values frozen at compile time for security\r\n // SECURITY: This prevents malicious .env files from overriding production URLs\r\n\r\n // Use production URL in production builds, localhost only in dev builds\r\n const baseURL = IS_DEV_BUILD ? DEV_BACKEND_URL : PRODUCTION_BACKEND_URL;\r\n\r\n this.client = axios.create({\r\n baseURL,\r\n timeout: 30000,\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n },\r\n });\r\n\r\n // Request interceptor: Add Authorization header if session token exists\r\n this.getClient().interceptors.request.use(\r\n (config) => {\r\n if (this.sessionToken) {\r\n config.headers.Authorization = `Bearer ${this.sessionToken}`;\r\n }\r\n return config;\r\n },\r\n (error) => {\r\n return Promise.reject(error);\r\n }\r\n );\r\n\r\n // Response interceptor: Handle 401 errors (expired/invalid session)\r\n this.getClient().interceptors.response.use(\r\n (response) => response,\r\n async (error: AxiosError) => {\r\n if (error.response?.status === 401) {\r\n // Clear invalid session\r\n this.clearSession();\r\n\r\n // Create a more user-friendly error\r\n const authError = new Error('Session expired. Please sign in again.');\r\n authError.name = 'AuthenticationError';\r\n throw authError;\r\n }\r\n\r\n // For other errors, extract message from API response if available\r\n if (error.response?.data) {\r\n const apiError = error.response.data as ApiResponse;\r\n if (apiError.error) {\r\n const customError = new Error(apiError.error.message);\r\n customError.name = apiError.error.code;\r\n throw customError;\r\n }\r\n }\r\n\r\n throw error;\r\n }\r\n );\r\n }\r\n\r\n return this.client;\r\n }\r\n\r\n /**\r\n * Load session token from local config file\r\n */\r\n private loadSession(): void {\r\n try {\r\n if (existsSync(this.configPath)) {\r\n const data = readFileSync(this.configPath, 'utf-8');\r\n const session = JSON.parse(data);\r\n this.sessionToken = session.sessionToken || null;\r\n }\r\n } catch (error) {\r\n // If there's any error reading the session, just start fresh\r\n this.sessionToken = null;\r\n }\r\n }\r\n\r\n /**\r\n * Save session token to local config file\r\n */\r\n private saveSession(token: string, expiresAt?: string): void {\r\n try {\r\n // Ensure config directory exists\r\n if (!existsSync(this.configDir)) {\r\n mkdirSync(this.configDir, { recursive: true, mode: 0o700 });\r\n }\r\n\r\n // Save session data\r\n const sessionData = {\r\n sessionToken: token,\r\n expiresAt: expiresAt || null,\r\n savedAt: new Date().toISOString(),\r\n };\r\n\r\n writeFileSync(this.configPath, JSON.stringify(sessionData, null, 2), { encoding: 'utf-8', mode: 0o600 });\r\n this.sessionToken = token;\r\n } catch (error) {\r\n logError('Failed to save session', error as Error);\r\n throw new Error('Failed to save session locally');\r\n }\r\n }\r\n\r\n /**\r\n * Clear session token from memory and local storage\r\n */\r\n private clearSession(): void {\r\n this.sessionToken = null;\r\n this.cachedUser = null;\r\n try {\r\n if (existsSync(this.configPath)) {\r\n unlinkSync(this.configPath);\r\n }\r\n } catch (error) {\r\n // Ignore errors when clearing session\r\n }\r\n }\r\n\r\n /**\r\n * Check if user is authenticated\r\n */\r\n public isAuthenticated(): boolean {\r\n return this.sessionToken !== null;\r\n }\r\n\r\n // ==================== Authentication Methods ====================\r\n\r\n /**\r\n * Initialize Google OAuth flow\r\n * @param redirectUri - The URI to redirect to after OAuth\r\n * @returns OAuth URL and state parameter\r\n */\r\n async initGoogleAuth(redirectUri: string): Promise<GoogleAuthInitResponse> {\r\n const response = await this.getClient().post<ApiResponse<GoogleAuthInitResponse>>(\r\n '/auth/google/init',\r\n { redirectUri }\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Complete Google OAuth authentication\r\n * @param code - Authorization code from Google\r\n * @param state - State parameter for CSRF protection\r\n * @returns Session token and user information\r\n */\r\n async authenticate(code: string, state: string): Promise<AuthResponse> {\r\n const response = await this.getClient().post<ApiResponse<AuthResponse>>(\r\n '/auth/google/callback',\r\n { code, state }\r\n );\r\n\r\n const authData = this.extractData(response);\r\n this.saveSession(authData.sessionToken, authData.expiresAt);\r\n\r\n return authData;\r\n }\r\n\r\n /**\r\n * Set session token directly (used when receiving token from web app)\r\n * @param sessionToken - The session token to save\r\n * @param user - User information\r\n */\r\n setSessionToken(sessionToken: string, user?: any): void {\r\n // Calculate expiration (30 days from now)\r\n const expiresAt = new Date();\r\n expiresAt.setDate(expiresAt.getDate() + 30);\r\n\r\n this.saveSession(sessionToken, expiresAt.toISOString());\r\n }\r\n\r\n /**\r\n * Refresh the current session token\r\n * @returns New session token and expiration\r\n */\r\n async refreshSession(): Promise<{ sessionToken: string; expiresAt: string }> {\r\n const response = await this.getClient().post<ApiResponse<{ sessionToken: string; expiresAt: string }>>(\r\n '/auth/refresh'\r\n );\r\n\r\n const refreshData = this.extractData(response);\r\n this.saveSession(refreshData.sessionToken, refreshData.expiresAt);\r\n\r\n return refreshData;\r\n }\r\n\r\n /**\r\n * Logout and invalidate current session\r\n */\r\n async logout(): Promise<void> {\r\n try {\r\n await this.getClient().post('/auth/logout');\r\n } finally {\r\n // Always clear local session, even if API call fails\r\n this.clearSession();\r\n }\r\n }\r\n\r\n /**\r\n * Get current authenticated user profile\r\n * @returns User profile information\r\n */\r\n async getCurrentUser(): Promise<UserProfile> {\r\n const response = await this.getClient().get<ApiResponse<UserProfile>>('/auth/me');\r\n const user = this.extractData(response);\r\n this.cachedUser = user;\r\n return user;\r\n }\r\n\r\n getCachedUser(): UserProfile | null {\r\n return this.cachedUser;\r\n }\r\n\r\n // ==================== Conversation Methods ====================\r\n\r\n /**\r\n * Create a new conversation\r\n * @param data - Conversation creation parameters\r\n * @returns Created conversation\r\n */\r\n async createConversation(data: CreateConversationRequest): Promise<Conversation> {\r\n const response = await this.getClient().post<ApiResponse<Conversation>>(\r\n '/threads',\r\n data\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Get all conversations for the authenticated user\r\n * @param params - Pagination and filter parameters\r\n * @returns List of conversations with pagination metadata\r\n */\r\n async getConversations(params?: {\r\n page?: number;\r\n limit?: number;\r\n includeArchived?: boolean;\r\n tags?: string[];\r\n }): Promise<ApiResponse<Conversation[]>> {\r\n const queryParams: any = {\r\n page: params?.page || 1,\r\n limit: params?.limit || 20,\r\n };\r\n\r\n if (params?.includeArchived !== undefined) {\r\n queryParams.includeArchived = params.includeArchived;\r\n }\r\n\r\n if (params?.tags && params.tags.length > 0) {\r\n queryParams.tags = params.tags.join(',');\r\n }\r\n\r\n const response = await this.getClient().get<ApiResponse<Conversation[]>>(\r\n '/threads',\r\n { params: queryParams }\r\n );\r\n\r\n return response.data;\r\n }\r\n\r\n /**\r\n * Get a specific conversation by ID\r\n * @param conversationId - The conversation ID\r\n * @returns Conversation details\r\n */\r\n async getConversation(conversationId: string): Promise<Conversation> {\r\n const response = await this.getClient().get<ApiResponse<Conversation>>(\r\n `/threads/${conversationId}`\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Update a conversation\r\n * @param conversationId - The conversation ID\r\n * @param data - Fields to update\r\n * @returns Updated conversation\r\n */\r\n async updateConversation(\r\n conversationId: string,\r\n data: UpdateConversationRequest\r\n ): Promise<Conversation> {\r\n const response = await this.getClient().put<ApiResponse<Conversation>>(\r\n `/threads/${conversationId}`,\r\n data\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Delete (archive) a conversation\r\n * @param conversationId - The conversation ID\r\n */\r\n async deleteConversation(conversationId: string): Promise<void> {\r\n await this.getClient().delete(`/threads/${conversationId}`);\r\n }\r\n\r\n // ==================== Message Methods ====================\r\n\r\n /**\r\n * Add a message to a conversation\r\n * @param conversationId - The conversation ID\r\n * @param message - Message data\r\n * @returns Created message\r\n */\r\n async addMessage(\r\n conversationId: string,\r\n message: CreateMessageRequest\r\n ): Promise<Message> {\r\n const response = await this.getClient().post<ApiResponse<Message>>(\r\n `/threads/${conversationId}/messages`,\r\n message\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Get all messages for a conversation\r\n * @param conversationId - The conversation ID\r\n * @param params - Pagination parameters\r\n * @returns List of messages with pagination metadata\r\n */\r\n async getMessages(\r\n conversationId: string,\r\n params?: { page?: number; limit?: number }\r\n ): Promise<ApiResponse<Message[]>> {\r\n const queryParams = {\r\n page: params?.page || 1,\r\n limit: params?.limit || 50,\r\n };\r\n\r\n const response = await this.getClient().get<ApiResponse<Message[]>>(\r\n `/threads/${conversationId}/messages`,\r\n { params: queryParams }\r\n );\r\n\r\n return response.data;\r\n }\r\n\r\n // ==================== Settings Methods ====================\r\n\r\n /**\r\n * Get user settings\r\n * @returns User settings object\r\n */\r\n async getSettings(): Promise<UserSettings> {\r\n const response = await this.getClient().get<ApiResponse<UserSettings>>('/settings');\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Update user settings\r\n * @param settings - Settings to update (partial update supported)\r\n * @returns Updated settings\r\n */\r\n async updateSettings(settings: Partial<UserSettings>): Promise<UserSettings> {\r\n const response = await this.getClient().put<ApiResponse<UserSettings>>(\r\n '/settings',\r\n settings\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n // ==================== Models Configuration Methods ====================\r\n\r\n /**\r\n * Get available AI models configuration from backend\r\n * This is a public endpoint (no auth required)\r\n * @returns Models configuration including all model variants and default model\r\n */\r\n async getModelsConfig(): Promise<ModelsConfig> {\r\n const response = await this.getClient().get<ApiResponse<ModelsConfig>>('/models');\r\n return this.extractData(response);\r\n }\r\n\r\n // ==================== Rate Limits Configuration Methods ====================\r\n\r\n /**\r\n * Get rate limits configuration from backend\r\n * This is a public endpoint (no auth required)\r\n * @returns Rate limits configuration including session quotas\r\n */\r\n async getRateLimitsConfig(): Promise<{\r\n maxMessagesPerSession: number;\r\n sessionDurationHours: number;\r\n warningThreshold: number;\r\n defaultPlan: string;\r\n }> {\r\n const response = await this.getClient().get<ApiResponse<{\r\n maxMessagesPerSession: number;\r\n sessionDurationHours: number;\r\n warningThreshold: number;\r\n defaultPlan: string;\r\n }>>('/rate-limits/session');\r\n return this.extractData(response);\r\n }\r\n\r\n // ==================== Classification Methods ====================\r\n\r\n /**\r\n * Classify user input to determine if it's a terminal command or AI message\r\n * @param text - Input text to classify\r\n * @returns Mode prediction: 'terminal' or 'ai'\r\n */\r\n async classifyInput(text: string): Promise<'terminal' | 'ai'> {\r\n try {\r\n const response = await this.getClient().post<ApiResponse<{ mode: 'terminal' | 'ai' }>>(\r\n '/classify',\r\n { text }\r\n );\r\n return response.data.data?.mode || 'ai';\r\n } catch (error) {\r\n // On error, return default 'ai' mode - silent fallback\r\n return 'ai';\r\n }\r\n }\r\n\r\n // ==================== File Upload Methods ====================\r\n\r\n /**\r\n * Upload a file to the backend for AI processing\r\n * @param conversationId - The conversation ID\r\n * @param fileName - Original file name\r\n * @param fileType - MIME type\r\n * @param fileData - Base64 encoded file data\r\n * @returns Upload result with gcsUri for Vertex AI\r\n */\r\n async uploadFile(\r\n conversationId: string,\r\n fileName: string,\r\n fileType: string,\r\n fileData: string\r\n ): Promise<{\r\n id: string;\r\n storagePath: string;\r\n publicUrl: string;\r\n fileName: string;\r\n fileType: string;\r\n fileSize: number;\r\n gcsUri?: string;\r\n gcsPath?: string;\r\n }> {\r\n const response = await this.getClient().post<ApiResponse<{\r\n id: string;\r\n storagePath: string;\r\n publicUrl: string;\r\n fileName: string;\r\n fileType: string;\r\n fileSize: number;\r\n gcsUri?: string;\r\n gcsPath?: string;\r\n }>>('/files', { conversationId, fileName, fileType, mimeType: fileType, fileData, clientType: 'cli' });\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Delete all files for a conversation from storage (Supabase and GCS)\r\n * Call this when deleting a conversation to clean up associated images\r\n * @param conversationId - The conversation ID\r\n */\r\n async deleteConversationFiles(conversationId: string): Promise<void> {\r\n try {\r\n await this.getClient().delete(`/files/by-thread/${conversationId}`);\r\n } catch (error) {\r\n // Silently fail - files might not exist or user might not be authenticated\r\n // This is a cleanup operation, so we don't want to block chat deletion\r\n }\r\n }\r\n\r\n // ==================== Sync Methods ====================\r\n\r\n /**\r\n * Upload sync data (combined chat history and config)\r\n * @param syncData - The combined data to sync\r\n * @returns Upload result with version info\r\n */\r\n async uploadSyncData(syncData: any): Promise<{\r\n id: string;\r\n version: number;\r\n updatedAt: string;\r\n }> {\r\n const response = await this.getClient().post<ApiResponse<{\r\n id: string;\r\n version: number;\r\n updatedAt: string;\r\n }>>('/sync', { syncData });\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Get sync data for the current user\r\n * @returns Sync data or null if not found\r\n */\r\n async getSyncData(): Promise<{\r\n id: string;\r\n syncData: any;\r\n version: number;\r\n createdAt: string;\r\n updatedAt: string;\r\n } | null> {\r\n try {\r\n const response = await this.getClient().get<ApiResponse<{\r\n id: string;\r\n syncData: any;\r\n version: number;\r\n createdAt: string;\r\n updatedAt: string;\r\n }>>('/sync');\r\n return this.extractData(response);\r\n } catch (error: any) {\r\n if (error.response?.status === 404) {\r\n return null;\r\n }\r\n throw error;\r\n }\r\n }\r\n\r\n /**\r\n * Delete sync data for the current user\r\n */\r\n async deleteSyncData(): Promise<void> {\r\n await this.getClient().delete('/sync');\r\n }\r\n\r\n // ==================== Token Counting Methods ====================\r\n\r\n /**\r\n * Count tokens for a given model and messages\r\n * Uses backend's accurate token counting (Vertex AI countTokens API)\r\n * @param model - Model name (e.g., gemini-2.5-flash)\r\n * @param messages - Array of conversation messages\r\n * @returns Total token count including system prompt\r\n */\r\n async countTokens(model: string, messages: any[]): Promise<number> {\r\n try {\r\n const response = await this.getClient().post<ApiResponse<{ tokenCount: number; model: string }>>(\r\n '/chat/token-count',\r\n { model, messages }\r\n );\r\n return response.data.data?.tokenCount || 0;\r\n } catch (error) {\r\n logError('Failed to count tokens via API', error as Error);\r\n\r\n // Fallback to character-based estimation\r\n const totalCharacters = messages.reduce((sum, msg) => {\r\n const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);\r\n return sum + content.length;\r\n }, 0);\r\n\r\n // Add system prompt estimate (roughly 14000 characters)\r\n // Use 1 token ≈ 4 characters for Gemini models\r\n return Math.ceil((totalCharacters + 14000) / 4);\r\n }\r\n }\r\n\r\n // ==================== Health Check ====================\r\n\r\n /**\r\n * Check backend service health\r\n * @returns Health status information\r\n */\r\n async healthCheck(): Promise<{\r\n status: string;\r\n timestamp: string;\r\n database: string;\r\n version: string;\r\n }> {\r\n // Health endpoint is at root level, not under /api\r\n // So we need to construct the full URL manually\r\n // Use build config for URL (frozen at compile time)\r\n const baseURL = IS_DEV_BUILD ? DEV_BACKEND_URL : PRODUCTION_BACKEND_URL;\r\n const healthURL = baseURL.replace('/v1', '/health');\r\n\r\n const response = await axios.get<ApiResponse<any>>(healthURL);\r\n return this.extractData(response);\r\n }\r\n}\r\n\r\n// Export singleton instance\r\nexport const apiClient = new ApiClient();\r\n\r\n// Export types for use in other modules\r\nexport type {\r\n GoogleAuthInitResponse,\r\n AuthResponse,\r\n UserProfile,\r\n CreateConversationRequest,\r\n Conversation,\r\n UpdateConversationRequest,\r\n CreateMessageRequest,\r\n Message,\r\n UserSettings,\r\n ApiResponse,\r\n};\r\n\r\n"],"mappings":"AAUA,OAAO,WAA0C;AACjD,SAAS,cAAc,eAAe,WAAW,YAAY,kBAAkB;AAC/E,SAAS,YAAqB;AAC9B,SAAS,eAAe;AACxB,SAAS,cAAc,iBAAiB,8BAA8B;AACtE,SAAS,gBAAgB;AA8IzB,MAAM,UAAU;AAAA,EACN,SAA+B;AAAA,EAC/B,eAA8B;AAAA,EAC9B;AAAA,EACA;AAAA,EACA,aAAiC;AAAA,EAEzC,cAAc;AAEZ,SAAK,YAAY,KAAK,QAAQ,GAAG,YAAY;AAC7C,SAAK,aAAa,KAAK,KAAK,WAAW,cAAc;AAGrD,SAAK,YAAY;AAAA,EAInB;AAAA;AAAA;AAAA;AAAA,EAKQ,YAAe,UAAuC;AAC5D,QAAI,SAAS,KAAK,SAAS,UAAa,SAAS,KAAK,SAAS,MAAM;AACnE,YAAM,IAAI,MAAM,4CAA4C;AAAA,IAC9D;AACA,WAAO,SAAS,KAAK;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,YAA2B;AACjC,QAAI,CAAC,KAAK,QAAQ;AAKhB,YAAM,UAAU,eAAe,kBAAkB;AAEjD,WAAK,SAAS,MAAM,OAAO;AAAA,QACzB;AAAA,QACA,SAAS;AAAA,QACT,SAAS;AAAA,UACP,gBAAgB;AAAA,QAClB;AAAA,MACF,CAAC;AAGD,WAAK,UAAU,EAAE,aAAa,QAAQ;AAAA,QACpC,CAAC,WAAW;AACV,cAAI,KAAK,cAAc;AACrB,mBAAO,QAAQ,gBAAgB,UAAU,KAAK,YAAY;AAAA,UAC5D;AACA,iBAAO;AAAA,QACT;AAAA,QACA,CAAC,UAAU;AACT,iBAAO,QAAQ,OAAO,KAAK;AAAA,QAC7B;AAAA,MACF;AAGA,WAAK,UAAU,EAAE,aAAa,SAAS;AAAA,QACrC,CAAC,aAAa;AAAA,QACd,OAAO,UAAsB;AAC3B,cAAI,MAAM,UAAU,WAAW,KAAK;AAElC,iBAAK,aAAa;AAGlB,kBAAM,YAAY,IAAI,MAAM,wCAAwC;AACpE,sBAAU,OAAO;AACjB,kBAAM;AAAA,UACR;AAGA,cAAI,MAAM,UAAU,MAAM;AACxB,kBAAM,WAAW,MAAM,SAAS;AAChC,gBAAI,SAAS,OAAO;AAClB,oBAAM,cAAc,IAAI,MAAM,SAAS,MAAM,OAAO;AACpD,0BAAY,OAAO,SAAS,MAAM;AAClC,oBAAM;AAAA,YACR;AAAA,UACF;AAEA,gBAAM;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAEA,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKQ,cAAoB;AAC1B,QAAI;AACF,UAAI,WAAW,KAAK,UAAU,GAAG;AAC/B,cAAM,OAAO,aAAa,KAAK,YAAY,OAAO;AAClD,cAAM,UAAU,KAAK,MAAM,IAAI;AAC/B,aAAK,eAAe,QAAQ,gBAAgB;AAAA,MAC9C;AAAA,IACF,SAAS,OAAO;AAEd,WAAK,eAAe;AAAA,IACtB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,YAAY,OAAe,WAA0B;AAC3D,QAAI;AAEF,UAAI,CAAC,WAAW,KAAK,SAAS,GAAG;AAC/B,kBAAU,KAAK,WAAW,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AAAA,MAC5D;AAGA,YAAM,cAAc;AAAA,QAClB,cAAc;AAAA,QACd,WAAW,aAAa;AAAA,QACxB,UAAS,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC;AAEA,oBAAc,KAAK,YAAY,KAAK,UAAU,aAAa,MAAM,CAAC,GAAG,EAAE,UAAU,SAAS,MAAM,IAAM,CAAC;AACvG,WAAK,eAAe;AAAA,IACtB,SAAS,OAAO;AACd,eAAS,0BAA0B,KAAc;AACjD,YAAM,IAAI,MAAM,gCAAgC;AAAA,IAClD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,eAAqB;AAC3B,SAAK,eAAe;AACpB,SAAK,aAAa;AAClB,QAAI;AACF,UAAI,WAAW,KAAK,UAAU,GAAG;AAC/B,mBAAW,KAAK,UAAU;AAAA,MAC5B;AAAA,IACF,SAAS,OAAO;AAAA,IAEhB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKO,kBAA2B;AAChC,WAAO,KAAK,iBAAiB;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,eAAe,aAAsD;AACzE,UAAM,WAAW,MAAM,KAAK,UAAU,EAAE;AAAA,MACtC;AAAA,MACA,EAAE,YAAY;AAAA,IAChB;AACA,WAAO,KAAK,YAAY,QAAQ;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,aAAa,MAAc,OAAsC;AACrE,UAAM,WAAW,MAAM,KAAK,UAAU,EAAE;AAAA,MACtC;AAAA,MACA,EAAE,MAAM,MAAM;AAAA,IAChB;AAEA,UAAM,WAAW,KAAK,YAAY,QAAQ;AAC1C,SAAK,YAAY,SAAS,cAAc,SAAS,SAAS;AAE1D,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,gBAAgB,cAAsB,MAAkB;AAEtD,UAAM,YAAY,oBAAI,KAAK;AAC3B,cAAU,QAAQ,UAAU,QAAQ,IAAI,EAAE;AAE1C,SAAK,YAAY,cAAc,UAAU,YAAY,CAAC;AAAA,EACxD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,iBAAuE;AAC3E,UAAM,WAAW,MAAM,KAAK,UAAU,EAAE;AAAA,MACtC;AAAA,IACF;AAEA,UAAM,cAAc,KAAK,YAAY,QAAQ;AAC7C,SAAK,YAAY,YAAY,cAAc,YAAY,SAAS;AAEhE,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,SAAwB;AAC5B,QAAI;AACF,YAAM,KAAK,UAAU,EAAE,KAAK,cAAc;AAAA,IAC5C,UAAE;AAEA,WAAK,aAAa;AAAA,IACpB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,iBAAuC;AAC3C,UAAM,WAAW,MAAM,KAAK,UAAU,EAAE,IAA8B,UAAU;AAChF,UAAM,OAAO,KAAK,YAAY,QAAQ;AACtC,SAAK,aAAa;AAClB,WAAO;AAAA,EACT;AAAA,EAEA,gBAAoC;AAClC,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,mBAAmB,MAAwD;AAC/E,UAAM,WAAW,MAAM,KAAK,UAAU,EAAE;AAAA,MACtC;AAAA,MACA;AAAA,IACF;AACA,WAAO,KAAK,YAAY,QAAQ;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,iBAAiB,QAKkB;AACvC,UAAM,cAAmB;AAAA,MACvB,MAAM,QAAQ,QAAQ;AAAA,MACtB,OAAO,QAAQ,SAAS;AAAA,IAC1B;AAEA,QAAI,QAAQ,oBAAoB,QAAW;AACzC,kBAAY,kBAAkB,OAAO;AAAA,IACvC;AAEA,QAAI,QAAQ,QAAQ,OAAO,KAAK,SAAS,GAAG;AAC1C,kBAAY,OAAO,OAAO,KAAK,KAAK,GAAG;AAAA,IACzC;AAEA,UAAM,WAAW,MAAM,KAAK,UAAU,EAAE;AAAA,MACtC;AAAA,MACA,EAAE,QAAQ,YAAY;AAAA,IACxB;AAEA,WAAO,SAAS;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,gBAAgB,gBAA+C;AACnE,UAAM,WAAW,MAAM,KAAK,UAAU,EAAE;AAAA,MACtC,YAAY,cAAc;AAAA,IAC5B;AACA,WAAO,KAAK,YAAY,QAAQ;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,mBACJ,gBACA,MACuB;AACvB,UAAM,WAAW,MAAM,KAAK,UAAU,EAAE;AAAA,MACtC,YAAY,cAAc;AAAA,MAC1B;AAAA,IACF;AACA,WAAO,KAAK,YAAY,QAAQ;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,mBAAmB,gBAAuC;AAC9D,UAAM,KAAK,UAAU,EAAE,OAAO,YAAY,cAAc,EAAE;AAAA,EAC5D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,WACJ,gBACA,SACkB;AAClB,UAAM,WAAW,MAAM,KAAK,UAAU,EAAE;AAAA,MACtC,YAAY,cAAc;AAAA,MAC1B;AAAA,IACF;AACA,WAAO,KAAK,YAAY,QAAQ;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,YACJ,gBACA,QACiC;AACjC,UAAM,cAAc;AAAA,MAClB,MAAM,QAAQ,QAAQ;AAAA,MACtB,OAAO,QAAQ,SAAS;AAAA,IAC1B;AAEA,UAAM,WAAW,MAAM,KAAK,UAAU,EAAE;AAAA,MACtC,YAAY,cAAc;AAAA,MAC1B,EAAE,QAAQ,YAAY;AAAA,IACxB;AAEA,WAAO,SAAS;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,cAAqC;AACzC,UAAM,WAAW,MAAM,KAAK,UAAU,EAAE,IAA+B,WAAW;AAClF,WAAO,KAAK,YAAY,QAAQ;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,eAAe,UAAwD;AAC3E,UAAM,WAAW,MAAM,KAAK,UAAU,EAAE;AAAA,MACtC;AAAA,MACA;AAAA,IACF;AACA,WAAO,KAAK,YAAY,QAAQ;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,kBAAyC;AAC7C,UAAM,WAAW,MAAM,KAAK,UAAU,EAAE,IAA+B,SAAS;AAChF,WAAO,KAAK,YAAY,QAAQ;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,sBAKH;AACD,UAAM,WAAW,MAAM,KAAK,UAAU,EAAE,IAKpC,sBAAsB;AAC1B,WAAO,KAAK,YAAY,QAAQ;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,cAAc,MAA0C;AAC5D,QAAI;AACF,YAAM,WAAW,MAAM,KAAK,UAAU,EAAE;AAAA,QACtC;AAAA,QACA,EAAE,KAAK;AAAA,MACT;AACA,aAAO,SAAS,KAAK,MAAM,QAAQ;AAAA,IACrC,SAAS,OAAO;AAEd,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,WACJ,gBACA,UACA,UACA,UAUC;AACD,UAAM,WAAW,MAAM,KAAK,UAAU,EAAE,KASpC,UAAU,EAAE,gBAAgB,UAAU,UAAU,UAAU,UAAU,UAAU,YAAY,MAAM,CAAC;AACrG,WAAO,KAAK,YAAY,QAAQ;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,wBAAwB,gBAAuC;AACnE,QAAI;AACF,YAAM,KAAK,UAAU,EAAE,OAAO,oBAAoB,cAAc,EAAE;AAAA,IACpE,SAAS,OAAO;AAAA,IAGhB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,eAAe,UAIlB;AACD,UAAM,WAAW,MAAM,KAAK,UAAU,EAAE,KAIpC,SAAS,EAAE,SAAS,CAAC;AACzB,WAAO,KAAK,YAAY,QAAQ;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,cAMI;AACR,QAAI;AACF,YAAM,WAAW,MAAM,KAAK,UAAU,EAAE,IAMpC,OAAO;AACX,aAAO,KAAK,YAAY,QAAQ;AAAA,IAClC,SAAS,OAAY;AACnB,UAAI,MAAM,UAAU,WAAW,KAAK;AAClC,eAAO;AAAA,MACT;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,iBAAgC;AACpC,UAAM,KAAK,UAAU,EAAE,OAAO,OAAO;AAAA,EACvC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,YAAY,OAAe,UAAkC;AACjE,QAAI;AACF,YAAM,WAAW,MAAM,KAAK,UAAU,EAAE;AAAA,QACtC;AAAA,QACA,EAAE,OAAO,SAAS;AAAA,MACpB;AACA,aAAO,SAAS,KAAK,MAAM,cAAc;AAAA,IAC3C,SAAS,OAAO;AACd,eAAS,kCAAkC,KAAc;AAGzD,YAAM,kBAAkB,SAAS,OAAO,CAAC,KAAK,QAAQ;AACpD,cAAM,UAAU,OAAO,IAAI,YAAY,WAAW,IAAI,UAAU,KAAK,UAAU,IAAI,OAAO;AAC1F,eAAO,MAAM,QAAQ;AAAA,MACvB,GAAG,CAAC;AAIJ,aAAO,KAAK,MAAM,kBAAkB,QAAS,CAAC;AAAA,IAChD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,cAKH;AAID,UAAM,UAAU,eAAe,kBAAkB;AACjD,UAAM,YAAY,QAAQ,QAAQ,OAAO,SAAS;AAElD,UAAM,WAAW,MAAM,MAAM,IAAsB,SAAS;AAC5D,WAAO,KAAK,YAAY,QAAQ;AAAA,EAClC;AACF;AAGO,MAAM,YAAY,IAAI,UAAU;","names":[]}
@@ -3074,7 +3074,7 @@ const App = ({
3074
3074
  }
3075
3075
  }
3076
3076
  }
3077
- ), state.screen === "version-update" && state.versionInfo && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(WelcomeBanner, null), /* @__PURE__ */ React.createElement(
3077
+ ), state.screen === "version-update" && state.versionInfo && /* @__PURE__ */ React.createElement(
3078
3078
  VersionUpdatePrompt,
3079
3079
  {
3080
3080
  currentVersion: state.versionInfo.currentVersion,
@@ -3086,7 +3086,7 @@ const App = ({
3086
3086
  }));
3087
3087
  }
3088
3088
  }
3089
- )), state.screen === "background-task-list" && state.backgroundTasks && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#9966ff", paddingX: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "#9966ff", bold: true }, "Running Background Tasks"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Select a task to view its output in focus mode"), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(
3089
+ ), state.screen === "background-task-list" && state.backgroundTasks && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#9966ff", paddingX: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "#9966ff", bold: true }, "Running Background Tasks"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Select a task to view its output in focus mode"), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(
3090
3090
  SelectInput,
3091
3091
  {
3092
3092
  items: state.backgroundTasks.map((task) => {