import type { Logger, LogLevel } from '@agentuity/core'; import { format, inspect } from 'node:util'; // Save original console before it might be patched (must be at module level) const originalConsole = { log: console.log.bind(console), error: console.error.bind(console), warn: console.warn.bind(console), debug: console.debug.bind(console), }; const LOG_LEVELS: Record = { trace: 0, debug: 1, info: 2, warn: 3, error: 4, }; const BOLD = '\x1b[1m'; const RESET = '\x1b[0m'; // Helper to convert hex color to ANSI 24-bit color code function hexToAnsi(hex: string): string { const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); return `\x1b[38;2;${r};${g};${b}m`; } function shouldUseColors(): boolean { // FORCE_COLOR overrides all checks (used when stdout is piped but we still want colors) if (process.env.FORCE_COLOR === '1') { return true; } // Check for NO_COLOR environment variable (any non-empty value disables colors) if (process.env.NO_COLOR) { return false; } // Check for TERM=dumb if (process.env.TERM === 'dumb') { return false; } // Check if stdout is a TTY if (!process.stdout || typeof process.stdout.isTTY === 'undefined') { return false; } if (!process.stdout.isTTY) { return false; } return true; } const USE_COLORS = shouldUseColors(); interface LogColors { level: string; message: string; timestamp: string; } export type ColorScheme = 'light' | 'dark'; function getLogColors(scheme: ColorScheme): Record { if (scheme === 'light') { // Darker, high-contrast colors for light backgrounds return { trace: { level: hexToAnsi('#008B8B') + BOLD, // Dark cyan message: hexToAnsi('#4B4B4B'), // Dark gray timestamp: hexToAnsi('#808080'), // Gray }, debug: { level: hexToAnsi('#0000CD') + BOLD, // Medium blue message: hexToAnsi('#006400'), // Dark green timestamp: hexToAnsi('#808080'), }, info: { level: hexToAnsi('#FF8C00') + BOLD, // Dark orange message: hexToAnsi('#0066CC') + BOLD, // Strong blue timestamp: hexToAnsi('#808080'), }, warn: { level: hexToAnsi('#9400D3') + BOLD, // Dark violet message: hexToAnsi('#8B008B'), // Dark magenta timestamp: hexToAnsi('#808080'), }, error: { level: hexToAnsi('#DC143C') + BOLD, // Crimson message: hexToAnsi('#8B0000') + BOLD, // Dark red timestamp: hexToAnsi('#808080'), }, }; } // Dark mode colors (brighter for dark backgrounds) return { trace: { level: hexToAnsi('#00FFFF') + BOLD, // Cyan message: hexToAnsi('#A0A0A0'), // Light gray timestamp: hexToAnsi('#666666'), }, debug: { level: hexToAnsi('#5C9CFF') + BOLD, // Blue message: hexToAnsi('#90EE90'), // Light green timestamp: hexToAnsi('#666666'), }, info: { level: hexToAnsi('#FFD700') + BOLD, // Gold/Yellow message: hexToAnsi('#FFFFFF') + BOLD, // White timestamp: hexToAnsi('#666666'), }, warn: { level: hexToAnsi('#FF00FF') + BOLD, // Magenta message: hexToAnsi('#FF00FF'), // Magenta timestamp: hexToAnsi('#666666'), }, error: { level: hexToAnsi('#FF4444') + BOLD, // Red message: hexToAnsi('#FF4444'), // Red timestamp: hexToAnsi('#666666'), }, }; } /** * Console logger implementation */ export class ConsoleLogger implements Logger { public level: LogLevel; private showTimestamp: boolean; private colorScheme: ColorScheme; private colors: Record; private showPrefix = true; private context: Record; constructor( level: LogLevel = 'info', showTimestamp: boolean = false, colorScheme: ColorScheme = 'dark', context: Record = {} ) { this.level = level; this.showTimestamp = showTimestamp; this.colorScheme = colorScheme; this.colors = getLogColors(this.colorScheme); this.context = context; } setLevel(level: LogLevel): void { this.level = level; } setTimestamp(enabled: boolean): void { this.showTimestamp = enabled; } setColorScheme(scheme: ColorScheme): void { this.colorScheme = scheme; this.colors = getLogColors(this.colorScheme); } setShowPrefix(show: boolean): void { this.showPrefix = show; } private shouldLog(level: LogLevel): boolean { return LOG_LEVELS[level] >= LOG_LEVELS[this.level]; } private formatMessage(message: unknown, args: unknown[]): string { try { // eslint-disable-next-line @typescript-eslint/no-explicit-any const base = format(message as any, ...(args as any[])); if (!this.context || Object.keys(this.context).length === 0) { return base; } const ctx = Object.entries(this.context) .map( ([k, v]) => `${k}=${typeof v === 'object' ? inspect(v, { depth: 2, maxArrayLength: 50, colors: false }) : String(v)}` ) .join(' '); const result = `${base} ${ctx}`; const MAX_LENGTH = 10000; if (result.length > MAX_LENGTH) { return `${result.slice(0, MAX_LENGTH)} …(+${result.length - MAX_LENGTH} chars truncated)`; } return result; } catch { const base = [String(message), ...args.map((a) => String(a))].join(' '); return this.context && Object.keys(this.context).length > 0 ? `${base} ${JSON.stringify(this.context)}` : base; } } private log(level: LogLevel, message: unknown, ...args: unknown[]): void { if (!this.shouldLog(level)) { return; } const colors = this.colors[level]; const levelText = `[${level.toUpperCase()}]`; const formattedMessage = this.formatMessage(message, args); let output = ''; if (USE_COLORS) { if (this.showPrefix) { if (this.showTimestamp) { const timestamp = new Date().toISOString(); output = `${colors.timestamp}[${timestamp}]${RESET} ${colors.level}${levelText}${RESET} ${colors.message}${formattedMessage}${RESET}`; } else { output = `${colors.level}${levelText}${RESET} ${colors.message}${formattedMessage}${RESET}`; } } else { // No prefix - just the message with color output = `${colors.message}${formattedMessage}${RESET}`; } } else { // No colors - plain text output if (this.showPrefix) { if (this.showTimestamp) { const timestamp = new Date().toISOString(); output = `[${timestamp}] ${levelText} ${formattedMessage}`; } else { output = `${levelText} ${formattedMessage}`; } } else { // No prefix, no colors - just message output = formattedMessage; } } // Use original console to avoid recursive logging when console is patched if (level === 'error') { originalConsole.error(output); } else if (level === 'warn') { originalConsole.warn(output); } else { originalConsole.log(output); } } trace(message: unknown, ...args: unknown[]): void { this.log('trace', message, ...args); } debug(message: unknown, ...args: unknown[]): void { this.log('debug', message, ...args); } info(message: unknown, ...args: unknown[]): void { this.log('info', message, ...args); } warn(message: unknown, ...args: unknown[]): void { this.log('warn', message, ...args); } error(message: unknown, ...args: unknown[]): void { this.log('error', message, ...args); } fatal(message: unknown, ...args: unknown[]): never { this.log('error', message, ...args); process.exit(1); } child(opts: Record): Logger { return new ConsoleLogger(this.level, this.showTimestamp, this.colorScheme, { ...this.context, ...opts, }); } } /** * Create a new console logger instance * * @param level - The minimum log level to display * @param showTimestamp - Whether to show timestamps in log messages * @param colorScheme - The color scheme to use ('light' or 'dark') * @param context - Initial context for the logger * @returns A new ConsoleLogger instance */ export function createLogger( level: LogLevel = 'info', showTimestamp: boolean = false, colorScheme: ColorScheme = 'dark', context: Record = {} ): Logger { return new ConsoleLogger(level, showTimestamp, colorScheme, context); }