/** * Simple structured logger for autoresearch extension. * Logs to stderr with timestamp and context information. */ export type LogLevel = "debug" | "info" | "warn" | "error"; export interface LogEntry { timestamp: string; level: LogLevel; message: string; context?: unknown; } // Minimum log level (can be overridden via AUTORESEARCH_LOG_LEVEL env var) const LOG_LEVELS: Record = { debug: 0, info: 1, warn: 2, error: 3, }; function getMinLevel(): LogLevel { const env = process.env.AUTORESEARCH_LOG_LEVEL?.toLowerCase(); if (env === "debug" || env === "info" || env === "warn" || env === "error") { return env; } return "warn"; } function shouldLog(level: LogLevel): boolean { const minLevel = getMinLevel(); return LOG_LEVELS[level] >= LOG_LEVELS[minLevel]; } function formatEntry(entry: LogEntry): string { const { timestamp, level, message, context } = entry; const prefix = `[${timestamp}] [${level.toUpperCase()}]`; if (context && Object.keys(context).length > 0) { return `${prefix} ${message} ${JSON.stringify(context)}`; } return `${prefix} ${message}`; } function sanitizeString(value: string): string { return value .replace(/[A-Za-z]:\\[^\s"]+/g, "[path]") .replace(/\\\\[^\\\s"]+(?:\\[^\s"]+)*/g, "[path]") .replace(/\/[^\s"]+/g, "[path]"); } function sanitizeError(error: unknown): string { if (error instanceof Error) { return sanitizeString(error.message); } return sanitizeString(String(error)); } function sanitizeValue(value: unknown): unknown { if (typeof value === "string") return sanitizeString(value); if (value instanceof Error) return sanitizeError(value); if (Array.isArray(value)) return value.map((item) => sanitizeValue(item)); if (value && typeof value === "object") { return Object.fromEntries( Object.entries(value as Record).map(([key, nested]) => [ key, sanitizeValue(nested), ]) ); } return value; } function sanitizeContext(context: Record): Record { return sanitizeValue(context) as Record; } function log(level: LogLevel, message: string, context?: Record): void { if (!shouldLog(level)) return; const entry: LogEntry = { timestamp: new Date().toISOString(), level, message, context: context ? sanitizeContext(context) : undefined, }; // Log to stderr to avoid interfering with stdout console.error(formatEntry(entry)); } export const logger = { debug: (message: string, context?: Record) => log("debug", message, context), info: (message: string, context?: Record) => log("info", message, context), warn: (message: string, context?: Record) => log("warn", message, context), error: (message: string, context?: Record) => log("error", message, context), // Convenience method for catching and logging errors catch: (operation: string, error: unknown, context?: Record): void => { logger.error(`${operation} failed`, { error: sanitizeError(error), ...context, }); }, };