@autifyhq/muon
Version:
Muon - AI-Powered Playwright Test Coding Agent with Advanced Test Fixing Capabilities
150 lines (149 loc) • 6.56 kB
JavaScript
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' });
}
}