import { z } from 'zod'; import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ConfigSchema = z.object({ github: z.object({ token: z.string().min(10, 'GITHUB_TOKEN must be a valid token'), owner: z.string().min(1, 'GITHUB_OWNER is required'), repo: z.string().min(1, 'GITHUB_REPO is required'), }), ai: z.object({ provider: z.enum(['ollama', 'huggingface', 'groq', 'openrouter', 'fallback']).default('fallback'), ollama: z.object({ model: z.string().default('llama3.2'), baseUrl: z.string().default('http://localhost:11434'), timeout: z.number().min(1000).max(120000).default(60000), maxRetries: z.number().min(0).max(10).default(3), }), huggingface: z.object({ apiKey: z.string().default(''), model: z.string().default('mistralai/Mistral-7B-Instruct-v0.2'), timeout: z.number().min(1000).max(120000).default(60000), maxRetries: z.number().min(0).max(10).default(3), }), groq: z.object({ apiKey: z.string().default(''), model: z.string().default('llama-3.1-8b-instant'), timeout: z.number().min(1000).max(120000).default(30000), maxRetries: z.number().min(0).max(10).default(3), }), openrouter: z.object({ apiKey: z.string().default(''), model: z.string().default('meta-llama/llama-3.1-8b-instruct:free'), timeout: z.number().min(1000).max(120000).default(30000), maxRetries: z.number().min(0).max(10).default(3), }), }), schedule: z.object({ timezone: z.string().default('Africa/Cairo'), noWorkHoursRestriction: z.boolean().default(true), }), activity: z.object({ minIntervalMinutes: z.number().min(1).max(1440).default(5), maxIntervalMinutes: z.number().min(1).max(1440).default(45), maxCommitsPerSession: z.number().min(1).max(50).default(8), maxFilesPerCommit: z.number().min(1).max(20).default(3), bugProbability: z.number().min(0).max(1).default(0.15), typoProbability: z.number().min(0).max(1).default(0.1), maxActionsPerCycle: z.number().min(1).max(10).default(3), }), daemon: z.object({ enabled: z.boolean().default(true), checkIntervalMinutes: z.number().min(1).max(120).default(10), maxCyclesBeforeRestart: z.number().min(10).max(1000).default(100), }), memory: z.object({ persistToFile: z.boolean().default(true), persistIntervalMinutes: z.number().min(1).max(60).default(15), maxHistorySize: z.number().min(100).max(10000).default(500), maxDecisionsSize: z.number().min(50).max(5000).default(200), }), logging: z.object({ level: z.enum(['debug', 'info', 'warn', 'error']).default('info'), file: z.string().default('logs/simulator.log'), maxFileSize: z.number().min(1000000).max(100000000).default(10000000), }), }); export function loadConfig() { const rawConfig = { github: { token: process.env.GITHUB_TOKEN, owner: process.env.GITHUB_OWNER, repo: process.env.GITHUB_REPO, }, ai: { provider: process.env.AI_PROVIDER || 'fallback', ollama: { model: process.env.OLLAMA_MODEL || 'llama3.2', baseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434', timeout: parseInt(process.env.OLLAMA_TIMEOUT) || 60000, maxRetries: parseInt(process.env.OLLAMA_MAX_RETRIES) || 3, }, huggingface: { apiKey: process.env.HUGGINGFACE_API_KEY || '', model: process.env.HUGGINGFACE_MODEL || 'mistralai/Mistral-7B-Instruct-v0.2', timeout: parseInt(process.env.HUGGINGFACE_TIMEOUT) || 60000, maxRetries: parseInt(process.env.HUGGINGFACE_MAX_RETRIES) || 3, }, groq: { apiKey: process.env.GROQ_API_KEY || '', model: process.env.GROQ_MODEL || 'llama-3.1-8b-instant', timeout: parseInt(process.env.GROQ_TIMEOUT) || 30000, maxRetries: parseInt(process.env.GROQ_MAX_RETRIES) || 3, }, openrouter: { apiKey: process.env.OPENROUTER_API_KEY || '', model: process.env.OPENROUTER_MODEL || 'meta-llama/llama-3.1-8b-instruct:free', timeout: parseInt(process.env.OPENROUTER_TIMEOUT) || 30000, maxRetries: parseInt(process.env.OPENROUTER_MAX_RETRIES) || 3, }, }, schedule: { timezone: process.env.TIMEZONE || 'Africa/Cairo', noWorkHoursRestriction: process.env.NO_WORK_HOURS_RESTRICTION !== 'false', }, activity: { minIntervalMinutes: _parseInt(process.env.MIN_ACTIVITY_INTERVAL_MINUTES, 5), maxIntervalMinutes: _parseInt(process.env.MAX_ACTIVITY_INTERVAL_MINUTES, 45), maxCommitsPerSession: _parseInt(process.env.MAX_COMMITS_PER_SESSION, 8), maxFilesPerCommit: _parseInt(process.env.MAX_FILES_PER_COMMIT, 3), bugProbability: _parseFloat(process.env.BUG_PROBABILITY, 0.15), typoProbability: _parseFloat(process.env.TYPO_PROBABILITY, 0.1), maxActionsPerCycle: _parseInt(process.env.MAX_ACTIONS_PER_CYCLE, 3), }, daemon: { enabled: process.env.DAEMON_ENABLED !== 'false', checkIntervalMinutes: _parseInt(process.env.DAEMON_CHECK_INTERVAL_MINUTES, 10), maxCyclesBeforeRestart: _parseInt(process.env.MAX_CYCLES_BEFORE_RESTART, 100), }, memory: { persistToFile: process.env.MEMORY_PERSIST_TO_FILE !== 'false', persistIntervalMinutes: _parseInt(process.env.MEMORY_PERSIST_INTERVAL_MINUTES, 15), maxHistorySize: _parseInt(process.env.MEMORY_MAX_HISTORY_SIZE, 500), maxDecisionsSize: _parseInt(process.env.MEMORY_MAX_DECISIONS_SIZE, 200), }, logging: { level: process.env.LOG_LEVEL || 'info', file: process.env.LOG_FILE || 'logs/simulator.log', maxFileSize: _parseInt(process.env.LOG_MAX_FILE_SIZE, 10000000), }, }; try { return ConfigSchema.parse(rawConfig); } catch (error) { const formattedErrors = error.errors .map(e => ` - ${e.path.join('.')}: ${e.message}`) .join('\n'); throw new Error(`Invalid configuration:\n${formattedErrors}`); } } export async function saveConfig(config, filePath = '.env') { const envContent = `GITHUB_TOKEN=${config.github.token} GITHUB_OWNER=${config.github.owner} GITHUB_REPO=${config.github.repo} AI_PROVIDER=${config.ai.provider} OLLAMA_MODEL=${config.ai.ollama.model} OLLAMA_BASE_URL=${config.ai.ollama.baseUrl} HUGGINGFACE_API_KEY=${config.ai.huggingface.apiKey} HUGGINGFACE_MODEL=${config.ai.huggingface.model} TIMEZONE=${config.schedule.timezone} NO_WORK_HOURS_RESTRICTION=true MIN_ACTIVITY_INTERVAL_MINUTES=${config.activity.minIntervalMinutes} MAX_ACTIVITY_INTERVAL_MINUTES=${config.activity.maxIntervalMinutes} MAX_COMMITS_PER_SESSION=${config.activity.maxCommitsPerSession} MAX_FILES_PER_COMMIT=${config.activity.maxFilesPerCommit} BUG_PROBABILITY=${config.activity.bugProbability} TYPO_PROBABILITY=${config.activity.typoProbability} DAEMON_ENABLED=${config.daemon.enabled} DAEMON_CHECK_INTERVAL_MINUTES=${config.daemon.checkIntervalMinutes} MEMORY_PERSIST_TO_FILE=${config.memory.persistToFile} MEMORY_PERSIST_INTERVAL_MINUTES=${config.memory.persistIntervalMinutes} LOG_LEVEL=${config.logging.level} LOG_FILE=${config.logging.file} `; await fs.writeFile(filePath, envContent, 'utf8'); } export function validateConfig(config) { try { ConfigSchema.parse(config); return { valid: true, errors: [] }; } catch (error) { return { valid: false, errors: error.errors.map(e => `${e.path.join('.')}: ${e.message}`), }; } } function _parseInt(value, defaultValue) { if (value === undefined || value === null || value === '') return defaultValue; const parsed = parseInt(value, 10); return isNaN(parsed) ? defaultValue : parsed; } function _parseFloat(value, defaultValue) { if (value === undefined || value === null || value === '') return defaultValue; const parsed = parseFloat(value); return isNaN(parsed) ? defaultValue : parsed; } export default loadConfig;