feat: Phase 0+1 — repo sync, pino, lint fixes, core components
Phase 0: - Synced latest live-status.js from workspace (9928 bytes) - Fixed 43 lint issues: empty catch blocks, console statements - Added pino dependency - Created src/tool-labels.json with all known tool mappings - make check passes Phase 1 (Core Components): - src/config.js: env-var config with validation, throws on missing required vars - src/logger.js: pino singleton with child loggers, level validation - src/circuit-breaker.js: CLOSED/OPEN/HALF_OPEN state machine with callbacks - src/tool-labels.js: exact/prefix/regex tool->label resolver with external override - src/status-box.js: Mattermost post manager (keepAlive, throttle, retry, circuit breaker) - src/status-formatter.js: pure SessionState->text formatter (nested, compact) - src/health.js: HTTP health endpoint + metrics - src/status-watcher.js: JSONL file watcher (inotify, compaction detection, idle detection) Tests: - test/unit/config.test.js: 7 tests - test/unit/circuit-breaker.test.js: 12 tests - test/unit/logger.test.js: 5 tests - test/unit/status-formatter.test.js: 20 tests - test/unit/tool-labels.test.js: 15 tests All 59 unit tests pass. make check clean.
This commit is contained in:
112
src/config.js
Normal file
112
src/config.js
Normal file
@@ -0,0 +1,112 @@
|
||||
'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),
|
||||
},
|
||||
|
||||
// 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),
|
||||
};
|
||||
|
||||
// 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 };
|
||||
Reference in New Issue
Block a user