UNPKG

@iflow-mcp/ejmockler-brutalist

Version:

Deploy Claude, Codex & Gemini CLI agents to demolish your work before users do. Real file analysis. Brutal honesty. Now with conversation continuation & intelligent pagination.

198 lines 7.89 kB
import { appendFileSync, statSync, renameSync, unlinkSync, mkdirSync, existsSync, openSync, closeSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; /** * Production logger with optional size-rotated file output. * * Stderr output (human-readable) is always active — this is the MCP-safe * channel since stdout is reserved for protocol messages. * * File output (NDJSON) is opt-in via BRUTALIST_LOG_FILE=true. * Files are written to ~/.brutalist-mcp/logs/ by default and capped * via size-based ring rotation so disk usage never exceeds * MAX_SIZE × (MAX_FILES + 1). * * Uses synchronous file writes (appendFileSync) so every log line * survives crashes — essential for post-mortem debugging. * * Environment variables: * BRUTALIST_LOG_FILE – "true" to enable file logging * BRUTALIST_LOG_DIR – override log directory * BRUTALIST_LOG_MAX_SIZE – max MB per file (default 5) * BRUTALIST_LOG_MAX_FILES – rotated files to keep (default 3) * BRUTALIST_LOG_LEVEL – minimum file log level (default "info") */ const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 }; const LOG_FILENAME = 'brutalist.log'; export class Logger { static instance; debugMode; // File logging state fileEnabled = false; logDir = ''; logFilePath = ''; maxFileSize = 5 * 1024 * 1024; // 5 MB maxFiles = 3; fileLogLevel = LOG_LEVELS.info; currentFileSize = 0; rotating = false; pid = process.pid; constructor() { this.debugMode = process.env.DEBUG === 'true' || process.env.NODE_ENV === 'development'; this.initFileLogging(); } static getInstance() { if (!Logger.instance) { Logger.instance = new Logger(); } return Logger.instance; } // --------------------------------------------------------------------------- // Public API — unchanged signatures, all call sites continue to work // --------------------------------------------------------------------------- info(message, data) { console.error(`[BRUTALIST MCP] INFO: ${message}`, data ? JSON.stringify(data) : ''); this.writeToFile('info', message, data); } warn(message, data) { console.error(`[BRUTALIST MCP] WARN: ${message}`, data ? JSON.stringify(data) : ''); this.writeToFile('warn', message, data); } error(message, error) { console.error(`[BRUTALIST MCP] ERROR: ${message}`, error instanceof Error ? error.message : error); if (this.debugMode && error instanceof Error && error.stack) { console.error(error.stack); } // File output always gets the full error shape for post-mortem debugging const fileData = error instanceof Error ? { message: error.message, stack: error.stack, name: error.name } : error; this.writeToFile('error', message, fileData); } debug(message, data) { if (this.debugMode) { console.error(`[BRUTALIST MCP] DEBUG: ${message}`, data ? JSON.stringify(data) : ''); } // File always receives debug lines if the configured level allows it, // regardless of the stderr debug gate this.writeToFile('debug', message, data); } /** No-op kept for API compatibility. Writes are synchronous, nothing to flush. */ shutdown() { // All writes use appendFileSync — every line is already on disk. } // --------------------------------------------------------------------------- // File logging internals // --------------------------------------------------------------------------- initFileLogging() { const enabled = process.env.BRUTALIST_LOG_FILE === 'true'; const isSubprocess = process.env.BRUTALIST_SUBPROCESS === '1'; if (!enabled || isSubprocess) return; try { this.logDir = process.env.BRUTALIST_LOG_DIR || join(homedir(), '.brutalist-mcp', 'logs'); const maxSizeMB = Number(process.env.BRUTALIST_LOG_MAX_SIZE) || 5; this.maxFileSize = maxSizeMB * 1024 * 1024; this.maxFiles = Number(process.env.BRUTALIST_LOG_MAX_FILES) || 3; const levelStr = (process.env.BRUTALIST_LOG_LEVEL || 'info').toLowerCase(); this.fileLogLevel = LOG_LEVELS[levelStr] ?? LOG_LEVELS.info; // Ensure directory exists mkdirSync(this.logDir, { recursive: true }); this.logFilePath = join(this.logDir, LOG_FILENAME); // Measure existing file so we rotate correctly if (existsSync(this.logFilePath)) { try { this.currentFileSize = statSync(this.logFilePath).size; } catch { this.currentFileSize = 0; } } else { // Touch the file so appendFileSync doesn't fail on first write const fd = openSync(this.logFilePath, 'a'); closeSync(fd); } this.fileEnabled = true; } catch (err) { // Never let logging prevent the server from starting console.error(`[BRUTALIST MCP] WARN: File logging init failed: ${err instanceof Error ? err.message : err}`); this.fileEnabled = false; } } writeToFile(level, message, data) { if (!this.fileEnabled) return; if (LOG_LEVELS[level] < this.fileLogLevel) return; try { const entry = { ts: new Date().toISOString(), level, msg: message, ...(data !== undefined && { data }), pid: this.pid }; const line = JSON.stringify(entry) + '\n'; const lineBytes = Buffer.byteLength(line); // Rotate before writing if this line would exceed the cap if (this.currentFileSize + lineBytes > this.maxFileSize) { this.rotate(); } appendFileSync(this.logFilePath, line); this.currentFileSize += lineBytes; } catch { this.disableFileLogging('write failure'); } } /** * Size-based ring rotation. * * brutalist.log → brutalist.1.log * brutalist.1.log → brutalist.2.log * … * brutalist.{max}.log → deleted */ rotate() { if (this.rotating) return; this.rotating = true; try { // Shift existing rotated files for (let i = this.maxFiles - 1; i >= 1; i--) { const src = join(this.logDir, `brutalist.${i}.log`); const dst = join(this.logDir, `brutalist.${i + 1}.log`); if (existsSync(src)) { renameSync(src, dst); } } // Drop the oldest file if it exists const oldest = join(this.logDir, `brutalist.${this.maxFiles}.log`); if (existsSync(oldest)) { unlinkSync(oldest); } // Current → .1 if (existsSync(this.logFilePath)) { renameSync(this.logFilePath, join(this.logDir, 'brutalist.1.log')); } this.currentFileSize = 0; } catch (err) { this.disableFileLogging(`rotation failed: ${err instanceof Error ? err.message : err}`); } finally { this.rotating = false; } } disableFileLogging(reason) { if (!this.fileEnabled) return; this.fileEnabled = false; console.error(`[BRUTALIST MCP] WARN: File logging disabled (${reason})`); } } export const logger = Logger.getInstance(); //# sourceMappingURL=logger.js.map