@stackmemoryai/stackmemory
Version:
Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, a
278 lines (273 loc) • 7.74 kB
JavaScript
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";
import { STRUCTURED_RESPONSE_SUFFIX } from "../../orchestrators/multimodal/constants.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) {
const enhancedRequest = {
...request,
systemPrompt: request.systemPrompt + STRUCTURED_RESPONSE_SUFFIX
};
await this.checkRateLimit();
logger.debug("Sending completion request", {
model: enhancedRequest.model,
promptLength: enhancedRequest.prompt.length,
maxTokens: enhancedRequest.maxTokens
});
if (!this.apiKey) {
return this.mockComplete(enhancedRequest);
}
try {
const response = await this.sendRequest(enhancedRequest);
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
};