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
JavaScript
/**
* 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 };