'use strict'; /** * config.js — Centralized env-var config with validation. * All config is read from environment variables. * Throws on missing required variables at startup. */ function getEnv(name, defaultValue, required = false) { const val = process.env[name]; if (val === undefined || val === '') { if (required) { throw new Error(`Required environment variable ${name} is not set`); } return defaultValue; } return val; } function getEnvInt(name, defaultValue, required = false) { const val = getEnv(name, undefined, required); if (val === undefined) return defaultValue; const n = parseInt(val, 10); if (isNaN(n)) throw new Error(`Environment variable ${name} must be an integer, got: ${val}`); return n; } function getEnvBool(name, defaultValue) { const val = process.env[name]; if (val === undefined || val === '') return defaultValue; return val === '1' || val.toLowerCase() === 'true' || val.toLowerCase() === 'yes'; } /** * Build and validate the config object. * Called once at startup; throws on invalid config. */ function buildConfig() { const config = { // Mattermost API mm: { token: getEnv('MM_BOT_TOKEN', null, true), baseUrl: getEnv('MM_BASE_URL', 'https://slack.solio.tech'), maxSockets: getEnvInt('MM_MAX_SOCKETS', 4), botUserId: getEnv('MM_BOT_USER_ID', null), }, // Transcript directory (OpenClaw agents) transcriptDir: getEnv('TRANSCRIPT_DIR', '/home/node/.openclaw/agents'), // Timing throttleMs: getEnvInt('THROTTLE_MS', 500), idleTimeoutS: getEnvInt('IDLE_TIMEOUT_S', 60), sessionPollMs: getEnvInt('SESSION_POLL_MS', 2000), // Limits maxActiveSessions: getEnvInt('MAX_ACTIVE_SESSIONS', 20), maxMessageChars: getEnvInt('MAX_MESSAGE_CHARS', 15000), maxStatusLines: getEnvInt('MAX_STATUS_LINES', 20), maxRetries: getEnvInt('MAX_RETRIES', 3), // Circuit breaker circuitBreakerThreshold: getEnvInt('CIRCUIT_BREAKER_THRESHOLD', 5), circuitBreakerCooldownS: getEnvInt('CIRCUIT_BREAKER_COOLDOWN_S', 30), // Health check healthPort: getEnvInt('HEALTH_PORT', 9090), // Logging logLevel: getEnv('LOG_LEVEL', 'info'), // PID file pidFile: getEnv('PID_FILE', '/tmp/status-watcher.pid'), // Offset persistence offsetFile: getEnv('OFFSET_FILE', '/tmp/status-watcher-offsets.json'), // Optional external tool labels override toolLabelsFile: getEnv('TOOL_LABELS_FILE', null), // Fallback channel for non-MM sessions (null = skip) defaultChannel: getEnv('DEFAULT_CHANNEL', null), // Feature flags enableFsWatch: getEnvBool('ENABLE_FS_WATCH', true), // Mattermost plugin integration (optional) // When configured, updates are sent to the plugin instead of using PUT on posts plugin: { url: getEnv('PLUGIN_URL', null), // e.g. https://slack.solio.tech/plugins/com.openclaw.livestatus secret: getEnv('PLUGIN_SECRET', null), enabled: getEnvBool('PLUGIN_ENABLED', true), detectIntervalMs: getEnvInt('PLUGIN_DETECT_INTERVAL_MS', 60000), }, }; // Validate MM base URL try { new URL(config.mm.baseUrl); } catch (_e) { throw new Error(`MM_BASE_URL is not a valid URL: ${config.mm.baseUrl}`); } return config; } // Singleton — built once, exported let _config = null; function getConfig() { if (!_config) { _config = buildConfig(); } return _config; } // Allow resetting config in tests function resetConfig() { _config = null; } module.exports = { getConfig, resetConfig, buildConfig };