UNPKG

@autifyhq/muon

Version:

Muon - AI-Powered Playwright Test Coding Agent with Advanced Test Fixing Capabilities

150 lines (149 loc) 6.56 kB
export class MuonHTTPClient { constructor(config) { this.config = config; } async request(path, options = { method: 'GET' }) { // Proactively refresh tokens if needed if (this.config.auth && this.config.accessToken) { const validTokens = await this.config.auth.getValidTokens(); if (!validTokens) { // Tokens are no longer valid, clear them console.log('🔄 Access tokens are no longer valid, clearing authentication'); this.config.accessToken = undefined; } else if (validTokens.accessToken !== this.config.accessToken) { console.log('🔄 Using refreshed access token for request'); this.config.accessToken = validTokens.accessToken; } } const url = `${this.config.baseUrl}${path}`; const headers = { 'Content-Type': 'application/json', ...(options.headers || {}), }; // Add authorization header if access token is available if (this.config.accessToken) { headers.Authorization = `Bearer ${this.config.accessToken}`; } else if (this.config.apiKey) { headers['x-muon-api-key'] = this.config.apiKey; } // Add streaming header if requested if (options.stream) { headers['x-muon-streaming'] = 'true'; headers.Accept = 'text/event-stream'; } const fetchOptions = { method: options.method, headers, }; if (options.body && (options.method === 'POST' || options.method === 'PUT')) { fetchOptions.body = options.body; } const response = await fetch(url, fetchOptions); // Handle errors (proactive refresh eliminates need for reactive refresh) if (!response.ok) { const errorText = await response.text(); let errorMessage; try { const errorJson = JSON.parse(errorText); errorMessage = errorJson.error || errorText; } catch { errorMessage = errorText || `HTTP ${response.status}`; } // Provide helpful error messages for auth failures if (response.status === 401) { if (this.config.accessToken) { throw new Error(`Authentication failed: ${errorMessage}\n\n` + 'Your login session has expired and could not be refreshed. Please try:\n' + '1. Run "muon login" to re-authenticate\n' + '2. Or use an API key instead:\n' + ` Get an API key from: ${this.config.baseUrl}/keys`); } else if (errorMessage.toLowerCase().includes('api key')) { throw new Error(`Authentication failed: ${errorMessage}\n\n` + 'Please check your API key:\n' + '1. Ensure MUON_API_KEY environment variable is set correctly\n' + '2. Verify your API key is valid and not expired\n' + `3. Get a new API key from: ${this.config.baseUrl}/keys`); } else { throw new Error(`Authentication failed: ${errorMessage}\n\n` + 'Authentication is required. Please choose one option:\n' + 'Option 1: Run "muon login" to authenticate via OAuth\n' + 'Option 2: Set MUON_API_KEY environment variable\n' + `Option 3: Get an API key from: ${this.config.baseUrl}/keys`); } } if (response.status === 403) { throw new Error(`Access forbidden: ${errorMessage}\n\n` + 'This might be due to:\n' + '1. Inactive subscription\n' + '2. API key without required permissions\n' + '3. Rate limit exceeded\n\n' + `Please check your subscription status at: ${this.config.baseUrl}/billing`); } throw new Error(`Request failed: ${errorMessage}`); } // Handle streaming responses if (options.stream && response.body) { return this.handleStreamingResponse(response); } // Handle regular JSON responses return response.json(); } async *handleStreamingResponse(response) { if (!response.body) { throw new Error('No response body for streaming'); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; try { while (true) { const { done, value } = await reader.read(); if (done) { break; } buffer += decoder.decode(value, { stream: true }); // Process complete lines const lines = buffer.split('\n'); buffer = lines.pop() || ''; // Keep incomplete line in buffer for (const line of lines) { if (!line.trim()) continue; // Handle SSE format (with 'data: ' prefix) if (line.startsWith('data: ')) { try { const data = line.slice(6); // Remove 'data: ' prefix if (data.trim()) { const parsed = JSON.parse(data); yield parsed; } } catch (error) { console.warn('❌ Failed to parse SSE data:', line, error); } } // Handle plain JSON lines (fallback for non-SSE streams) else { try { const parsed = JSON.parse(line); yield parsed; } catch (error) { console.warn('❌ Failed to parse JSON line:', line, error); } } } } } finally { reader.releaseLock(); } } async healthCheck() { await this.request('/api/health', { method: 'GET' }); } }