UNPKG

@stackmemoryai/stackmemory

Version:

Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.

264 lines (259 loc) 7.32 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { logger } from "../../core/monitoring/logger.js"; class AnthropicClient { apiKey; baseURL; maxRetries; timeout; // Rate limiting requestCount = 0; lastResetTime = Date.now(); rateLimitPerMinute = 60; constructor(config) { this.apiKey = config?.apiKey || process.env["ANTHROPIC_API_KEY"] || ""; this.baseURL = config?.baseURL || "https://api.anthropic.com"; this.maxRetries = config?.maxRetries || 3; this.timeout = config?.timeout || 6e4; if (!this.apiKey) { logger.warn("Anthropic API key not configured. Using mock mode."); } logger.info("Anthropic client initialized", { baseURL: this.baseURL, maxRetries: this.maxRetries, timeout: this.timeout, mockMode: !this.apiKey }); } /** * Send completion request to Claude */ async complete(request) { await this.checkRateLimit(); logger.debug("Sending completion request", { model: request.model, promptLength: request.prompt.length, maxTokens: request.maxTokens }); if (!this.apiKey) { return this.mockComplete(request); } try { const response = await this.sendRequest(request); return response.content; } catch (error) { logger.error("Anthropic API error", { error }); throw error; } } /** * Send request with retry logic */ async sendRequest(request, attempt = 1) { try { return this.createMockResponse(request); } catch (error) { if (attempt < this.maxRetries) { const delay = Math.pow(2, attempt) * 1e3; logger.warn(`Retrying after ${delay}ms (attempt ${attempt}/${this.maxRetries})`); await this.delay(delay); return this.sendRequest(request, attempt + 1); } throw error; } } /** * Mock completion for development/testing */ async mockComplete(request) { await this.delay(500 + Math.random() * 1500); if (request.systemPrompt.includes("Planning Agent")) { return this.mockPlanningResponse(request.prompt); } else if (request.systemPrompt.includes("Code Agent")) { return this.mockCodeResponse(request.prompt); } else if (request.systemPrompt.includes("Testing Agent")) { return this.mockTestingResponse(request.prompt); } else if (request.systemPrompt.includes("Review Agent")) { return this.mockReviewResponse(request.prompt); } else { return `Mock response for: ${request.prompt.slice(0, 100)}...`; } } /** * Mock response generators */ mockPlanningResponse(prompt) { return JSON.stringify({ plan: { type: "sequential", tasks: [ { id: "task-1", description: "Analyze requirements", agent: "context", dependencies: [] }, { id: "task-2", type: "parallel", description: "Implementation phase", children: [ { id: "task-2a", description: "Write core logic", agent: "code", dependencies: ["task-1"] }, { id: "task-2b", description: "Write tests", agent: "testing", dependencies: ["task-1"] } ] }, { id: "task-3", description: "Review and improve", agent: "review", dependencies: ["task-2"] } ] } }, null, 2); } mockCodeResponse(prompt) { return ` // Mock implementation export function processTask(input: string): string { // TODO: Implement actual logic console.log('Processing:', input); return \`Processed: \${input}\`; } export function validateInput(input: unknown): boolean { return typeof input === 'string' && input.length > 0; } `.trim(); } mockTestingResponse(prompt) { return ` import { describe, test, expect } from 'vitest'; import { processTask, validateInput } from './implementation'; describe('processTask', () => { test('should process valid input', () => { const result = processTask('test input'); expect(result).toBe('Processed: test input'); }); test('should handle empty input', () => { const result = processTask(''); expect(result).toBe('Processed: '); }); }); describe('validateInput', () => { test('should validate string input', () => { expect(validateInput('valid')).toBe(true); expect(validateInput('')).toBe(false); expect(validateInput(123)).toBe(false); expect(validateInput(null)).toBe(false); }); }); `.trim(); } mockReviewResponse(prompt) { return JSON.stringify({ quality: 0.75, issues: [ "Missing error handling in processTask function", "No input validation before processing", "Tests could cover more edge cases" ], suggestions: [ "Add try-catch block in processTask", "Validate input length and type", "Add tests for special characters and long inputs", "Consider adding performance tests" ], improvements: [ { file: "implementation.ts", line: 3, suggestion: "Add input validation", priority: "high" }, { file: "tests.ts", line: 15, suggestion: "Add edge case tests", priority: "medium" } ] }, null, 2); } /** * Create mock response object */ createMockResponse(request) { const content = this.mockComplete(request).toString(); return { content, stopReason: "stop_sequence", model: request.model, usage: { inputTokens: Math.ceil(request.prompt.length / 4), outputTokens: Math.ceil(content.length / 4) } }; } /** * Check rate limiting */ async checkRateLimit() { const now = Date.now(); const timeSinceReset = now - this.lastResetTime; if (timeSinceReset >= 6e4) { this.requestCount = 0; this.lastResetTime = now; } if (this.requestCount >= this.rateLimitPerMinute) { const waitTime = 6e4 - timeSinceReset; logger.warn(`Rate limit reached, waiting ${waitTime}ms`); await this.delay(waitTime); this.requestCount = 0; this.lastResetTime = Date.now(); } this.requestCount++; } /** * Stream completion response */ async *streamComplete(request) { const response = await this.complete(request); const words = response.split(" "); for (const word of words) { yield word + " "; await this.delay(50); } } /** * Utility delay function */ delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Get API usage statistics */ getUsageStats() { return { requestCount: this.requestCount, rateLimitRemaining: this.rateLimitPerMinute - this.requestCount, resetTime: new Date(this.lastResetTime + 6e4).toISOString() }; } } export { AnthropicClient }; //# sourceMappingURL=client.js.map