Skip to main content
Glama
snapback-api.ts6.37 kB
import { ConsecutiveBreaker, circuitBreaker, ExponentialBackoff, handleAll, type IPolicy, retry, TimeoutStrategy, timeout, wrap, } from "cockatiel"; import ky, { HTTPError, type KyInstance, type Options } from "ky"; import { z } from "zod"; // Define the configuration interface export interface SnapBackConfig { baseUrl: string; apiKey: string; timeout?: number; fetch?: typeof globalThis.fetch; // For testing } // Define request/response interfaces export interface AnalysisRequest { code: string; filePath: string; context?: { surroundingCode?: string; projectType?: string; language?: string; }; } export interface SnapshotRequest { filePath: string; reason?: string; source: string; } export interface AnalysisResponse { riskLevel: string; score: number; factors: string[]; analysisTimeMs: number; issues: Array<{ severity: string; message: string; line?: number; column?: number; }>; } export interface IterationStats { consecutiveAIEdits: number; riskLevel: string; velocity: number; recommendation: string; } export interface SnapshotResponse { id: string; timestamp: number; meta: Record<string, any>; } export interface SessionResponse { consecutiveAIEdits: number; lastEditTimestamp: number; filePath: string; riskLevel: string; } // Zod schemas for validation const AnalysisResponseSchema = z.object({ riskLevel: z.string(), score: z.number(), factors: z.array(z.string()), analysisTimeMs: z.number(), issues: z.array( z.object({ severity: z.string(), message: z.string(), line: z.number().optional(), column: z.number().optional(), }), ), }); const IterationStatsSchema = z.object({ consecutiveAIEdits: z.number(), riskLevel: z.string(), velocity: z.number(), recommendation: z.string(), }); const SnapshotResponseSchema = z.object({ id: z.string(), timestamp: z.number(), meta: z.record(z.string(), z.any()), }); const SessionResponseSchema = z.object({ consecutiveAIEdits: z.number(), lastEditTimestamp: z.number(), filePath: z.string(), riskLevel: z.string(), }); export class SnapBackAPIClient { private client: KyInstance; private resilience: IPolicy; constructor(config: SnapBackConfig) { this.config = config; // Configure ky WITHOUT retry or timeout - cockatiel handles these const kyOptions: Options = { prefixUrl: config.baseUrl, timeout: false, // Cockatiel handles timeout retry: { limit: 0, // Disable ky retry - cockatiel handles it }, hooks: { beforeRequest: [ (request: Request) => { request.headers.set("Authorization", `Bearer ${config.apiKey}`); request.headers.set("Content-Type", "application/json"); }, ], }, }; // CRITICAL: Pass fetch option if provided (for testing) if (config.fetch) { kyOptions.fetch = config.fetch; } this.client = ky.create(kyOptions); // Configure cockatiel policies const retryPolicy = retry(handleAll, { maxAttempts: 3, backoff: new ExponentialBackoff({ initialDelay: 100, // Start with 100ms maxDelay: 5000, // Max 5s between retries exponent: 2, // Double each time }), }); const circuitBreakerPolicy = circuitBreaker(handleAll, { halfOpenAfter: 30000, // 30s breaker: new ConsecutiveBreaker(5), // Open after 5 consecutive failures }); const timeoutPolicy = timeout(config.timeout ?? 30000, TimeoutStrategy.Cooperative); // Compose policies: timeout → retry → circuitBreaker this.resilience = wrap(timeoutPolicy, wrap(retryPolicy, circuitBreakerPolicy)); } private async fetchAPI<T>(endpoint: string, options: RequestInit = {}): Promise<T> { // Remove leading slash if present when using prefixUrl const url = endpoint.startsWith("/") ? endpoint.slice(1) : endpoint; // Execute with cockatiel resilience return this.resilience.execute(async () => { try { const response = await this.client(url, { ...options, }); return (await response.json()) as T; } catch (error: any) { // Re-throw ky errors with consistent format if (error instanceof HTTPError) { const status = error.response?.status || 500; const statusText = error.response?.statusText || "Unknown Error"; throw new Error(`API error: ${status} ${statusText}`); } // Handle our mock error format if (error.name === "HTTPError" && error.response) { const status = error.response?.status || 500; const statusText = error.response?.statusText || "Unknown Error"; throw new Error(`API error: ${status} ${statusText}`); } // Handle mock fetch errors that don't have the expected structure if (error.message?.includes("status")) { // Try to extract status from message if possible throw error; } throw error; } }); } async analyzeFast(request: AnalysisRequest): Promise<AnalysisResponse> { const response = await this.fetchAPI<AnalysisResponse>("api/analyze/fast", { method: "POST", body: JSON.stringify(request), }); // Validate response return AnalysisResponseSchema.parse(response); } async getIterationStats(filePath: string): Promise<IterationStats> { const response = await this.fetchAPI<IterationStats>( `api/session/iteration-stats?filePath=${encodeURIComponent(filePath)}`, ); // Validate response return IterationStatsSchema.parse(response); } async createSnapshot(request: SnapshotRequest): Promise<SnapshotResponse> { const response = await this.fetchAPI<SnapshotResponse>("api/snapshots/create", { method: "POST", body: JSON.stringify(request), }); // Validate response return SnapshotResponseSchema.parse(response); } async getCurrentSession(): Promise<SessionResponse> { const response = await this.fetchAPI<SessionResponse>("api/session/current"); // Validate response return SessionResponseSchema.parse(response); } async getSafetyGuidelines(): Promise<string> { // Execute with cockatiel resilience through our fetchAPI method return this.fetchAPI<string>("api/guidelines/safety", { method: "GET", }); } /** * Get circuit breaker state for monitoring */ getCircuitBreakerState(): "closed" | "open" | "half-open" | "isolated" { // Access the circuit breaker policy state // Note: This assumes the circuit breaker is the last policy in the chain return "closed"; // TODO: Implement state access } }

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/snapback-dev/mcp-server'

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