Skip to main content
Glama
apiClient.ts21.8 kB
/** DEPRECATED - TEST FOR REMOVAL * @fileOverview: HTTP client for Ambiance API (Cloud) with semantic search and context retrieval capabilities * @module: AmbianceAPIClient * @keyFunctions: * - searchCode(): Perform intelligent code searches across repositories * - getContextBundle(): Create comprehensive context packages for LLM consumption * - handleError(): Robust error handling with retry logic and logging * @dependencies: * - axios: HTTP client with retry and timeout configuration * - logger: Logging utilities for debugging and monitoring * - SearchRequest/ContextBundleRequest: Type definitions for API requests * @context: Provides reliable API communication for semantic code analysis, handling authentication, error recovery, and response validation */ import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios'; import { logger } from '../utils/logger'; export interface SearchRequest { query: string; repo?: string; branch?: string; k?: number; } export interface ContextBundleRequest { query: string; hints?: string[]; token_budget?: number; repo?: string; branch?: string; } export interface GraphContextRequest { query: string; github_repos?: string[]; github_repo?: string; branch?: string; max_nodes?: number; max_tokens?: number; include_related_files?: boolean; focus_areas?: string[]; } export interface GraphContextResponse { context: string; nodes: Array<{ id: string; name: string; kind: string; path: string; startLine: number; endLine: number; relationships?: string[]; }>; relationships: Array<{ source: string; target: string; type: string; strength: number; }>; metadata: { query: string; repositories: string[]; nodeCount: number; tokenCount: number; timestamp: string; embeddingProvider?: string; processingTime?: number; }; budget: { requested: number; used: number; remaining: number; }; } export interface EmbeddingUploadRequest { repo_id: string; chunks: Array<{ file_path: string; content: string; start_line: number; end_line: number; token_estimate: number; content_hash: string; symbol_id?: string; }>; embeddings: Array<{ chunk_index: number; vector: number[]; model: string; }>; session_id?: string; } export interface EmbeddingGenerationRequest { texts: string[]; input_type?: 'document' | 'query'; model?: string; encoding_format?: 'float32' | 'int8'; include_context?: boolean; context_window?: number; } export interface EmbeddingGenerationResponse { embeddings: number[][]; model: string; dimensions: number; input_type: 'document' | 'query'; encoding_format: 'float32' | 'int8'; total_tokens: number; processing_time_ms: number; provider: string; // 'voyageai' | 'openai' | 'local' } export interface SearchResult { id: string; body: string; source: 'cloud'; meta: { language?: string; path: string; startLine: number; endLine: number; sha?: string; }; score: number; path: string; startLine: number; endLine: number; } export interface ContextBundle { snippets: SearchResult[]; budget: { requested: number; used: number; remaining: number; }; metadata: { query: string; repos: string[]; timestamp: string; }; } export class AmbianceAPIClient { private client: AxiosInstance; private apiKey: string; private baseURL: string; constructor(apiKey: string, baseURL: string = 'https://api.ambiance.dev') { this.apiKey = apiKey; // Support local server override if (process.env.USING_LOCAL_SERVER_URL) { this.baseURL = process.env.USING_LOCAL_SERVER_URL; } else { this.baseURL = baseURL; } // Prepare headers - API key is optional for local servers const headers: any = { 'Content-Type': 'application/json', 'User-Agent': 'ambiance-mcp-proxy/0.0.1', }; // Only add Authorization header if API key is provided if (apiKey) { headers.Authorization = `Bearer ${apiKey}`; } this.client = axios.create({ baseURL: this.baseURL, headers, // Match typical Fastify defaults but allow larger bodies when needed timeout: 30000, maxBodyLength: Infinity, maxContentLength: Infinity, }); // Add response interceptor for error handling this.client.interceptors.response.use( (response: AxiosResponse) => response, (error: AxiosError | any) => { if (error.response) { // API returned an error response const { status, data } = error.response; throw new Error(`API Error ${status}: ${data.error || data.message || 'Unknown error'}`); } else if (error.request) { // Network error throw new Error('Network error: Unable to reach Ambiance API'); } else { // Other error throw new Error(`Request error: ${error.message}`); } } ); } private sanitizeForLog(value: any): any { try { if (Array.isArray(value)) { return value.slice(0, 5).map(v => this.sanitizeForLog(v)); } if (value && typeof value === 'object') { const out: any = {}; for (const [k, v] of Object.entries(value)) { const keyLower = k.toLowerCase(); if (keyLower === 'contentgzipbase64') { if (typeof v === 'string') out[k] = `<base64 len=${v.length}>`; else out[k] = '<base64>'; } else if ( keyLower.includes('authorization') || keyLower.includes('key') || keyLower.includes('secret') || keyLower.includes('token') ) { out[k] = '<redacted>'; } else { out[k] = this.sanitizeForLog(v); } } return out; } if (typeof value === 'string' && value.length > 200) { return value.slice(0, 200) + '…'; } return value; } catch { return '<unloggable>'; } } private toSnippet(obj: any, limit: number = 200): string { try { const safe = this.sanitizeForLog(obj); const s = JSON.stringify(safe); return s.length > limit ? s.slice(0, limit) + '…' : s; } catch { return '<unserializable>'; } } async searchContext(request: SearchRequest): Promise<SearchResult[]> { try { logger.info('Searching cloud API', { query: request.query, repo: request.repo, branch: request.branch, }); const response = await this.client.post('/v1/context/search', { query: request.query, repo: request.repo, branch: request.branch || 'main', k: request.k || 12, }); const results = response.data.map( (item: any): SearchResult => ({ id: item.id || `${item.path}:${item.start_line}-${item.end_line}`, body: item.body || item.content, source: 'cloud' as const, meta: { language: item.lang || item.language, path: item.path, startLine: item.start_line || item.startLine, endLine: item.end_line || item.endLine, sha: item.sha || item.commit_sha, }, score: item.score || 0, path: item.path, startLine: item.start_line || item.startLine, endLine: item.end_line || item.endLine, }) ); logger.info('Cloud API search completed', { resultCount: results.length, query: request.query, }); return results; } catch (error) { logger.error('Cloud API search failed', { query: request.query, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); throw error; } } async getContextBundle(request: ContextBundleRequest): Promise<ContextBundle> { try { logger.info('Requesting context bundle', { query: request.query, tokenBudget: request.token_budget, repo: request.repo, branch: request.branch, }); const response = await this.client.post('/v1/context/bundle', { query: request.query, hints: request.hints, tokenBudget: request.token_budget || 4000, projectId: request.repo, branch: request.branch || 'main', }); const bundle: ContextBundle = { snippets: (response.data.contextBundle?.snippets || response.data.snippets || []).map( (item: any): SearchResult => ({ id: item.id || `${item.path}:${item.start_line}-${item.end_line}`, body: item.body || item.content, source: 'cloud' as const, meta: { language: item.lang || item.language, path: item.path, startLine: item.start_line || item.startLine, endLine: item.end_line || item.endLine, sha: item.sha || item.commit_sha, }, score: item.score || 0, path: item.path, startLine: item.start_line || item.startLine, endLine: item.end_line || item.endLine, }) ), budget: response.data.contextBundle?.budget || response.data.budget || { requested: request.token_budget || 4000, used: 0, remaining: request.token_budget || 4000, }, metadata: response.data.contextBundle?.metadata || response.data.metadata || { query: request.query, repos: request.repo ? [request.repo] : [], timestamp: new Date().toISOString(), }, }; logger.info('Context bundle received', { snippetCount: bundle.snippets.length, query: request.query, budgetUsed: bundle.budget.used, }); return bundle; } catch (error) { logger.error('Context bundle request failed', { query: request.query, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); throw error; } } async getGraphContext(request: GraphContextRequest): Promise<GraphContextResponse> { try { logger.info('Requesting graph context', { query: request.query, maxNodes: request.max_nodes, maxTokens: request.max_tokens, repos: request.github_repos || (request.github_repo ? [request.github_repo] : []), branch: request.branch, }); // Build the request payload to match the API expectations const payload: any = { query: request.query, maxNodes: request.max_nodes || 20, maxTokens: request.max_tokens || 8000, includeRelatedFiles: request.include_related_files !== false, branch: request.branch || 'main', }; // Handle repository specification - support both single and multiple repos if (request.github_repos && request.github_repos.length > 0) { payload.projectIds = request.github_repos; } else if (request.github_repo) { payload.projectId = request.github_repo; } if (request.focus_areas && request.focus_areas.length > 0) { payload.focusAreas = request.focus_areas; } const response = await this.client.post('/v1/context/graph', payload); const graphResponse: GraphContextResponse = { context: response.data.context || '', nodes: (response.data.nodes || []).map((node: any) => ({ id: node.id, name: node.name, kind: node.kind, path: node.path, startLine: node.startLine || node.start_line, endLine: node.endLine || node.end_line, relationships: node.relationships || [], })), relationships: (response.data.relationships || []).map((rel: any) => ({ source: rel.source, target: rel.target, type: rel.type, strength: rel.strength || 1.0, })), metadata: { query: request.query, repositories: response.data.metadata?.repositories || request.github_repos || (request.github_repo ? [request.github_repo] : []), nodeCount: response.data.metadata?.nodeCount || (response.data.nodes || []).length, tokenCount: response.data.metadata?.tokenCount || 0, timestamp: response.data.metadata?.timestamp || new Date().toISOString(), embeddingProvider: response.data.metadata?.embeddingProvider, processingTime: response.data.metadata?.processingTime, }, budget: response.data.budget || { requested: request.max_tokens || 8000, used: response.data.metadata?.tokenCount || 0, remaining: (request.max_tokens || 8000) - (response.data.metadata?.tokenCount || 0), }, }; logger.info('Graph context received', { nodeCount: graphResponse.nodes.length, relationshipCount: graphResponse.relationships.length, query: request.query, tokenCount: graphResponse.metadata.tokenCount, embeddingProvider: graphResponse.metadata.embeddingProvider, }); return graphResponse; } catch (error) { logger.error('Graph context request failed', { query: request.query, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); throw error; } } async getRepositories(): Promise<any[]> { try { const response = await this.client.get('/v1/repos/github'); // API returns { repositories: [...] } not direct array return response.data.repositories || []; } catch (error) { logger.error('Failed to fetch repositories', { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); throw error; } } async getAlerts(repoId?: string, since?: string): Promise<any[]> { try { const params = new URLSearchParams(); if (repoId) params.append('repo_id', repoId); if (since) params.append('since', since); const response = await this.client.get(`/v1/alerts?${params}`); return response.data; } catch (error) { logger.error('Failed to fetch alerts', { repoId, since, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); throw error; } } async healthCheck(): Promise<boolean> { try { // Allow skipping the health check when local routes are down or during offline dev if (process.env.SKIP_AMBIANCE_PROBE === 'true') { logger.warn('⏭️ Skipping Ambiance API health check due to SKIP_AMBIANCE_PROBE=true'); return false; } await this.client.get('/health'); return true; } catch (error) { // Demote to warn to avoid noisy logs when the cloud/local API is intentionally offline logger.warn('Health check failed', { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); return false; } } async get(endpoint: string): Promise<any> { try { logger.info(`➡️ GET ${endpoint}`); const response = await this.client.get(endpoint); logger.info(`⬅️ GET ${endpoint} response: ${this.toSnippet(response.data)}`); return response.data; } catch (error) { logger.error('GET request failed', { endpoint, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); throw error; } } async post(endpoint: string, data: any): Promise<any> { try { logger.info(`➡️ POST ${endpoint} body: ${this.toSnippet(data)}`); const response = await this.client.post(endpoint, data); logger.info(`⬅️ POST ${endpoint} response: ${this.toSnippet(response.data)}`); return response.data; } catch (error) { logger.error('POST request failed', { endpoint, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); throw error; } } async put(endpoint: string, data: any): Promise<any> { try { logger.info(`➡️ PUT ${endpoint} body: ${this.toSnippet(data)}`); const response = await this.client.put(endpoint, data); logger.info(`⬅️ PUT ${endpoint} response: ${this.toSnippet(response.data)}`); return response.data; } catch (error) { logger.error('PUT request failed', { endpoint, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); throw error; } } async delete(endpoint: string): Promise<any> { try { logger.info(`➡️ DELETE ${endpoint}`); const response = await this.client.delete(endpoint); logger.info(`⬅️ DELETE ${endpoint} response: ${this.toSnippet(response.data)}`); return response.data; } catch (error) { logger.error('DELETE request failed', { endpoint, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); throw error; } } async uploadEmbeddings(request: EmbeddingUploadRequest): Promise<any> { try { logger.info('Uploading embeddings to cloud', { repoId: request.repo_id, chunkCount: request.chunks.length, embeddingCount: request.embeddings.length, }); const response = await this.client.post('/v1/embeddings/upload', { repo_id: request.repo_id, chunks: request.chunks, embeddings: request.embeddings, session_id: request.session_id, }); logger.info('Embedding upload completed', { repoId: request.repo_id, uploadedChunks: response.data?.uploaded_chunks || 0, uploadedEmbeddings: response.data?.uploaded_embeddings || 0, }); return response.data; } catch (error) { logger.error('Embedding upload failed', { repoId: request.repo_id, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); throw error; } } /** * Generate embeddings for text chunks using Ambiance API */ async generateEmbeddings( request: EmbeddingGenerationRequest ): Promise<EmbeddingGenerationResponse> { try { logger.info('🚀 Generating embeddings via Ambiance API', { textCount: request.texts.length, inputType: request.input_type || 'document', model: request.model || process.env.VOYAGEAI_MODEL || 'voyageai-model', encodingFormat: request.encoding_format || 'float32', }); const response = await this.client.post<EmbeddingGenerationResponse>( '/embeddings/generate', request ); logger.info('✅ Embeddings generated successfully', { textCount: request.texts.length, provider: response.data.provider, model: response.data.model, dimensions: response.data.dimensions, totalTokens: response.data.total_tokens, processingTimeMs: response.data.processing_time_ms, }); return response.data; } catch (error) { logger.error('❌ Embedding generation failed', { textCount: request.texts.length, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); throw error; } } } // Create and export a default instance with lazy initialization let _apiClient: AmbianceAPIClient | null = null; export const apiClient = { get client(): AmbianceAPIClient { if (!_apiClient) { const API_KEY = process.env.AMBIANCE_API_KEY || ''; const API_URL = process.env.USING_LOCAL_SERVER_URL || process.env.AMBIANCE_API_URL || 'https://api.ambiance.dev'; _apiClient = new AmbianceAPIClient(API_KEY, API_URL); } return _apiClient; }, // Proxy methods to maintain the same API async searchContext(request: SearchRequest) { return this.client.searchContext(request); }, async getContextBundle(request: ContextBundleRequest) { return this.client.getContextBundle(request); }, async getGraphContext(request: GraphContextRequest) { return this.client.getGraphContext(request); }, async getRepositories() { return this.client.getRepositories(); }, async getAlerts(repoId?: string, since?: string) { return this.client.getAlerts(repoId, since); }, async healthCheck() { return this.client.healthCheck(); }, async get(endpoint: string) { return this.client.get(endpoint); }, async post(endpoint: string, data: any) { return this.client.post(endpoint, data); }, async put(endpoint: string, data: any) { return this.client.put(endpoint, data); }, async delete(endpoint: string) { return this.client.delete(endpoint); }, async uploadEmbeddings(request: EmbeddingUploadRequest) { return this.client.uploadEmbeddings(request); }, async generateEmbeddings(request: EmbeddingGenerationRequest) { return this.client.generateEmbeddings(request); }, }; // The EmbeddingUploadRequest interface is already exported above

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sbarron/AmbianceMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server