UNPKG

shipdeck

Version:

Ship MVPs in 48 hours. Fix bugs in 30 seconds. The command deck for developers who ship.

446 lines (392 loc) 10.5 kB
/** * Configuration Manager for Anthropic API Integration * Handles API key management, usage limits, and configuration persistence */ const fs = require('fs'); const path = require('path'); const os = require('os'); const CONFIG_DIR = path.join(os.homedir(), '.shipdeck'); const CONFIG_FILE = path.join(CONFIG_DIR, 'ultimate-config.json'); const USAGE_FILE = path.join(CONFIG_DIR, 'usage-stats.json'); // Default configuration const DEFAULT_CONFIG = { anthropic: { model: 'claude-opus-4-1-20250805', // Claude 4.1 Opus as default maxTokensPerRequest: 4096, temperature: 0.7, maxRetries: 3, timeout: 300000 }, usage: { dailyLimitEnabled: false, dailyLimitUSD: 50.0, warningThresholdUSD: 40.0, trackUsage: true }, agents: { autoGenerateTests: true, contextTimeout: 1800000, // 30 minutes defaultAgent: 'backend-architect' }, features: { streamingEnabled: true, parallelExecution: true, costOptimization: true } }; class ConfigManager { constructor() { this.config = null; this.usageStats = null; this._ensureConfigDir(); this._loadConfig(); this._loadUsageStats(); } /** * Ensure configuration directory exists */ _ensureConfigDir() { if (!fs.existsSync(CONFIG_DIR)) { fs.mkdirSync(CONFIG_DIR, { recursive: true }); } } /** * Load configuration from file */ _loadConfig() { try { if (fs.existsSync(CONFIG_FILE)) { const configData = fs.readFileSync(CONFIG_FILE, 'utf8'); this.config = { ...DEFAULT_CONFIG, ...JSON.parse(configData) }; } else { this.config = { ...DEFAULT_CONFIG }; this._saveConfig(); } } catch (error) { console.warn('Failed to load config, using defaults:', error.message); this.config = { ...DEFAULT_CONFIG }; } } /** * Save configuration to file */ _saveConfig() { try { fs.writeFileSync(CONFIG_FILE, JSON.stringify(this.config, null, 2)); } catch (error) { throw new Error(`Failed to save configuration: ${error.message}`); } } /** * Load usage statistics */ _loadUsageStats() { try { if (fs.existsSync(USAGE_FILE)) { const usageData = fs.readFileSync(USAGE_FILE, 'utf8'); this.usageStats = JSON.parse(usageData); } else { this.usageStats = this._createEmptyUsageStats(); this._saveUsageStats(); } } catch (error) { console.warn('Failed to load usage stats, creating new:', error.message); this.usageStats = this._createEmptyUsageStats(); } } /** * Create empty usage statistics structure */ _createEmptyUsageStats() { return { daily: {}, monthly: {}, total: { inputTokens: 0, outputTokens: 0, requests: 0, cost: 0, errors: 0 }, lastReset: new Date().toISOString() }; } /** * Save usage statistics */ _saveUsageStats() { try { fs.writeFileSync(USAGE_FILE, JSON.stringify(this.usageStats, null, 2)); } catch (error) { console.warn('Failed to save usage stats:', error.message); } } /** * Get current configuration */ getConfig() { return { ...this.config }; } /** * Update configuration */ updateConfig(updates) { this.config = { ...this.config, ...updates }; this._saveConfig(); return this.config; } /** * Set Anthropic API key */ setApiKey(apiKey) { if (!apiKey || typeof apiKey !== 'string') { throw new Error('Valid API key is required'); } // Basic validation - Anthropic keys start with 'sk-ant-' if (!apiKey.startsWith('sk-ant-')) { throw new Error('Invalid Anthropic API key format. Keys should start with "sk-ant-"'); } this.config.anthropic = this.config.anthropic || {}; this.config.anthropic.apiKey = apiKey; this._saveConfig(); return true; } /** * Get API key (with masking for display) */ getApiKey(masked = true) { const apiKey = this.config.anthropic?.apiKey; if (!apiKey) return null; if (masked) { return apiKey.substring(0, 12) + '*'.repeat(apiKey.length - 16) + apiKey.substring(apiKey.length - 4); } return apiKey; } /** * Check if API key is configured */ hasApiKey() { return !!(this.config.anthropic?.apiKey); } /** * Update usage statistics */ updateUsage(usage) { const today = new Date().toISOString().split('T')[0]; const thisMonth = today.substring(0, 7); // Initialize daily stats if needed if (!this.usageStats.daily[today]) { this.usageStats.daily[today] = { inputTokens: 0, outputTokens: 0, requests: 0, cost: 0, errors: 0 }; } // Initialize monthly stats if needed if (!this.usageStats.monthly[thisMonth]) { this.usageStats.monthly[thisMonth] = { inputTokens: 0, outputTokens: 0, requests: 0, cost: 0, errors: 0 }; } // Update all stats const stats = [ this.usageStats.daily[today], this.usageStats.monthly[thisMonth], this.usageStats.total ]; stats.forEach(stat => { stat.inputTokens += usage.inputTokens || 0; stat.outputTokens += usage.outputTokens || 0; stat.requests += usage.requests || 0; stat.cost += usage.cost || 0; stat.errors += usage.errors || 0; }); this._saveUsageStats(); return this.getTodayUsage(); } /** * Get today's usage */ getTodayUsage() { const today = new Date().toISOString().split('T')[0]; return this.usageStats.daily[today] || this._createEmptyDayStats(); } /** * Get this month's usage */ getMonthUsage() { const thisMonth = new Date().toISOString().split('T')[0].substring(0, 7); return this.usageStats.monthly[thisMonth] || this._createEmptyDayStats(); } /** * Get total usage */ getTotalUsage() { return { ...this.usageStats.total }; } /** * Create empty day statistics */ _createEmptyDayStats() { return { inputTokens: 0, outputTokens: 0, requests: 0, cost: 0, errors: 0 }; } /** * Check if daily limit is exceeded */ checkDailyLimit() { if (!this.config.usage.dailyLimitEnabled) { return { exceeded: false, limit: null, current: 0 }; } const todayUsage = this.getTodayUsage(); const exceeded = todayUsage.cost >= this.config.usage.dailyLimitUSD; return { exceeded, limit: this.config.usage.dailyLimitUSD, current: todayUsage.cost, warning: todayUsage.cost >= this.config.usage.warningThresholdUSD }; } /** * Set daily spending limit */ setDailyLimit(limitUSD, enabled = true) { if (limitUSD <= 0) { throw new Error('Daily limit must be greater than 0'); } this.config.usage.dailyLimitEnabled = enabled; this.config.usage.dailyLimitUSD = limitUSD; this.config.usage.warningThresholdUSD = Math.min(limitUSD * 0.8, limitUSD - 5); this._saveConfig(); return this.config.usage; } /** * Get usage summary for display */ getUsageSummary() { const today = this.getTodayUsage(); const month = this.getMonthUsage(); const total = this.getTotalUsage(); const limit = this.checkDailyLimit(); return { today: { ...today, totalTokens: today.inputTokens + today.outputTokens }, month: { ...month, totalTokens: month.inputTokens + month.outputTokens }, total: { ...total, totalTokens: total.inputTokens + total.outputTokens }, limits: { daily: limit, hasLimits: this.config.usage.dailyLimitEnabled }, averages: { costPerRequest: total.requests > 0 ? total.cost / total.requests : 0, tokensPerRequest: total.requests > 0 ? (total.inputTokens + total.outputTokens) / total.requests : 0, successRate: total.requests > 0 ? ((total.requests - total.errors) / total.requests) * 100 : 0 } }; } /** * Clean old usage data (keep last 90 days) */ cleanOldUsageData() { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - 90); const cutoffString = cutoffDate.toISOString().split('T')[0]; // Clean daily data Object.keys(this.usageStats.daily).forEach(date => { if (date < cutoffString) { delete this.usageStats.daily[date]; } }); // Keep last 12 months of monthly data const cutoffMonth = cutoffDate.toISOString().substring(0, 7); Object.keys(this.usageStats.monthly).forEach(month => { if (month < cutoffMonth) { delete this.usageStats.monthly[month]; } }); this._saveUsageStats(); } /** * Reset all usage statistics */ resetUsageStats() { this.usageStats = this._createEmptyUsageStats(); this._saveUsageStats(); return this.usageStats; } /** * Export configuration and usage data */ exportData() { return { config: this.getConfig(), usage: this.usageStats, exportedAt: new Date().toISOString() }; } /** * Import configuration and usage data */ importData(data) { if (data.config) { this.config = { ...DEFAULT_CONFIG, ...data.config }; this._saveConfig(); } if (data.usage) { this.usageStats = data.usage; this._saveUsageStats(); } return { configImported: !!data.config, usageImported: !!data.usage }; } /** * Validate configuration */ validateConfig() { const issues = []; // Check API key if (!this.hasApiKey()) { issues.push('Anthropic API key not configured'); } // Check model const supportedModels = [ 'claude-opus-4-1-20250805', // Claude 4.1 Opus (default) 'claude-3-opus-20240229', 'claude-3-5-haiku-20241022', 'claude-3-5-sonnet-20241022' // Deprecated but still supported ]; if (!supportedModels.includes(this.config.anthropic.model)) { issues.push(`Unsupported model: ${this.config.anthropic.model}`); } // Check limits if (this.config.usage.dailyLimitEnabled && this.config.usage.dailyLimitUSD <= 0) { issues.push('Daily limit must be greater than 0'); } return { valid: issues.length === 0, issues }; } } module.exports = { ConfigManager, DEFAULT_CONFIG, CONFIG_DIR };