diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 402b331ed..318ed2956 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -47,16 +47,6 @@ import { mcpCommand } from '../commands/mcp.js'; import { isWorkspaceTrusted } from './trustedFolders.js'; import { buildWebSearchConfig } from './webSearch.js'; -// Simple console logger for now - replace with actual logger if available -const logger = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - debug: (...args: any[]) => console.debug('[DEBUG]', ...args), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - warn: (...args: any[]) => console.warn('[WARN]', ...args), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - error: (...args: any[]) => console.error('[ERROR]', ...args), -}; - const VALID_APPROVAL_MODE_VALUES = [ 'plan', 'default', @@ -626,7 +616,6 @@ export async function parseArguments(settings: Settings): Promise { export async function loadHierarchicalGeminiMemory( currentWorkingDirectory: string, includeDirectoriesToReadGemini: readonly string[] = [], - debugMode: boolean, fileService: FileDiscoveryService, extensionContextFilePaths: string[] = [], folderTrust: boolean, @@ -641,17 +630,10 @@ export async function loadHierarchicalGeminiMemory( // function to signal that it should skip the workspace search. const effectiveCwd = isHomeDirectory ? '' : currentWorkingDirectory; - if (debugMode) { - logger.debug( - `CLI: Delegating hierarchical memory load to server for CWD: ${currentWorkingDirectory} (memoryImportFormat: ${memoryImportFormat})`, - ); - } - // Directly call the server function with the corrected path. return loadServerHierarchicalMemory( effectiveCwd, includeDirectoriesToReadGemini, - debugMode, fileService, extensionContextFilePaths, folderTrust, @@ -698,11 +680,7 @@ export async function loadCliConfig( 'output-language.md', ); if (fs.existsSync(outputLanguageFilePath)) { - if (debugMode) { - logger.debug( - `Found output-language.md, adding to context files: ${outputLanguageFilePath}`, - ); - } + // output-language.md found - will be added to context files } else { outputLanguageFilePath = undefined; } @@ -749,7 +727,7 @@ export async function loadCliConfig( approvalMode !== ApprovalMode.DEFAULT && approvalMode !== ApprovalMode.PLAN ) { - logger.warn( + console.warn( `Approval mode overridden to "default" because the current folder is not trusted.`, ); approvalMode = ApprovalMode.DEFAULT; diff --git a/packages/cli/src/nonInteractiveCli.ts b/packages/cli/src/nonInteractiveCli.ts index 4088c9283..40197fa71 100644 --- a/packages/cli/src/nonInteractiveCli.ts +++ b/packages/cli/src/nonInteractiveCli.ts @@ -467,7 +467,7 @@ export async function runNonInteractive( process.removeListener('SIGINT', shutdownHandler); process.removeListener('SIGTERM', shutdownHandler); if (isTelemetrySdkInitialized()) { - await shutdownTelemetry(config); + await shutdownTelemetry(); } } }); diff --git a/packages/cli/src/services/CommandService.test.ts b/packages/cli/src/services/CommandService.test.ts index e2d5b9f58..51f962753 100644 --- a/packages/cli/src/services/CommandService.test.ts +++ b/packages/cli/src/services/CommandService.test.ts @@ -139,10 +139,6 @@ describe('CommandService', () => { const commands = service.getCommands(); expect(commands).toHaveLength(1); expect(commands).toEqual([mockCommandA]); - expect(console.debug).toHaveBeenCalledWith( - 'A command loader failed:', - error, - ); }); it('getCommands should return a readonly array that cannot be mutated', async () => { diff --git a/packages/cli/src/services/CommandService.ts b/packages/cli/src/services/CommandService.ts index 5f1e09d50..41086dac1 100644 --- a/packages/cli/src/services/CommandService.ts +++ b/packages/cli/src/services/CommandService.ts @@ -6,6 +6,9 @@ import type { SlashCommand } from '../ui/commands/types.js'; import type { ICommandLoader } from './types.js'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; + +const debugLogger = createDebugLogger('CLI_COMMANDS'); /** * Orchestrates the discovery and loading of all slash commands for the CLI. @@ -57,7 +60,7 @@ export class CommandService { if (result.status === 'fulfilled') { allCommands.push(...result.value); } else { - console.debug('A command loader failed:', result.reason); + debugLogger.debug('A command loader failed:', result.reason); } } diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index c1bd7b80c..c5717192d 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -584,10 +584,13 @@ export const AppContainer = (props: AppContainerProps) => { [visionSwitchResolver], ); - // onDebugMessage should log to console, not update footer debugMessage - const onDebugMessage = useCallback((message: string) => { - console.debug(message); - }, []); + // onDebugMessage should log to debug logfile, not update footer debugMessage + const onDebugMessage = useCallback( + (message: string) => { + config.getDebugLogger().debug(message); + }, + [config], + ); const performMemoryRefresh = useCallback(async () => { historyManager.addItem( @@ -603,7 +606,6 @@ export const AppContainer = (props: AppContainerProps) => { settings.merged.context?.loadMemoryFromIncludeDirectories ? config.getWorkspaceContext().getDirectories() : [], - config.getDebugMode(), config.getFileService(), config.getExtensionContextFilePaths(), config.isTrustedFolder(), diff --git a/packages/cli/src/ui/commands/copyCommand.ts b/packages/cli/src/ui/commands/copyCommand.ts index 3b79dd489..421b0323b 100644 --- a/packages/cli/src/ui/commands/copyCommand.ts +++ b/packages/cli/src/ui/commands/copyCommand.ts @@ -48,7 +48,7 @@ export const copyCommand: SlashCommand = { }; } catch (error) { const message = error instanceof Error ? error.message : String(error); - console.debug(message); + context.services.config?.getDebugLogger().debug(message); return { type: 'message', diff --git a/packages/cli/src/ui/commands/directoryCommand.tsx b/packages/cli/src/ui/commands/directoryCommand.tsx index f5c91b46b..1fcd83dd3 100644 --- a/packages/cli/src/ui/commands/directoryCommand.tsx +++ b/packages/cli/src/ui/commands/directoryCommand.tsx @@ -113,7 +113,6 @@ export const directoryCommand: SlashCommand = { ...config.getWorkspaceContext().getDirectories(), ...pathsToAdd, ], - config.getDebugMode(), config.getFileService(), config.getExtensionContextFilePaths(), config.getFolderTrust(), diff --git a/packages/cli/src/ui/commands/memoryCommand.ts b/packages/cli/src/ui/commands/memoryCommand.ts index d9d2950b1..507444e5a 100644 --- a/packages/cli/src/ui/commands/memoryCommand.ts +++ b/packages/cli/src/ui/commands/memoryCommand.ts @@ -309,7 +309,6 @@ export const memoryCommand: SlashCommand = { config.shouldLoadMemoryFromIncludeDirectories() ? config.getWorkspaceContext().getDirectories() : [], - config.getDebugMode(), config.getFileService(), config.getExtensionContextFilePaths(), config.getFolderTrust(), diff --git a/packages/cli/src/ui/commands/setupGithubCommand.test.ts b/packages/cli/src/ui/commands/setupGithubCommand.test.ts index 6f0faae37..16a938bac 100644 --- a/packages/cli/src/ui/commands/setupGithubCommand.test.ts +++ b/packages/cli/src/ui/commands/setupGithubCommand.test.ts @@ -219,20 +219,13 @@ describe('updateGitignore', () => { }); it('handles permission errors gracefully', async () => { - const consoleSpy = vi.spyOn(console, 'debug').mockImplementation(() => {}); - const fsModule = await import('node:fs'); const writeFileSpy = vi .spyOn(fsModule.promises, 'writeFile') .mockRejectedValue(new Error('Permission denied')); await expect(updateGitignore(scratchDir)).resolves.toBeUndefined(); - expect(consoleSpy).toHaveBeenCalledWith( - 'Failed to update .gitignore:', - expect.any(Error), - ); writeFileSpy.mockRestore(); - consoleSpy.mockRestore(); }); }); diff --git a/packages/cli/src/ui/commands/setupGithubCommand.ts b/packages/cli/src/ui/commands/setupGithubCommand.ts index b12268edd..baba59b6c 100644 --- a/packages/cli/src/ui/commands/setupGithubCommand.ts +++ b/packages/cli/src/ui/commands/setupGithubCommand.ts @@ -21,6 +21,9 @@ import type { SlashCommand, SlashCommandActionReturn } from './types.js'; import { CommandKind } from './types.js'; import { getUrlOpenCommand } from '../../ui/utils/commandUtils.js'; import { t } from '../../i18n/index.js'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; + +const debugLogger = createDebugLogger('SETUP_GITHUB'); export const GITHUB_WORKFLOW_PATHS = [ 'qwen-dispatch/qwen-dispatch.yml', @@ -85,7 +88,7 @@ export async function updateGitignore(gitRepoRoot: string): Promise { } } } catch (error) { - console.debug('Failed to update .gitignore:', error); + debugLogger.debug('Failed to update .gitignore:', error); // Continue without failing the whole command } } @@ -112,7 +115,7 @@ export const setupGithubCommand: SlashCommand = { try { gitRepoRoot = getGitRepoRoot(); } catch (_error) { - console.debug(`Failed to get git repo root:`, _error); + debugLogger.debug(`Failed to get git repo root:`, _error); throw new Error( 'Unable to determine the GitHub repository. /setup-github must be run from a git repository.', ); @@ -128,7 +131,7 @@ export const setupGithubCommand: SlashCommand = { try { await fs.promises.mkdir(githubWorkflowsDir, { recursive: true }); } catch (_error) { - console.debug( + debugLogger.debug( `Failed to create ${githubWorkflowsDir} directory:`, _error, ); diff --git a/packages/cli/src/ui/hooks/useWelcomeBack.ts b/packages/cli/src/ui/hooks/useWelcomeBack.ts index bbc164016..36ce931be 100644 --- a/packages/cli/src/ui/hooks/useWelcomeBack.ts +++ b/packages/cli/src/ui/hooks/useWelcomeBack.ts @@ -57,9 +57,9 @@ export function useWelcomeBack( } } catch (error) { // Silently ignore errors - welcome back is not critical - console.debug('Welcome back check failed:', error); + config.getDebugLogger().debug('Welcome back check failed:', error); } - }, [settings.ui?.enableWelcomeBack]); + }, [config, settings.ui?.enableWelcomeBack]); // Handle welcome back dialog selection const handleWelcomeBackSelection = useCallback( diff --git a/packages/cli/src/ui/utils/terminalSetup.ts b/packages/cli/src/ui/utils/terminalSetup.ts index d7a7f41e9..04d5aa9c4 100644 --- a/packages/cli/src/ui/utils/terminalSetup.ts +++ b/packages/cli/src/ui/utils/terminalSetup.ts @@ -31,6 +31,9 @@ import { promisify } from 'node:util'; import { isKittyProtocolEnabled } from './kittyProtocolDetector.js'; import { VSCODE_SHIFT_ENTER_SEQUENCE } from './platformConstants.js'; import { t } from '../../i18n/index.js'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; + +const debugLogger = createDebugLogger('TERMINAL_SETUP'); const execAsync = promisify(exec); @@ -96,7 +99,7 @@ async function detectTerminal(): Promise { return 'trae'; } catch (error) { // Continue detection even if process check fails - console.debug('Parent process detection failed:', error); + debugLogger.debug('Parent process detection failed:', error); } } diff --git a/packages/cli/src/utils/gitUtils.ts b/packages/cli/src/utils/gitUtils.ts index fcef4bf3e..58b0bfde1 100644 --- a/packages/cli/src/utils/gitUtils.ts +++ b/packages/cli/src/utils/gitUtils.ts @@ -6,6 +6,9 @@ import { execSync } from 'node:child_process'; import { ProxyAgent } from 'undici'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; + +const debugLogger = createDebugLogger('GIT'); /** * Checks if a directory is within a git repository hosted on GitHub. @@ -24,7 +27,7 @@ export const isGitHubRepository = (): boolean => { return pattern.test(remotes); } catch (_error) { // If any filesystem error occurs, assume not a git repo - console.debug(`Failed to get git remote:`, _error); + debugLogger.debug(`Failed to get git remote:`, _error); return false; } }; @@ -83,7 +86,7 @@ export const getLatestGitHubRelease = async ( } return releaseTag; } catch (_error) { - console.debug( + debugLogger.debug( `Failed to determine latest qwen-code-action release:`, _error, ); diff --git a/packages/cli/src/utils/nonInteractiveHelpers.test.ts b/packages/cli/src/utils/nonInteractiveHelpers.test.ts index 1f4e4f618..f75f6095f 100644 --- a/packages/cli/src/utils/nonInteractiveHelpers.test.ts +++ b/packages/cli/src/utils/nonInteractiveHelpers.test.ts @@ -301,11 +301,8 @@ describe('extractUsageFromGeminiClient', () => { throw new Error('Test error'); }), }; - const consoleSpy = vi.spyOn(console, 'debug').mockImplementation(() => {}); const result = extractUsageFromGeminiClient(client); expect(result).toBeUndefined(); - expect(consoleSpy).toHaveBeenCalled(); - consoleSpy.mockRestore(); }); it('should skip responses without usageMetadata', () => { diff --git a/packages/cli/src/utils/nonInteractiveHelpers.ts b/packages/cli/src/utils/nonInteractiveHelpers.ts index fca109cbe..fbffd5dee 100644 --- a/packages/cli/src/utils/nonInteractiveHelpers.ts +++ b/packages/cli/src/utils/nonInteractiveHelpers.ts @@ -16,6 +16,7 @@ import type { import { OutputFormat, ToolErrorType, + createDebugLogger, getMCPServerStatus, } from '@qwen-code/qwen-code-core'; import type { Part, PartListUnion } from '@google/genai'; @@ -29,6 +30,8 @@ import type { JsonOutputAdapterInterface } from '../nonInteractive/io/BaseJsonOu import { computeSessionStats } from '../ui/utils/computeStats.js'; import { getAvailableCommands } from '../nonInteractiveCliCommands.js'; +const debugLogger = createDebugLogger('NON_INTERACTIVE'); + /** * Normalizes various part list formats into a consistent Part[] array. * @@ -144,7 +147,7 @@ export function extractUsageFromGeminiClient( } } } catch (error) { - console.debug('Failed to extract usage metadata:', error); + debugLogger.debug('Failed to extract usage metadata:', error); } return undefined; diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 7dbb68138..ef4ef7fdb 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -107,7 +107,11 @@ import { } from '../services/sessionService.js'; import { randomUUID } from 'node:crypto'; import { loadServerHierarchicalMemory } from '../utils/memoryDiscovery.js'; -import { createDebugLogger, type DebugLogger } from '../utils/debugLogger.js'; +import { + createDebugLogger, + setDebugLogSession, + type DebugLogger, +} from '../utils/debugLogger.js'; import { ModelsConfig, @@ -513,7 +517,8 @@ export class Config { constructor(params: ConfigParameters) { this.sessionId = params.sessionId ?? randomUUID(); this.sessionData = params.sessionData; - this.debugLogger = createDebugLogger(this); + setDebugLogSession(this); + this.debugLogger = createDebugLogger(); this.embeddingModel = params.embeddingModel ?? DEFAULT_QWEN_EMBEDDING_MODEL; this.fileSystemService = new StandardFileSystemService(); this.sandbox = params.sandbox; @@ -713,7 +718,6 @@ export class Config { this.shouldLoadMemoryFromIncludeDirectories() ? this.getWorkspaceContext().getDirectories() : [], - this.getDebugMode(), this.getFileService(), this.getExtensionContextFilePaths(), this.getFolderTrust(), @@ -831,7 +835,8 @@ export class Config { ): string { this.sessionId = sessionId ?? randomUUID(); this.sessionData = sessionData; - this.debugLogger = createDebugLogger(this); + setDebugLogSession(this); + this.debugLogger = createDebugLogger(); this.chatRecordingService = this.chatRecordingEnabled ? new ChatRecordingService(this) : undefined; @@ -1654,7 +1659,7 @@ export class Config { } await registry.discoverAllTools(); - console.debug('ToolRegistry created', registry.getAllToolNames()); + this.debugLogger.debug('ToolRegistry created', registry.getAllToolNames()); return registry; } } diff --git a/packages/core/src/core/logger.test.ts b/packages/core/src/core/logger.test.ts index 0b506b4c4..07b8a9e8a 100644 --- a/packages/core/src/core/logger.test.ts +++ b/packages/core/src/core/logger.test.ts @@ -72,6 +72,20 @@ vi.mock('../utils/session.js', () => ({ sessionId: 'test-session-id', })); +vi.mock('../utils/debugLogger.js', async (importOriginal) => { + const original = + await importOriginal(); + return { + ...original, + createDebugLogger: () => ({ + debug: (...args: unknown[]) => console.debug(...args), + info: (...args: unknown[]) => console.info(...args), + warn: (...args: unknown[]) => console.warn(...args), + error: (...args: unknown[]) => console.error(...args), + }), + }; +}); + describe('Logger', () => { let logger: Logger; const testSessionId = 'test-session-id'; diff --git a/packages/core/src/core/logger.ts b/packages/core/src/core/logger.ts index e7be90415..ad414b03e 100644 --- a/packages/core/src/core/logger.ts +++ b/packages/core/src/core/logger.ts @@ -8,6 +8,7 @@ import path from 'node:path'; import { promises as fs } from 'node:fs'; import type { Content } from '@google/genai'; import type { Storage } from '../config/storage.js'; +import { createDebugLogger, type DebugLogger } from '../utils/debugLogger.js'; const LOG_FILE_NAME = 'logs.json'; @@ -74,12 +75,14 @@ export class Logger { private messageId = 0; // Instance-specific counter for the next messageId private initialized = false; private logs: LogEntry[] = []; // In-memory cache, ideally reflects the last known state of the file + private debugLogger: DebugLogger; constructor( sessionId: string, private readonly storage: Storage, ) { this.sessionId = sessionId; + this.debugLogger = createDebugLogger('LOGGER'); } private async _readLogFile(): Promise { @@ -90,7 +93,7 @@ export class Logger { const fileContent = await fs.readFile(this.logFilePath, 'utf-8'); const parsedLogs = JSON.parse(fileContent); if (!Array.isArray(parsedLogs)) { - console.debug( + this.debugLogger.debug( `Log file at ${this.logFilePath} is not a valid JSON array. Starting with empty logs.`, ); await this._backupCorruptedLogFile('malformed_array'); @@ -110,14 +113,14 @@ export class Logger { return []; } if (error instanceof SyntaxError) { - console.debug( + this.debugLogger.debug( `Invalid JSON in log file ${this.logFilePath}. Backing up and starting fresh.`, error, ); await this._backupCorruptedLogFile('invalid_json'); return []; } - console.debug( + this.debugLogger.debug( `Failed to read or parse log file ${this.logFilePath}:`, error, ); @@ -130,7 +133,7 @@ export class Logger { const backupPath = `${this.logFilePath}.${reason}.${Date.now()}.bak`; try { await fs.rename(this.logFilePath, backupPath); - console.debug(`Backed up corrupted log file to ${backupPath}`); + this.debugLogger.debug(`Backed up corrupted log file to ${backupPath}`); } catch (_backupError) { // If rename fails (e.g. file doesn't exist), no need to log an error here as the primary error (e.g. invalid JSON) is already handled. } @@ -165,7 +168,7 @@ export class Logger { : 0; this.initialized = true; } catch (err) { - console.error('Failed to initialize logger:', err); + this.debugLogger.error('Failed to initialize logger:', err); this.initialized = false; } } @@ -174,7 +177,9 @@ export class Logger { entryToAppend: LogEntry, ): Promise { if (!this.logFilePath) { - console.debug('Log file path not set. Cannot persist log entry.'); + this.debugLogger.debug( + 'Log file path not set. Cannot persist log entry.', + ); throw new Error('Log file path not set during update attempt.'); } @@ -182,7 +187,7 @@ export class Logger { try { currentLogsOnDisk = await this._readLogFile(); } catch (readError) { - console.debug( + this.debugLogger.debug( 'Critical error reading log file before append:', readError, ); @@ -213,7 +218,7 @@ export class Logger { ); if (entryExists) { - console.debug( + this.debugLogger.debug( `Duplicate log entry detected and skipped: session ${entryToAppend.sessionId}, messageId ${entryToAppend.messageId}`, ); this.logs = currentLogsOnDisk; // Ensure in-memory is synced with disk @@ -231,7 +236,7 @@ export class Logger { this.logs = currentLogsOnDisk; return entryToAppend; // Return the successfully appended entry } catch (error) { - console.debug('Error writing to log file:', error); + this.debugLogger.debug('Error writing to log file:', error); throw error; } } @@ -250,7 +255,7 @@ export class Logger { async logMessage(type: MessageSenderType, message: string): Promise { if (!this.initialized || this.sessionId === undefined) { - console.debug( + this.debugLogger.debug( 'Logger not initialized or session ID missing. Cannot log message.', ); return; @@ -322,7 +327,7 @@ export class Logger { async saveCheckpoint(conversation: Content[], tag: string): Promise { if (!this.initialized) { - console.error( + this.debugLogger.error( 'Logger not initialized or checkpoint file path not set. Cannot save a checkpoint.', ); return; @@ -332,13 +337,13 @@ export class Logger { try { await fs.writeFile(path, JSON.stringify(conversation, null, 2), 'utf-8'); } catch (error) { - console.error('Error writing to checkpoint file:', error); + this.debugLogger.error('Error writing to checkpoint file:', error); } } async loadCheckpoint(tag: string): Promise { if (!this.initialized) { - console.error( + this.debugLogger.error( 'Logger not initialized or checkpoint file path not set. Cannot load checkpoint.', ); return []; @@ -349,7 +354,7 @@ export class Logger { const fileContent = await fs.readFile(path, 'utf-8'); const parsedContent = JSON.parse(fileContent); if (!Array.isArray(parsedContent)) { - console.warn( + this.debugLogger.warn( `Checkpoint file at ${path} is not a valid JSON array. Returning empty checkpoint.`, ); return []; @@ -361,14 +366,17 @@ export class Logger { // This is okay, it just means the checkpoint doesn't exist in either format. return []; } - console.error(`Failed to read or parse checkpoint file ${path}:`, error); + this.debugLogger.error( + `Failed to read or parse checkpoint file ${path}:`, + error, + ); return []; } } async deleteCheckpoint(tag: string): Promise { if (!this.initialized || !this.qwenDir) { - console.error( + this.debugLogger.error( 'Logger not initialized or checkpoint file path not set. Cannot delete checkpoint.', ); return false; @@ -384,7 +392,10 @@ export class Logger { } catch (error) { const nodeError = error as NodeJS.ErrnoException; if (nodeError.code !== 'ENOENT') { - console.error(`Failed to delete checkpoint file ${newPath}:`, error); + this.debugLogger.error( + `Failed to delete checkpoint file ${newPath}:`, + error, + ); throw error; // Rethrow unexpected errors } // It's okay if it doesn't exist. @@ -399,7 +410,10 @@ export class Logger { } catch (error) { const nodeError = error as NodeJS.ErrnoException; if (nodeError.code !== 'ENOENT') { - console.error(`Failed to delete checkpoint file ${oldPath}:`, error); + this.debugLogger.error( + `Failed to delete checkpoint file ${oldPath}:`, + error, + ); throw error; // Rethrow unexpected errors } // It's okay if it doesn't exist. @@ -428,7 +442,7 @@ export class Logger { return false; // It truly doesn't exist in either format. } // A different error occurred. - console.error( + this.debugLogger.error( `Failed to check checkpoint existence for ${ filePath ?? `path for tag "${tag}"` }:`, diff --git a/packages/core/src/ide/ide-client.ts b/packages/core/src/ide/ide-client.ts index b216506f7..d839004ad 100644 --- a/packages/core/src/ide/ide-client.ts +++ b/packages/core/src/ide/ide-client.ts @@ -25,13 +25,9 @@ import * as path from 'node:path'; import { EnvHttpProxyAgent } from 'undici'; import { ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js'; import { IDE_REQUEST_TIMEOUT_MS } from './constants.js'; +import { createDebugLogger } from '../utils/debugLogger.js'; -const logger = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - debug: (...args: any[]) => console.debug('[DEBUG] [IDEClient]', ...args), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - error: (...args: any[]) => console.error('[ERROR] [IDEClient]', ...args), -}; +const debugLogger = createDebugLogger('IDE'); export type DiffUpdateResult = | { @@ -261,7 +257,7 @@ export class IdeClient { ); const errorMessage = textPart?.text ?? `Tool 'openDiff' reported an error.`; - logger.debug( + debugLogger.debug( `Request for openDiff ${filePath} failed with isError:`, errorMessage, ); @@ -270,7 +266,7 @@ export class IdeClient { } }) .catch((err) => { - logger.debug(`Request for openDiff ${filePath} failed:`, err); + debugLogger.debug(`Request for openDiff ${filePath} failed:`, err); this.diffResponses.delete(filePath); reject(err); }); @@ -342,7 +338,7 @@ export class IdeClient { ); const errorMessage = textPart?.text ?? `Tool 'closeDiff' reported an error.`; - logger.debug( + debugLogger.debug( `Request for closeDiff ${filePath} failed with isError:`, errorMessage, ); @@ -361,14 +357,14 @@ export class IdeClient { return undefined; } } catch (_e) { - logger.debug( + debugLogger.debug( `Invalid JSON in closeDiff response for ${filePath}:`, textPart.text, ); } } } catch (err) { - logger.debug(`Request for closeDiff ${filePath} failed:`, err); + debugLogger.debug(`Request for closeDiff ${filePath} failed:`, err); } return undefined; } @@ -434,7 +430,7 @@ export class IdeClient { return; } try { - logger.debug('Discovering tools from IDE...'); + debugLogger.debug('Discovering tools from IDE...'); const response = await this.client.request( { method: 'tools/list', params: {} }, ListToolsResultSchema, @@ -444,11 +440,11 @@ export class IdeClient { this.availableTools = response.tools.map((tool) => tool.name); if (this.availableTools.length > 0) { - logger.debug( + debugLogger.debug( `Discovered ${this.availableTools.length} tools from IDE: ${this.availableTools.join(', ')}`, ); } else { - logger.debug( + debugLogger.debug( 'IDE supports tool discovery, but no tools are available.', ); } @@ -459,9 +455,9 @@ export class IdeClient { error instanceof Error && !error.message?.includes('Method not found') ) { - logger.error(`Error discovering tools from IDE: ${error.message}`); + debugLogger.error(`Error discovering tools from IDE: ${error.message}`); } else { - logger.debug('IDE does not support tool discovery.'); + debugLogger.debug('IDE does not support tool discovery.'); } this.availableTools = []; } @@ -485,11 +481,11 @@ export class IdeClient { } if (details) { if (logToConsole) { - logger.error(details); + debugLogger.error(details); } else { // We only want to log disconnect messages to debug // if they are not already being logged to the console. - logger.debug(details); + debugLogger.debug(details); } } } @@ -557,12 +553,15 @@ export class IdeClient { if (Array.isArray(parsedArgs)) { args = parsedArgs; } else { - logger.error( + debugLogger.error( 'QWEN_CODE_IDE_SERVER_STDIO_ARGS must be a JSON array string.', ); } } catch (e) { - logger.error('Failed to parse QWEN_CODE_IDE_SERVER_STDIO_ARGS:', e); + debugLogger.error( + 'Failed to parse QWEN_CODE_IDE_SERVER_STDIO_ARGS:', + e, + ); } } @@ -640,7 +639,7 @@ export class IdeClient { fileRegex.test(file), ); } catch (e) { - logger.debug('Failed to read IDE connection directory:', e); + debugLogger.debug('Failed to read IDE connection directory:', e); return []; } @@ -654,13 +653,13 @@ export class IdeClient { const parsed = JSON.parse(content); return { file, mtimeMs: stat.mtimeMs, parsed }; } catch (e) { - logger.debug('Failed to parse JSON from lock file: ', e); + debugLogger.debug('Failed to parse JSON from lock file: ', e); return { file, mtimeMs: stat.mtimeMs, parsed: undefined }; } } catch (e) { // If we can't stat/read the file, treat it as very old so it doesn't // win ties, and skip parsing by returning undefined content. - logger.debug('Failed to read/stat IDE lock file:', e); + debugLogger.debug('Failed to read/stat IDE lock file:', e); return { file, mtimeMs: -Infinity, parsed: undefined }; } }), @@ -742,7 +741,7 @@ export class IdeClient { resolver({ status: 'accepted', content }); this.diffResponses.delete(filePath); } else { - logger.debug(`No resolver found for ${filePath}`); + debugLogger.debug(`No resolver found for ${filePath}`); } }, ); @@ -756,7 +755,7 @@ export class IdeClient { resolver({ status: 'rejected', content: undefined }); this.diffResponses.delete(filePath); } else { - logger.debug(`No resolver found for ${filePath}`); + debugLogger.debug(`No resolver found for ${filePath}`); } }, ); @@ -772,7 +771,7 @@ export class IdeClient { resolver({ status: 'rejected', content: undefined }); this.diffResponses.delete(filePath); } else { - logger.debug(`No resolver found for ${filePath}`); + debugLogger.debug(`No resolver found for ${filePath}`); } }, ); @@ -781,7 +780,7 @@ export class IdeClient { private async establishHttpConnection(port: string): Promise { let transport: StreamableHTTPClientTransport | undefined; try { - logger.debug('Attempting to connect to IDE via HTTP SSE'); + debugLogger.debug('Attempting to connect to IDE via HTTP SSE'); this.client = new Client({ name: 'streamable-http-client', // TODO(#3487): use the CLI version here. @@ -812,7 +811,7 @@ export class IdeClient { try { await transport.close(); } catch (closeError) { - logger.debug('Failed to close transport:', closeError); + debugLogger.debug('Failed to close transport:', closeError); } } return false; @@ -825,7 +824,7 @@ export class IdeClient { }: StdioConfig): Promise { let transport: StdioClientTransport | undefined; try { - logger.debug('Attempting to connect to IDE via stdio'); + debugLogger.debug('Attempting to connect to IDE via stdio'); this.client = new Client({ name: 'stdio-client', // TODO(#3487): use the CLI version here. @@ -846,7 +845,7 @@ export class IdeClient { try { await transport.close(); } catch (closeError) { - logger.debug('Failed to close transport:', closeError); + debugLogger.debug('Failed to close transport:', closeError); } } return false; diff --git a/packages/core/src/mcp/oauth-provider.ts b/packages/core/src/mcp/oauth-provider.ts index 2b657f352..efa7ec5e1 100644 --- a/packages/core/src/mcp/oauth-provider.ts +++ b/packages/core/src/mcp/oauth-provider.ts @@ -13,6 +13,7 @@ import type { OAuthToken } from './token-storage/types.js'; import { MCPOAuthTokenStorage } from './oauth-token-storage.js'; import { getErrorMessage } from '../utils/errors.js'; import { OAuthUtils } from './oauth-utils.js'; +import { createDebugLogger } from '../utils/debugLogger.js'; import { MCP_OAUTH_CLIENT_NAME, OAUTH_REDIRECT_PORT, @@ -21,6 +22,8 @@ import { export const OAUTH_DISPLAY_MESSAGE_EVENT = 'oauth-display-message' as const; +const debugLogger = createDebugLogger('MCP_OAUTH'); + /** * OAuth configuration for an MCP server. */ @@ -610,7 +613,7 @@ export class MCPOAuthProvider { // If no authorization URL is provided, try to discover OAuth configuration if (!config.authorizationUrl && mcpServerUrl) { - console.debug(`Starting OAuth for MCP server "${serverName}"… + debugLogger.debug(`Starting OAuth for MCP server "${serverName}"… ✓ No authorization URL; using OAuth discovery`); // First check if the server requires authentication via WWW-Authenticate header @@ -647,7 +650,7 @@ export class MCPOAuthProvider { } } } catch (error) { - console.debug( + debugLogger.debug( `Failed to check endpoint for authentication requirements: ${getErrorMessage(error)}`, ); } @@ -692,7 +695,7 @@ export class MCPOAuthProvider { const authUrl = new URL(config.authorizationUrl); const serverUrl = `${authUrl.protocol}//${authUrl.host}`; - console.debug('→ Attempting dynamic client registration...'); + debugLogger.debug('→ Attempting dynamic client registration...'); // Get the authorization server metadata for registration const authServerMetadata = @@ -718,7 +721,7 @@ export class MCPOAuthProvider { config.clientSecret = clientRegistration.client_secret; } - console.debug('✓ Dynamic client registration successful'); + debugLogger.debug('✓ Dynamic client registration successful'); } else { throw new Error( 'No client ID provided and dynamic registration not supported', @@ -767,7 +770,9 @@ ${authUrl} // Wait for callback const { code } = await callbackPromise; - console.debug('✓ Authorization code received, exchanging for tokens...'); + debugLogger.debug( + '✓ Authorization code received, exchanging for tokens...', + ); // Exchange code for tokens const tokenResponse = await this.exchangeCodeForToken( @@ -802,7 +807,7 @@ ${authUrl} config.tokenUrl, mcpServerUrl, ); - console.debug('✓ Authentication successful! Token saved.'); + debugLogger.debug('✓ Authentication successful! Token saved.'); // Verify token was saved const savedToken = await this.tokenStorage.getCredentials(serverName); @@ -813,7 +818,7 @@ ${authUrl} .update(savedToken.token.accessToken) .digest('hex') .slice(0, 8); - console.debug( + debugLogger.debug( `✓ Token verification successful (fingerprint: ${tokenFingerprint})`, ); } else { @@ -840,22 +845,22 @@ ${authUrl} serverName: string, config: MCPOAuthConfig, ): Promise { - console.debug(`Getting valid token for server: ${serverName}`); + debugLogger.debug(`Getting valid token for server: ${serverName}`); const credentials = await this.tokenStorage.getCredentials(serverName); if (!credentials) { - console.debug(`No credentials found for server: ${serverName}`); + debugLogger.debug(`No credentials found for server: ${serverName}`); return null; } const { token } = credentials; - console.debug( + debugLogger.debug( `Found token for server: ${serverName}, expired: ${this.tokenStorage.isTokenExpired(token)}`, ); // Check if token is expired if (!this.tokenStorage.isTokenExpired(token)) { - console.debug(`Returning valid token for server: ${serverName}`); + debugLogger.debug(`Returning valid token for server: ${serverName}`); return token.accessToken; } diff --git a/packages/core/src/mcp/oauth-utils.ts b/packages/core/src/mcp/oauth-utils.ts index 47ef1d366..cbcde204b 100644 --- a/packages/core/src/mcp/oauth-utils.ts +++ b/packages/core/src/mcp/oauth-utils.ts @@ -6,6 +6,9 @@ import type { MCPOAuthConfig } from './oauth-provider.js'; import { getErrorMessage } from '../utils/errors.js'; +import { createDebugLogger } from '../utils/debugLogger.js'; + +const debugLogger = createDebugLogger('MCP_OAUTH'); /** * OAuth authorization server metadata as per RFC 8414. @@ -94,7 +97,7 @@ export class OAuthUtils { } return (await response.json()) as OAuthProtectedResourceMetadata; } catch (error) { - console.debug( + debugLogger.debug( `Failed to fetch protected resource metadata from ${resourceMetadataUrl}: ${getErrorMessage(error)}`, ); return null; @@ -117,7 +120,7 @@ export class OAuthUtils { } return (await response.json()) as OAuthAuthorizationServerMetadata; } catch (error) { - console.debug( + debugLogger.debug( `Failed to fetch authorization server metadata from ${authServerMetadataUrl}: ${getErrorMessage(error)}`, ); return null; @@ -205,7 +208,7 @@ export class OAuthUtils { } } - console.debug( + debugLogger.debug( `Metadata discovery failed for authorization server ${authServerUrl}`, ); return null; @@ -259,7 +262,7 @@ export class OAuthUtils { } // Fallback: try well-known endpoints at the base URL - console.debug(`Trying OAuth discovery fallback at ${serverUrl}`); + debugLogger.debug(`Trying OAuth discovery fallback at ${serverUrl}`); const authServerMetadata = await this.discoverAuthorizationServerMetadata(serverUrl); @@ -276,7 +279,7 @@ export class OAuthUtils { return null; } catch (error) { - console.debug( + debugLogger.debug( `Failed to discover OAuth configuration: ${getErrorMessage(error)}`, ); return null; diff --git a/packages/core/src/qwen/qwenOAuth2.ts b/packages/core/src/qwen/qwenOAuth2.ts index b18c0319d..5e94ffa5c 100644 --- a/packages/core/src/qwen/qwenOAuth2.ts +++ b/packages/core/src/qwen/qwenOAuth2.ts @@ -14,12 +14,15 @@ import { EventEmitter } from 'events'; import type { Config } from '../config/config.js'; import { randomUUID } from 'node:crypto'; import { formatFetchErrorForUser } from '../utils/fetch.js'; +import { createDebugLogger } from '../utils/debugLogger.js'; import { SharedTokenManager, TokenManagerError, TokenError, } from './sharedTokenManager.js'; +const debugLogger = createDebugLogger('QWEN_OAUTH'); + // OAuth Endpoints const QWEN_OAUTH_BASE_URL = 'https://chat.qwen.ai'; @@ -312,7 +315,7 @@ export class QwenOAuth2Client implements IQwenOAuth2Client { } const result = (await response.json()) as DeviceAuthorizationResponse; - console.debug('Device authorization result:', result); + debugLogger.debug('Device authorization result:', result); // Check if the response indicates success if (!isDeviceAuthorizationSuccess(result)) { @@ -498,20 +501,22 @@ export async function getQwenOAuthClient( if (error instanceof TokenManagerError) { switch (error.type) { case TokenError.NO_REFRESH_TOKEN: - console.debug( + debugLogger.debug( 'No refresh token available, proceeding with device flow', ); break; case TokenError.REFRESH_FAILED: - console.debug('Token refresh failed, proceeding with device flow'); + debugLogger.debug( + 'Token refresh failed, proceeding with device flow', + ); break; case TokenError.NETWORK_ERROR: - console.warn( + debugLogger.warn( 'Network error during token refresh, trying device flow', ); break; default: - console.warn('Token manager error:', (error as Error).message); + debugLogger.warn('Token manager error:', (error as Error).message); } } @@ -678,7 +683,7 @@ async function authWithQwenDeviceFlow( return null; } const message = 'Authentication cancelled by user.'; - console.debug('\n' + message); + debugLogger.debug('\n' + message); qwenOAuth2Events.emit(QwenOAuth2Event.AuthProgress, 'error', message); return { success: false, reason: 'cancelled', message }; }; @@ -702,14 +707,11 @@ async function authWithQwenDeviceFlow( // causing the entire Node.js process to crash. if (childProcess) { childProcess.on('error', (err) => { - console.debug( - 'Browser launch failed:', - err.message || 'Unknown error', - ); + debugLogger.debug('Browser launch failed:', err.message || err); }); } } catch (err) { - console.debug( + debugLogger.debug( 'Failed to open browser:', err instanceof Error ? err.message : 'Unknown error', ); @@ -748,7 +750,7 @@ async function authWithQwenDeviceFlow( } emitAuthProgress('polling', 'Waiting for authorization...'); - console.debug('Waiting for authorization...\n'); + debugLogger.debug('Waiting for authorization...\n'); // Poll for the token let pollInterval = 2000; // 2 seconds, can be increased if slow_down is received @@ -764,7 +766,7 @@ async function authWithQwenDeviceFlow( } try { - console.debug('polling for token...'); + debugLogger.debug('polling for token...'); const tokenResponse = await client.pollDeviceToken({ device_code: deviceAuth.device_code, code_verifier, @@ -808,7 +810,9 @@ async function authWithQwenDeviceFlow( 'Authentication successful! Access token obtained.', ); - console.debug('Authentication successful! Access token obtained.'); + debugLogger.debug( + 'Authentication successful! Access token obtained.', + ); return { success: true }; } @@ -819,7 +823,7 @@ async function authWithQwenDeviceFlow( // Handle slow_down error by increasing poll interval if (pendingData.slowDown) { pollInterval = Math.min(pollInterval * 1.5, 10000); // Increase by 50%, max 10 seconds - console.debug( + debugLogger.debug( `\nServer requested to slow down, increasing poll interval to ${pollInterval}ms'`, ); } else { @@ -981,7 +985,7 @@ export async function clearQwenCredentials(): Promise { try { const filePath = getQwenCachedCredentialPath(); await fs.unlink(filePath); - console.debug('Cached Qwen credentials cleared successfully.'); + debugLogger.debug('Cached Qwen credentials cleared successfully.'); } catch (error: unknown) { // If file doesn't exist or can't be deleted, we consider it cleared if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { diff --git a/packages/core/src/qwen/sharedTokenManager.ts b/packages/core/src/qwen/sharedTokenManager.ts index 5b769bbe6..32220bb3d 100644 --- a/packages/core/src/qwen/sharedTokenManager.ts +++ b/packages/core/src/qwen/sharedTokenManager.ts @@ -17,6 +17,9 @@ import { isErrorResponse, CredentialsClearRequiredError, } from './qwenOAuth2.js'; +import { createDebugLogger } from '../utils/debugLogger.js'; + +const debugLogger = createDebugLogger('QWEN_OAUTH'); // File System Configuration const QWEN_DIR = '.qwen'; @@ -549,7 +552,7 @@ export class SharedTokenManager { } catch (error) { // Handle credentials clear required error (400 status from refresh) if (error instanceof CredentialsClearRequiredError) { - console.debug( + debugLogger.debug( 'SharedTokenManager: Clearing memory cache due to credentials clear requirement', ); // Clear memory cache when credentials need to be cleared diff --git a/packages/core/src/services/loopDetectionService.test.ts b/packages/core/src/services/loopDetectionService.test.ts index 7d60ac897..c7629e134 100644 --- a/packages/core/src/services/loopDetectionService.test.ts +++ b/packages/core/src/services/loopDetectionService.test.ts @@ -644,6 +644,12 @@ describe('LoopDetectionService LLM Checks', () => { getGeminiClient: () => mockGeminiClient, getBaseLlmClient: () => mockBaseLlmClient, getDebugMode: () => false, + getDebugLogger: () => ({ + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + }), getTelemetryEnabled: () => true, getModel: () => 'test-model', } as unknown as Config; diff --git a/packages/core/src/services/loopDetectionService.ts b/packages/core/src/services/loopDetectionService.ts index 095ef0ab4..f8a06b5ef 100644 --- a/packages/core/src/services/loopDetectionService.ts +++ b/packages/core/src/services/loopDetectionService.ts @@ -431,7 +431,7 @@ export class LoopDetectionService { }); } catch (e) { // Do nothing, treat it as a non-loop. - this.config.getDebugMode() ? console.error(e) : console.debug(e); + this.config.getDebugLogger().error(e); return false; } diff --git a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts index 5c75d0c2a..540b8161f 100644 --- a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts +++ b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts @@ -33,6 +33,10 @@ import type { } from '../types.js'; import { EventMetadataKey } from './event-metadata-key.js'; import type { Config } from '../../config/config.js'; +import { + createDebugLogger, + type DebugLogger, +} from '../../utils/debugLogger.js'; import { InstallationManager } from '../../utils/installationManager.js'; import { safeJsonStringify } from '../../utils/safeJsonStringify.js'; import { FixedDeque } from 'mnemonist'; @@ -153,6 +157,7 @@ const MAX_RETRY_EVENTS = 100; export class ClearcutLogger { private static instance: ClearcutLogger; private config?: Config; + private debugLogger: DebugLogger; private sessionData: EventValue[] = []; private promptId: string = ''; private readonly installationManager: InstallationManager; @@ -181,6 +186,7 @@ export class ClearcutLogger { private constructor(config: Config) { this.config = config; + this.debugLogger = createDebugLogger('CLEARCUT'); this.events = new FixedDeque(Array, MAX_EVENTS); this.promptId = config?.getSessionId() ?? ''; this.installationManager = new InstallationManager(); @@ -217,15 +223,16 @@ export class ClearcutLogger { }, ]); - if (wasAtCapacity && this.config?.getDebugMode()) { - console.debug( + if (wasAtCapacity) { + this.debugLogger.debug( `ClearcutLogger: Dropped old event to prevent memory leak (queue size: ${this.events.size})`, ); } } catch (error) { - if (this.config?.getDebugMode()) { - console.error('ClearcutLogger: Failed to enqueue log event.', error); - } + this.debugLogger.error( + 'ClearcutLogger: Failed to enqueue log event.', + error, + ); } } @@ -254,25 +261,21 @@ export class ClearcutLogger { } this.flushToClearcut().catch((error) => { - console.debug('Error flushing to Clearcut:', error); + this.debugLogger.debug('Error flushing to Clearcut:', error); }); } async flushToClearcut(): Promise { if (this.flushing) { - if (this.config?.getDebugMode()) { - console.debug( - 'ClearcutLogger: Flush already in progress, marking pending flush.', - ); - } + this.debugLogger.debug( + 'ClearcutLogger: Flush already in progress, marking pending flush.', + ); this.pendingFlush = true; return Promise.resolve({}); } this.flushing = true; - if (this.config?.getDebugMode()) { - console.log('Flushing log events to Clearcut.'); - } + this.debugLogger.debug('Flushing log events to Clearcut.'); const eventsToSend = this.events.toArray() as LogEventEntry[][]; this.events.clear(); @@ -305,19 +308,15 @@ export class ClearcutLogger { nextRequestWaitMs, }; } else { - if (this.config?.getDebugMode()) { - console.error( - `Error flushing log events: HTTP ${response.status}: ${response.statusText}`, - ); - } + this.debugLogger.error( + `Error flushing log events: HTTP ${response.status}: ${response.statusText}`, + ); // Re-queue failed events for retry this.requeueFailedEvents(eventsToSend); } } catch (e: unknown) { - if (this.config?.getDebugMode()) { - console.error('Error flushing log events:', e as Error); - } + this.debugLogger.error('Error flushing log events:', e as Error); // Re-queue failed events for retry this.requeueFailedEvents(eventsToSend); @@ -330,9 +329,7 @@ export class ClearcutLogger { this.pendingFlush = false; // Fire and forget the pending flush this.flushToClearcut().catch((error) => { - if (this.config?.getDebugMode()) { - console.debug('Error in pending flush to Clearcut:', error); - } + this.debugLogger.debug('Error in pending flush to Clearcut:', error); }); } @@ -423,7 +420,7 @@ export class ClearcutLogger { // Flush start event immediately this.enqueueLogEvent(this.createLogEvent(EventNames.START_SESSION, data)); this.flushToClearcut().catch((error) => { - console.debug('Error flushing to Clearcut:', error); + this.debugLogger.debug('Error flushing to Clearcut:', error); }); } @@ -644,14 +641,14 @@ export class ClearcutLogger { logFlashFallbackEvent(): void { this.enqueueLogEvent(this.createLogEvent(EventNames.FLASH_FALLBACK, [])); this.flushToClearcut().catch((error) => { - console.debug('Error flushing to Clearcut:', error); + this.debugLogger.debug('Error flushing to Clearcut:', error); }); } logRipgrepFallbackEvent(): void { this.enqueueLogEvent(this.createLogEvent(EventNames.RIPGREP_FALLBACK, [])); this.flushToClearcut().catch((error) => { - console.debug('Error flushing to Clearcut:', error); + this.debugLogger.debug('Error flushing to Clearcut:', error); }); } @@ -791,7 +788,7 @@ export class ClearcutLogger { // Flush immediately on session end. this.enqueueLogEvent(this.createLogEvent(EventNames.END_SESSION, [])); this.flushToClearcut().catch((error) => { - console.debug('Error flushing to Clearcut:', error); + this.debugLogger.debug('Error flushing to Clearcut:', error); }); } @@ -890,7 +887,7 @@ export class ClearcutLogger { this.createLogEvent(EventNames.EXTENSION_INSTALL, data), ); this.flushToClearcut().catch((error) => { - console.debug('Error flushing to Clearcut:', error); + this.debugLogger.debug('Error flushing to Clearcut:', error); }); } @@ -910,7 +907,7 @@ export class ClearcutLogger { this.createLogEvent(EventNames.EXTENSION_UNINSTALL, data), ); this.flushToClearcut().catch((error) => { - console.debug('Error flushing to Clearcut:', error); + this.debugLogger.debug('Error flushing to Clearcut:', error); }); } @@ -964,7 +961,7 @@ export class ClearcutLogger { this.createLogEvent(EventNames.EXTENSION_ENABLE, data), ); this.flushToClearcut().catch((error) => { - console.debug('Error flushing to Clearcut:', error); + this.debugLogger.debug('Error flushing to Clearcut:', error); }); } @@ -999,7 +996,7 @@ export class ClearcutLogger { this.createLogEvent(EventNames.EXTENSION_DISABLE, data), ); this.flushToClearcut().catch((error) => { - console.debug('Error flushing to Clearcut:', error); + this.debugLogger.debug('Error flushing to Clearcut:', error); }); } @@ -1078,8 +1075,8 @@ export class ClearcutLogger { const eventsToRetry = eventsToSend.slice(-MAX_RETRY_EVENTS); // Keep only the most recent events // Log a warning if we're dropping events - if (eventsToSend.length > MAX_RETRY_EVENTS && this.config?.getDebugMode()) { - console.warn( + if (eventsToSend.length > MAX_RETRY_EVENTS) { + this.debugLogger.warn( `ClearcutLogger: Dropping ${ eventsToSend.length - MAX_RETRY_EVENTS } events due to retry queue limit. Total events: ${ @@ -1093,11 +1090,9 @@ export class ClearcutLogger { const numEventsToRequeue = Math.min(eventsToRetry.length, availableSpace); if (numEventsToRequeue === 0) { - if (this.config?.getDebugMode()) { - console.debug( - `ClearcutLogger: No events re-queued (queue size: ${this.events.size})`, - ); - } + this.debugLogger.debug( + `ClearcutLogger: No events re-queued (queue size: ${this.events.size})`, + ); return; } @@ -1116,11 +1111,9 @@ export class ClearcutLogger { this.events.pop(); } - if (this.config?.getDebugMode()) { - console.debug( - `ClearcutLogger: Re-queued ${numEventsToRequeue} events for retry (queue size: ${this.events.size})`, - ); - } + this.debugLogger.debug( + `ClearcutLogger: Re-queued ${numEventsToRequeue} events for retry (queue size: ${this.events.size})`, + ); } } diff --git a/packages/core/src/telemetry/qwen-logger/qwen-logger.test.ts b/packages/core/src/telemetry/qwen-logger/qwen-logger.test.ts index 41871c36e..0f80e87ac 100644 --- a/packages/core/src/telemetry/qwen-logger/qwen-logger.test.ts +++ b/packages/core/src/telemetry/qwen-logger/qwen-logger.test.ts @@ -26,6 +26,13 @@ import { } from '../types.js'; import type { RumEvent, RumPayload } from './event-types.js'; +const debugLoggerSpy = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), +}; + // Mock dependencies vi.mock('../../utils/user_id.js', () => ({ getInstallationId: vi.fn(() => 'test-installation-id'), @@ -35,6 +42,15 @@ vi.mock('../../utils/safeJsonStringify.js', () => ({ safeJsonStringify: vi.fn((obj) => JSON.stringify(obj)), })); +vi.mock('../../utils/debugLogger.js', async (importOriginal) => { + const original = + await importOriginal(); + return { + ...original, + createDebugLogger: () => debugLoggerSpy, + }; +}); + // Mock https module vi.mock('https', () => ({ request: vi.fn(), @@ -72,6 +88,10 @@ describe('QwenLogger', () => { vi.useFakeTimers(); vi.setSystemTime(new Date('2025-01-01T12:00:00.000Z')); mockConfig = makeFakeConfig(); + debugLoggerSpy.debug.mockClear(); + debugLoggerSpy.info.mockClear(); + debugLoggerSpy.warn.mockClear(); + debugLoggerSpy.error.mockClear(); // Clear singleton instance // eslint-disable-next-line @typescript-eslint/no-explicit-any (QwenLogger as any).instance = undefined; @@ -126,11 +146,7 @@ describe('QwenLogger', () => { describe('event queue management', () => { it('should handle event overflow gracefully', () => { - const debugConfig = makeFakeConfig({ getDebugMode: () => true }); - const logger = QwenLogger.getInstance(debugConfig)!; - const consoleSpy = vi - .spyOn(console, 'debug') - .mockImplementation(() => {}); + const logger = QwenLogger.getInstance(mockConfig)!; // Fill the queue beyond capacity for (let i = 0; i < TEST_ONLY.MAX_EVENTS + 10; i++) { @@ -143,7 +159,7 @@ describe('QwenLogger', () => { } // Should have logged debug messages about dropping events - expect(consoleSpy).toHaveBeenCalledWith( + expect(debugLoggerSpy.debug).toHaveBeenCalledWith( expect.stringContaining( 'QwenLogger: Dropped old event to prevent memory leak', ), @@ -151,11 +167,7 @@ describe('QwenLogger', () => { }); it('should handle enqueue errors gracefully', () => { - const debugConfig = makeFakeConfig({ getDebugMode: () => true }); - const logger = QwenLogger.getInstance(debugConfig)!; - const consoleSpy = vi - .spyOn(console, 'error') - .mockImplementation(() => {}); + const logger = QwenLogger.getInstance(mockConfig)!; // Mock the events deque to throw an error const originalPush = logger['events'].push; @@ -170,7 +182,7 @@ describe('QwenLogger', () => { name: 'test-event', }); - expect(consoleSpy).toHaveBeenCalledWith( + expect(debugLoggerSpy.error).toHaveBeenCalledWith( 'QwenLogger: Failed to enqueue log event.', expect.any(Error), ); @@ -182,11 +194,7 @@ describe('QwenLogger', () => { describe('concurrent flush protection', () => { it('should handle concurrent flush requests', () => { - const debugConfig = makeFakeConfig({ getDebugMode: () => true }); - const logger = QwenLogger.getInstance(debugConfig)!; - const consoleSpy = vi - .spyOn(console, 'debug') - .mockImplementation(() => {}); + const logger = QwenLogger.getInstance(mockConfig)!; // Manually set the flush in progress flag to simulate concurrent access logger['isFlushInProgress'] = true; @@ -195,7 +203,7 @@ describe('QwenLogger', () => { const result = logger.flushToRum(); // Should have logged about pending flush - expect(consoleSpy).toHaveBeenCalledWith( + expect(debugLoggerSpy.debug).toHaveBeenCalledWith( expect.stringContaining( 'QwenLogger: Flush already in progress, marking pending flush', ), @@ -211,11 +219,7 @@ describe('QwenLogger', () => { describe('failed event retry mechanism', () => { it('should requeue failed events with size limits', () => { - const debugConfig = makeFakeConfig({ getDebugMode: () => true }); - const logger = QwenLogger.getInstance(debugConfig)!; - const consoleSpy = vi - .spyOn(console, 'debug') - .mockImplementation(() => {}); + const logger = QwenLogger.getInstance(mockConfig)!; const failedEvents: RumEvent[] = []; for (let i = 0; i < TEST_ONLY.MAX_RETRY_EVENTS + 50; i++) { @@ -232,17 +236,13 @@ describe('QwenLogger', () => { (logger as any).requeueFailedEvents(failedEvents); // Should have logged about dropping events due to retry limit - expect(consoleSpy).toHaveBeenCalledWith( + expect(debugLoggerSpy.debug).toHaveBeenCalledWith( expect.stringContaining('QwenLogger: Re-queued'), ); }); it('should handle empty retry queue gracefully', () => { - const debugConfig = makeFakeConfig({ getDebugMode: () => true }); - const logger = QwenLogger.getInstance(debugConfig)!; - const consoleSpy = vi - .spyOn(console, 'debug') - .mockImplementation(() => {}); + const logger = QwenLogger.getInstance(mockConfig)!; // Fill the queue to capacity first for (let i = 0; i < TEST_ONLY.MAX_EVENTS; i++) { @@ -267,7 +267,7 @@ describe('QwenLogger', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (logger as any).requeueFailedEvents(failedEvents); - expect(consoleSpy).toHaveBeenCalledWith( + expect(debugLoggerSpy.debug).toHaveBeenCalledWith( expect.stringContaining('QwenLogger: No events re-queued'), ); }); @@ -386,11 +386,7 @@ describe('QwenLogger', () => { describe('error handling', () => { it('should handle flush errors gracefully with debug mode', async () => { - const debugConfig = makeFakeConfig({ getDebugMode: () => true }); - const logger = QwenLogger.getInstance(debugConfig)!; - const consoleSpy = vi - .spyOn(console, 'debug') - .mockImplementation(() => {}); + const logger = QwenLogger.getInstance(mockConfig)!; // Add an event first logger.enqueueLogEvent({ @@ -412,7 +408,7 @@ describe('QwenLogger', () => { // Wait for async operations await vi.runAllTimersAsync(); - expect(consoleSpy).toHaveBeenCalledWith( + expect(debugLoggerSpy.debug).toHaveBeenCalledWith( 'Error flushing to RUM:', expect.any(Error), ); diff --git a/packages/core/src/telemetry/qwen-logger/qwen-logger.ts b/packages/core/src/telemetry/qwen-logger/qwen-logger.ts index e5e2d2106..19c94b20c 100644 --- a/packages/core/src/telemetry/qwen-logger/qwen-logger.ts +++ b/packages/core/src/telemetry/qwen-logger/qwen-logger.ts @@ -54,6 +54,10 @@ import type { RumOS, } from './event-types.js'; import type { Config } from '../../config/config.js'; +import { + createDebugLogger, + type DebugLogger, +} from '../../utils/debugLogger.js'; import { safeJsonStringify } from '../../utils/safeJsonStringify.js'; import { InstallationManager } from '../../utils/installationManager.js'; import { FixedDeque } from 'mnemonist'; @@ -91,6 +95,7 @@ export interface LogResponse { export class QwenLogger { private static instance: QwenLogger; private config?: Config; + private debugLogger: DebugLogger; private readonly installationManager: InstallationManager; /** @@ -121,6 +126,7 @@ export class QwenLogger { private constructor(config: Config) { this.config = config; + this.debugLogger = createDebugLogger('QWEN_LOGGER'); this.events = new FixedDeque(Array, MAX_EVENTS); this.installationManager = new InstallationManager(); this.userId = this.generateUserId(); @@ -154,15 +160,13 @@ export class QwenLogger { this.events.push(event); - if (wasAtCapacity && this.config?.getDebugMode()) { - console.debug( + if (wasAtCapacity) { + this.debugLogger.debug( `QwenLogger: Dropped old event to prevent memory leak (queue size: ${this.events.size})`, ); } } catch (error) { - if (this.config?.getDebugMode()) { - console.error('QwenLogger: Failed to enqueue log event.', error); - } + this.debugLogger.error('QwenLogger: Failed to enqueue log event.', error); } } @@ -266,27 +270,21 @@ export class QwenLogger { } this.flushToRum().catch((error) => { - if (this.config?.getDebugMode()) { - console.debug('Error flushing to RUM:', error); - } + this.debugLogger.debug('Error flushing to RUM:', error); }); } async flushToRum(): Promise { if (this.isFlushInProgress) { - if (this.config?.getDebugMode()) { - console.debug( - 'QwenLogger: Flush already in progress, marking pending flush.', - ); - } + this.debugLogger.debug( + 'QwenLogger: Flush already in progress, marking pending flush.', + ); this.pendingFlush = true; return Promise.resolve({}); } this.isFlushInProgress = true; - if (this.config?.getDebugMode()) { - console.log('Flushing log events to RUM.'); - } + this.debugLogger.debug('Flushing log events to RUM.'); if (this.events.size === 0) { this.isFlushInProgress = false; return {}; @@ -338,9 +336,7 @@ export class QwenLogger { this.lastFlushTime = Date.now(); return {}; } catch (error) { - if (this.config?.getDebugMode()) { - console.error('RUM flush failed.', error); - } + this.debugLogger.error('RUM flush failed.', error); // Re-queue failed events for retry this.requeueFailedEvents(eventsToSend); @@ -353,9 +349,7 @@ export class QwenLogger { this.pendingFlush = false; // Fire and forget the pending flush this.flushToRum().catch((error) => { - if (this.config?.getDebugMode()) { - console.debug('Error in pending flush to RUM:', error); - } + this.debugLogger.debug('Error in pending flush to RUM:', error); }); } } @@ -366,12 +360,10 @@ export class QwenLogger { // Flush all pending events with the old session ID first. // If flush fails, discard the pending events to avoid mixing sessions. await this.flushToRum().catch((error: unknown) => { - if (this.config?.getDebugMode()) { - console.debug( - 'Error flushing pending events before session start:', - error, - ); - } + this.debugLogger.debug( + 'Error flushing pending events before session start:', + error, + ); }); // Clear any remaining events (discard if flush failed) @@ -402,9 +394,7 @@ export class QwenLogger { // Flush start event immediately this.enqueueLogEvent(applicationEvent); this.flushToRum().catch((error: unknown) => { - if (this.config?.getDebugMode()) { - console.debug('Error flushing to RUM:', error); - } + this.debugLogger.debug('Error flushing to RUM:', error); }); } @@ -414,9 +404,7 @@ export class QwenLogger { // Flush immediately on session end. this.enqueueLogEvent(applicationEvent); this.flushToRum().catch((error: unknown) => { - if (this.config?.getDebugMode()) { - console.debug('Error flushing to RUM:', error); - } + this.debugLogger.debug('Error flushing to RUM:', error); }); } @@ -917,8 +905,8 @@ export class QwenLogger { const eventsToRetry = eventsToSend.slice(-MAX_RETRY_EVENTS); // Keep only the most recent events // Log a warning if we're dropping events - if (eventsToSend.length > MAX_RETRY_EVENTS && this.config?.getDebugMode()) { - console.warn( + if (eventsToSend.length > MAX_RETRY_EVENTS) { + this.debugLogger.warn( `QwenLogger: Dropping ${ eventsToSend.length - MAX_RETRY_EVENTS } events due to retry queue limit. Total events: ${ @@ -932,11 +920,9 @@ export class QwenLogger { const numEventsToRequeue = Math.min(eventsToRetry.length, availableSpace); if (numEventsToRequeue === 0) { - if (this.config?.getDebugMode()) { - console.debug( - `QwenLogger: No events re-queued (queue size: ${this.events.size})`, - ); - } + this.debugLogger.debug( + `QwenLogger: No events re-queued (queue size: ${this.events.size})`, + ); return; } @@ -955,11 +941,9 @@ export class QwenLogger { this.events.pop(); } - if (this.config?.getDebugMode()) { - console.debug( - `QwenLogger: Re-queued ${numEventsToRequeue} events for retry (queue size: ${this.events.size})`, - ); - } + this.debugLogger.debug( + `QwenLogger: Re-queued ${numEventsToRequeue} events for retry (queue size: ${this.events.size})`, + ); } } diff --git a/packages/core/src/telemetry/sdk.test.ts b/packages/core/src/telemetry/sdk.test.ts index ee6bc05a7..9274631db 100644 --- a/packages/core/src/telemetry/sdk.test.ts +++ b/packages/core/src/telemetry/sdk.test.ts @@ -46,7 +46,7 @@ describe('Telemetry SDK', () => { }); afterEach(async () => { - await shutdownTelemetry(mockConfig); + await shutdownTelemetry(); }); it('should use gRPC exporters when protocol is grpc', () => { diff --git a/packages/core/src/telemetry/sdk.ts b/packages/core/src/telemetry/sdk.ts index cfaa878ce..3dba2acc4 100644 --- a/packages/core/src/telemetry/sdk.ts +++ b/packages/core/src/telemetry/sdk.ts @@ -36,6 +36,7 @@ import { FileMetricExporter, FileSpanExporter, } from './file-exporters.js'; +import { createDebugLogger } from '../utils/debugLogger.js'; // For troubleshooting, set the log level to DiagLogLevel.DEBUG diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO); @@ -77,6 +78,7 @@ export function initializeTelemetry(config: Config): void { return; } + const debugLogger = createDebugLogger('OTEL'); const resource = resourceFromAttributes({ [SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME, [SemanticResourceAttributes.SERVICE_VERSION]: process.version, @@ -159,37 +161,34 @@ export function initializeTelemetry(config: Config): void { try { sdk.start(); - if (config.getDebugMode()) { - console.log('OpenTelemetry SDK started successfully.'); - } + debugLogger.debug('OpenTelemetry SDK started successfully.'); telemetryInitialized = true; initializeMetrics(config); } catch (error) { - console.error('Error starting OpenTelemetry SDK:', error); + debugLogger.error('Error starting OpenTelemetry SDK:', error); } process.on('SIGTERM', () => { - shutdownTelemetry(config); + shutdownTelemetry(); }); process.on('SIGINT', () => { - shutdownTelemetry(config); + shutdownTelemetry(); }); process.on('exit', () => { - shutdownTelemetry(config); + shutdownTelemetry(); }); } -export async function shutdownTelemetry(config: Config): Promise { +export async function shutdownTelemetry(): Promise { if (!telemetryInitialized || !sdk) { return; } + const debugLogger = createDebugLogger('OTEL'); try { await sdk.shutdown(); - if (config.getDebugMode()) { - console.log('OpenTelemetry SDK shut down successfully.'); - } + debugLogger.debug('OpenTelemetry SDK shut down successfully.'); } catch (error) { - console.error('Error shutting down SDK:', error); + debugLogger.error('Error shutting down SDK:', error); } finally { telemetryInitialized = false; } diff --git a/packages/core/src/telemetry/telemetry.test.ts b/packages/core/src/telemetry/telemetry.test.ts index 6051d226a..ee8003b58 100644 --- a/packages/core/src/telemetry/telemetry.test.ts +++ b/packages/core/src/telemetry/telemetry.test.ts @@ -44,7 +44,7 @@ describe('telemetry', () => { afterEach(async () => { // Ensure we shut down telemetry even if a test fails. if (isTelemetrySdkInitialized()) { - await shutdownTelemetry(mockConfig); + await shutdownTelemetry(); } }); @@ -56,7 +56,7 @@ describe('telemetry', () => { it('should shutdown the telemetry service', async () => { initializeTelemetry(mockConfig); - await shutdownTelemetry(mockConfig); + await shutdownTelemetry(); expect(mockNodeSdk.shutdown).toHaveBeenCalled(); }); diff --git a/packages/core/src/tools/edit.ts b/packages/core/src/tools/edit.ts index ec2572904..f3f3e84ee 100644 --- a/packages/core/src/tools/edit.ts +++ b/packages/core/src/tools/edit.ts @@ -34,6 +34,7 @@ import type { } from './modifiable-tool.js'; import { IdeClient } from '../ide/ide-client.js'; import { safeLiteralReplace } from '../utils/textUtils.js'; +import { createDebugLogger } from '../utils/debugLogger.js'; import { countOccurrences, extractEditSnippet, @@ -41,6 +42,8 @@ import { normalizeEditStrings, } from '../utils/editHelper.js'; +const debugLogger = createDebugLogger('EDIT'); + export function applyReplacement( currentContent: string | null, oldString: string, @@ -256,12 +259,12 @@ class EditToolInvocation implements ToolInvocation { throw error; } const errorMsg = error instanceof Error ? error.message : String(error); - console.log(`Error preparing edit: ${errorMsg}`); + debugLogger.warn(`Error preparing edit: ${errorMsg}`); return false; } if (editData.error) { - console.log(`Error: ${editData.error.display}`); + debugLogger.warn(`Error: ${editData.error.display}`); return false; } diff --git a/packages/core/src/tools/exitPlanMode.ts b/packages/core/src/tools/exitPlanMode.ts index e3c92f924..d8b3df86f 100644 --- a/packages/core/src/tools/exitPlanMode.ts +++ b/packages/core/src/tools/exitPlanMode.ts @@ -15,6 +15,9 @@ import type { FunctionDeclaration } from '@google/genai'; import type { Config } from '../config/config.js'; import { ApprovalMode } from '../config/config.js'; import { ToolDisplayNames, ToolNames } from './tool-names.js'; +import { createDebugLogger } from '../utils/debugLogger.js'; + +const debugLogger = createDebugLogger('EXIT_PLAN_MODE'); export interface ExitPlanModeParams { plan: string; @@ -102,7 +105,7 @@ class ExitPlanModeToolInvocation extends BaseToolInvocation< } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - console.error( + debugLogger.error( `[ExitPlanModeTool] Failed to set approval mode to "${mode}": ${errorMessage}`, ); } @@ -135,7 +138,7 @@ class ExitPlanModeToolInvocation extends BaseToolInvocation< } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - console.error( + debugLogger.error( `[ExitPlanModeTool] Error executing exit_plan_mode: ${errorMessage}`, ); diff --git a/packages/core/src/tools/glob.ts b/packages/core/src/tools/glob.ts index a3b4a5d5a..74af58081 100644 --- a/packages/core/src/tools/glob.ts +++ b/packages/core/src/tools/glob.ts @@ -19,6 +19,9 @@ import { import { ToolErrorType } from './tool-error.js'; import { getErrorMessage } from '../utils/errors.js'; import type { FileDiscoveryService } from '../services/fileDiscoveryService.js'; +import { createDebugLogger } from '../utils/debugLogger.js'; + +const debugLogger = createDebugLogger('GLOB'); const MAX_FILE_COUNT = 100; @@ -203,7 +206,7 @@ class GlobToolInvocation extends BaseToolInvocation< } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - console.error(`GlobLogic execute Error: ${errorMessage}`, error); + debugLogger.error(`GlobLogic execute Error: ${errorMessage}`, error); const rawError = `Error during glob search operation: ${errorMessage}`; return { llmContent: rawError, diff --git a/packages/core/src/tools/grep.ts b/packages/core/src/tools/grep.ts index 934ab57b2..b8ce6d54f 100644 --- a/packages/core/src/tools/grep.ts +++ b/packages/core/src/tools/grep.ts @@ -12,6 +12,9 @@ import { globStream } from 'glob'; import type { ToolInvocation, ToolResult } from './tools.js'; import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js'; import { ToolNames, ToolDisplayNames } from './tool-names.js'; +import { createDebugLogger } from '../utils/debugLogger.js'; + +const debugLogger = createDebugLogger('GREP'); import { resolveAndValidatePath } from '../utils/paths.js'; import { getErrorMessage, isNodeError } from '../utils/errors.js'; import { isGitRepository } from '../utils/gitUtils.js'; @@ -183,7 +186,7 @@ class GrepToolInvocation extends BaseToolInvocation< returnDisplay: displayMessage, }; } catch (error) { - console.error(`Error during GrepLogic execution: ${error}`); + debugLogger.error(`Error during GrepLogic execution: ${error}`); const errorMessage = getErrorMessage(error); return { llmContent: `Error during grep search operation: ${errorMessage}`, @@ -319,7 +322,7 @@ class GrepToolInvocation extends BaseToolInvocation< }); return this.parseGrepOutput(output, absolutePath); } catch (gitError: unknown) { - console.debug( + debugLogger.debug( `GrepLogic: git grep failed: ${getErrorMessage( gitError, )}. Falling back...`, @@ -421,7 +424,7 @@ class GrepToolInvocation extends BaseToolInvocation< }); return this.parseGrepOutput(output, absolutePath); } catch (grepError: unknown) { - console.debug( + debugLogger.debug( `GrepLogic: System grep failed: ${getErrorMessage( grepError, )}. Falling back...`, @@ -430,7 +433,7 @@ class GrepToolInvocation extends BaseToolInvocation< } // --- Strategy 3: Pure JavaScript Fallback --- - console.debug( + debugLogger.debug( 'GrepLogic: Falling back to JavaScript grep implementation.', ); strategyUsed = 'javascript fallback'; @@ -468,7 +471,7 @@ class GrepToolInvocation extends BaseToolInvocation< } catch (readError: unknown) { // Ignore errors like permission denied or file gone during read if (!isNodeError(readError) || readError.code !== 'ENOENT') { - console.debug( + debugLogger.debug( `GrepLogic: Could not read/process ${fileAbsolutePath}: ${getErrorMessage( readError, )}`, @@ -479,7 +482,7 @@ class GrepToolInvocation extends BaseToolInvocation< return allMatches; } catch (error: unknown) { - console.error( + debugLogger.error( `GrepLogic: Error in performGrepSearch (Strategy: ${strategyUsed}): ${getErrorMessage( error, )}`, diff --git a/packages/core/src/tools/ls.test.ts b/packages/core/src/tools/ls.test.ts index 3a3f3a0d9..39a6b7b31 100644 --- a/packages/core/src/tools/ls.test.ts +++ b/packages/core/src/tools/ls.test.ts @@ -253,11 +253,6 @@ describe('LSTool', () => { return originalStat(p); }); - // Spy on console.error to verify it's called - const consoleErrorSpy = vi - .spyOn(console, 'error') - .mockImplementation(() => {}); - const invocation = lsTool.build({ path: tempRootDir }); const result = await invocation.execute(abortSignal); @@ -266,13 +261,7 @@ describe('LSTool', () => { expect(result.llmContent).not.toContain('problematic.txt'); expect(result.returnDisplay).toBe('Listed 1 item(s).'); - // Verify error was logged - expect(consoleErrorSpy).toHaveBeenCalledWith( - expect.stringMatching(/Error accessing.*problematic\.txt/s), - ); - statSpy.mockRestore(); - consoleErrorSpy.mockRestore(); }); }); diff --git a/packages/core/src/tools/ls.ts b/packages/core/src/tools/ls.ts index 6310a6827..b8edbe163 100644 --- a/packages/core/src/tools/ls.ts +++ b/packages/core/src/tools/ls.ts @@ -14,6 +14,9 @@ import type { Config } from '../config/config.js'; import { DEFAULT_FILE_FILTERING_OPTIONS } from '../config/constants.js'; import { ToolErrorType } from './tool-error.js'; import { ToolDisplayNames, ToolNames } from './tool-names.js'; +import { createDebugLogger } from '../utils/debugLogger.js'; + +const debugLogger = createDebugLogger('LS'); /** * Parameters for the LS tool @@ -202,7 +205,7 @@ class LSToolInvocation extends BaseToolInvocation { }); } catch (error) { // Log error internally but don't fail the whole listing - console.error(`Error accessing ${fullPath}: ${error}`); + debugLogger.warn(`Error accessing ${fullPath}: ${error}`); } } diff --git a/packages/core/src/tools/mcp-client.ts b/packages/core/src/tools/mcp-client.ts index efea02ad0..92a0b1893 100644 --- a/packages/core/src/tools/mcp-client.ts +++ b/packages/core/src/tools/mcp-client.ts @@ -38,6 +38,7 @@ import { MCPOAuthTokenStorage } from '../mcp/oauth-token-storage.js'; import { OAuthUtils } from '../mcp/oauth-utils.js'; import type { PromptRegistry } from '../prompts/prompt-registry.js'; import { getErrorMessage } from '../utils/errors.js'; +import { createDebugLogger } from '../utils/debugLogger.js'; import type { Unsubscribe, WorkspaceContext, @@ -54,6 +55,8 @@ export type SendSdkMcpMessage = ( export const MCP_DEFAULT_TIMEOUT_MSEC = 10 * 60 * 1000; // default to 10 minutes +const debugLogger = createDebugLogger('MCP'); + export type DiscoveredMCPPrompt = Prompt & { serverName: string; invoke: (params: Record) => Promise; @@ -910,7 +913,7 @@ export async function connectToMcpServer( } } } catch (fetchError) { - console.debug( + debugLogger.debug( `Failed to fetch www-authenticate header: ${getErrorMessage( fetchError, )}`, @@ -1360,7 +1363,7 @@ export async function createTransport( if (debugMode) { transport.stderr!.on('data', (data) => { const stderrStr = data.toString().trim(); - console.debug(`[DEBUG] [MCP STDERR (${mcpServerName})]: `, stderrStr); + debugLogger.debug(`MCP STDERR (${mcpServerName}):`, stderrStr); }); } return transport; diff --git a/packages/core/src/tools/memoryTool.ts b/packages/core/src/tools/memoryTool.ts index dc6bebef6..fff2d2be1 100644 --- a/packages/core/src/tools/memoryTool.ts +++ b/packages/core/src/tools/memoryTool.ts @@ -24,6 +24,9 @@ import type { ModifyContext, } from './modifiable-tool.js'; import { ToolErrorType } from './tool-error.js'; +import { createDebugLogger } from '../utils/debugLogger.js'; + +const debugLogger = createDebugLogger('MEMORY_TOOL'); const memoryToolSchemaData: FunctionDeclaration = { name: 'save_memory', @@ -361,7 +364,7 @@ Project: ${projectPath} (current project only)`; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - console.error( + debugLogger.error( `[MemoryTool] Error executing save_memory for fact "${fact}" in ${scope}: ${errorMessage}`, ); @@ -435,7 +438,7 @@ export class MemoryTool await fsAdapter.writeFile(memoryFilePath, newContent, 'utf-8'); } catch (error) { - console.error( + debugLogger.error( `[MemoryTool] Error adding memory entry to ${memoryFilePath}:`, error, ); diff --git a/packages/core/src/tools/modifiable-tool.test.ts b/packages/core/src/tools/modifiable-tool.test.ts index 165ecb128..3fa989c80 100644 --- a/packages/core/src/tools/modifiable-tool.test.ts +++ b/packages/core/src/tools/modifiable-tool.test.ts @@ -263,10 +263,7 @@ describe('modifyWithEditor', () => { }); it('should handle temp file cleanup errors gracefully', async () => { - const consoleErrorSpy = vi - .spyOn(console, 'error') - .mockImplementation(() => {}); - vi.spyOn(fs, 'unlinkSync').mockImplementation(() => { + const unlinkSpy = vi.spyOn(fs, 'unlinkSync').mockImplementation(() => { throw new Error('Failed to delete file'); }); @@ -278,12 +275,7 @@ describe('modifyWithEditor', () => { vi.fn(), ); - expect(consoleErrorSpy).toHaveBeenCalledTimes(2); - expect(consoleErrorSpy).toHaveBeenCalledWith( - expect.stringContaining('Error deleting temp diff file:'), - ); - - consoleErrorSpy.mockRestore(); + expect(unlinkSpy).toHaveBeenCalledTimes(2); }); it('should create temp files with correct naming with extension', async () => { diff --git a/packages/core/src/tools/modifiable-tool.ts b/packages/core/src/tools/modifiable-tool.ts index 12fa9a1f9..560e9a847 100644 --- a/packages/core/src/tools/modifiable-tool.ts +++ b/packages/core/src/tools/modifiable-tool.ts @@ -12,12 +12,15 @@ import fs from 'node:fs'; import * as Diff from 'diff'; import { DEFAULT_DIFF_OPTIONS } from './diffOptions.js'; import { isNodeError } from '../utils/errors.js'; +import { createDebugLogger } from '../utils/debugLogger.js'; import type { AnyDeclarativeTool, DeclarativeTool, ToolResult, } from './tools.js'; +const debugLogger = createDebugLogger('MODIFIABLE_TOOL'); + /** * A declarative tool that supports a modify operation. */ @@ -128,13 +131,13 @@ function deleteTempFiles(oldPath: string, newPath: string): void { try { fs.unlinkSync(oldPath); } catch { - console.error(`Error deleting temp diff file: ${oldPath}`); + debugLogger.warn(`Error deleting temp diff file: ${oldPath}`); } try { fs.unlinkSync(newPath); } catch { - console.error(`Error deleting temp diff file: ${newPath}`); + debugLogger.warn(`Error deleting temp diff file: ${newPath}`); } } diff --git a/packages/core/src/tools/ripGrep.ts b/packages/core/src/tools/ripGrep.ts index 9fcd0e3d9..1db231d45 100644 --- a/packages/core/src/tools/ripGrep.ts +++ b/packages/core/src/tools/ripGrep.ts @@ -16,6 +16,9 @@ import { runRipgrep } from '../utils/ripgrepUtils.js'; import { SchemaValidator } from '../utils/schemaValidator.js'; import type { FileFilteringOptions } from '../config/constants.js'; import { DEFAULT_FILE_FILTERING_OPTIONS } from '../config/constants.js'; +import { createDebugLogger } from '../utils/debugLogger.js'; + +const debugLogger = createDebugLogger('RIPGREP'); /** * Parameters for the GrepTool (Simplified) @@ -157,7 +160,7 @@ class GrepToolInvocation extends BaseToolInvocation< returnDisplay: displayMessage, }; } catch (error) { - console.error(`Error during ripgrep search operation: ${error}`); + debugLogger.error('Error during ripgrep search operation:', error); const errorMessage = getErrorMessage(error); return { llmContent: `Error during grep search operation: ${errorMessage}`, diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index a02412626..0d49a5e8e 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -41,6 +41,9 @@ import { isCommandNeedsPermission, stripShellWrapper, } from '../utils/shell-utils.js'; +import { createDebugLogger } from '../utils/debugLogger.js'; + +const debugLogger = createDebugLogger('SHELL'); export const OUTPUT_UPDATE_INTERVAL_MS = 1000; const DEFAULT_FOREGROUND_TIMEOUT_MS = 120000; @@ -271,7 +274,7 @@ export class ShellToolInvocation extends BaseToolInvocation< .filter(Boolean); for (const line of pgrepLines) { if (!/^\d+$/.test(line)) { - console.error(`pgrep: ${line}`); + debugLogger.warn(`pgrep: ${line}`); } const pid = Number(line); if (pid !== result.pid) { @@ -280,7 +283,7 @@ export class ShellToolInvocation extends BaseToolInvocation< } } else { if (!signal.aborted) { - console.error('missing pgrep output'); + debugLogger.warn('missing pgrep output'); } } } @@ -573,7 +576,7 @@ export class ShellTool extends BaseDeclarativeTool< const commandCheck = isCommandAllowed(params.command, this.config); if (!commandCheck.allowed) { if (!commandCheck.reason) { - console.error( + debugLogger.error( 'Unexpected: isCommandAllowed returned false without a reason', ); return `Command is not allowed: ${params.command}`; diff --git a/packages/core/src/tools/skill.test.ts b/packages/core/src/tools/skill.test.ts index e22a062df..7f327be73 100644 --- a/packages/core/src/tools/skill.test.ts +++ b/packages/core/src/tools/skill.test.ts @@ -156,16 +156,12 @@ describe('SkillTool', () => { new Error('Loading failed'), ); - const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - - new SkillTool(config); + const failedSkillTool = new SkillTool(config); await vi.runAllTimersAsync(); - expect(consoleSpy).toHaveBeenCalledWith( - 'Failed to load skills for Skills tool:', - expect.any(Error), + expect(failedSkillTool.description).toContain( + 'No skills are currently configured', ); - consoleSpy.mockRestore(); }); }); @@ -375,10 +371,6 @@ describe('SkillTool', () => { new Error('Loading failed'), ); - const consoleSpy = vi - .spyOn(console, 'error') - .mockImplementation(() => {}); - const params: SkillParams = { skill: 'code-review', }; @@ -391,8 +383,6 @@ describe('SkillTool', () => { const llmText = partToString(result.llmContent); expect(llmText).toContain('Failed to load skill'); expect(llmText).toContain('Loading failed'); - - consoleSpy.mockRestore(); }); it('should not require confirmation', async () => { diff --git a/packages/core/src/tools/skill.ts b/packages/core/src/tools/skill.ts index f1dc1596b..83ae43b33 100644 --- a/packages/core/src/tools/skill.ts +++ b/packages/core/src/tools/skill.ts @@ -12,6 +12,9 @@ import type { SkillManager } from '../skills/skill-manager.js'; import type { SkillConfig } from '../skills/types.js'; import { logSkillLaunch, SkillLaunchEvent } from '../telemetry/index.js'; import path from 'path'; +import { createDebugLogger } from '../utils/debugLogger.js'; + +const debugLogger = createDebugLogger('SKILL'); export interface SkillParams { skill: string; @@ -71,7 +74,7 @@ export class SkillTool extends BaseDeclarativeTool { this.availableSkills = await this.skillManager.listSkills(); this.updateDescriptionAndSchema(); } catch (error) { - console.warn('Failed to load skills for Skills tool:', error); + debugLogger.warn('Failed to load skills for Skills tool:', error); this.availableSkills = []; this.updateDescriptionAndSchema(); } finally { @@ -251,7 +254,7 @@ class SkillToolInvocation extends BaseToolInvocation { } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - console.error(`[SkillsTool] Error using skill: ${errorMessage}`); + debugLogger.error(`[SkillsTool] Error using skill: ${errorMessage}`); // Log failed skill launch logSkillLaunch( diff --git a/packages/core/src/tools/smart-edit.ts b/packages/core/src/tools/smart-edit.ts index ae9d61fbf..08fa2b2d1 100644 --- a/packages/core/src/tools/smart-edit.ts +++ b/packages/core/src/tools/smart-edit.ts @@ -33,6 +33,7 @@ import { IdeClient } from '../ide/ide-client.js'; import { FixLLMEditWithInstruction } from '../utils/llm-edit-fixer.js'; import { applyReplacement } from './edit.js'; import { safeLiteralReplace } from '../utils/textUtils.js'; +import { createDebugLogger } from '../utils/debugLogger.js'; interface ReplacementContext { params: EditToolParams; @@ -47,6 +48,8 @@ interface ReplacementResult { finalNewString: string; } +const debugLogger = createDebugLogger('SMART_EDIT'); + function restoreTrailingNewline( originalContent: string, modifiedContent: string, @@ -571,12 +574,12 @@ class EditToolInvocation implements ToolInvocation { throw error; } const errorMsg = error instanceof Error ? error.message : String(error); - console.log(`Error preparing edit: ${errorMsg}`); + debugLogger.warn(`Error preparing edit: ${errorMsg}`); return false; } if (editData.error) { - console.log(`Error: ${editData.error.display}`); + debugLogger.warn(`Error: ${editData.error.display}`); return false; } diff --git a/packages/core/src/tools/task.test.ts b/packages/core/src/tools/task.test.ts index 40b538801..458b026b6 100644 --- a/packages/core/src/tools/task.test.ts +++ b/packages/core/src/tools/task.test.ts @@ -150,16 +150,12 @@ describe('TaskTool', () => { new Error('Loading failed'), ); - const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - - new TaskTool(config); + const failedTaskTool = new TaskTool(config); await vi.runAllTimersAsync(); - expect(consoleSpy).toHaveBeenCalledWith( - 'Failed to load subagents for Task tool:', - expect.any(Error), + expect(failedTaskTool.description).toContain( + 'No subagents are currently configured', ); - consoleSpy.mockRestore(); }); }); diff --git a/packages/core/src/tools/task.ts b/packages/core/src/tools/task.ts index e8fd64d57..e811dde0d 100644 --- a/packages/core/src/tools/task.ts +++ b/packages/core/src/tools/task.ts @@ -34,6 +34,7 @@ import type { SubAgentErrorEvent, SubAgentApprovalRequestEvent, } from '../subagents/subagent-events.js'; +import { createDebugLogger } from '../utils/debugLogger.js'; export interface TaskParams { description: string; @@ -41,6 +42,8 @@ export interface TaskParams { subagent_type: string; } +const debugLogger = createDebugLogger('TASK'); + /** * Task tool that enables primary agents to delegate tasks to specialized subagents. * The tool dynamically loads available subagents and includes them in its description @@ -103,7 +106,7 @@ export class TaskTool extends BaseDeclarativeTool { this.availableSubagents = await this.subagentManager.listSubagents(); this.updateDescriptionAndSchema(); } catch (error) { - console.warn('Failed to load subagents for Task tool:', error); + debugLogger.warn('Failed to load subagents for Task tool:', error); this.availableSubagents = []; this.updateDescriptionAndSchema(); } finally { @@ -550,7 +553,7 @@ class TaskToolInvocation extends BaseToolInvocation { } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - console.error(`[TaskTool] Error running subagent: ${errorMessage}`); + debugLogger.error(`[TaskTool] Error running subagent: ${errorMessage}`); const errorDisplay: TaskResultDisplay = { ...this.currentDisplay!, diff --git a/packages/core/src/tools/todoWrite.ts b/packages/core/src/tools/todoWrite.ts index 23deb2603..f99fbccdd 100644 --- a/packages/core/src/tools/todoWrite.ts +++ b/packages/core/src/tools/todoWrite.ts @@ -15,6 +15,9 @@ import * as process from 'process'; import { QWEN_DIR } from '../utils/paths.js'; import type { Config } from '../config/config.js'; import { ToolDisplayNames, ToolNames } from './tool-names.js'; +import { createDebugLogger } from '../utils/debugLogger.js'; + +const debugLogger = createDebugLogger('TODO_WRITE'); export interface TodoItem { id: string; @@ -370,7 +373,7 @@ ${todosJson}. Continue on with the tasks at hand if applicable. } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - console.error( + debugLogger.error( `[TodoWriteTool] Error executing todo_write: ${errorMessage}`, ); diff --git a/packages/core/src/tools/tool-registry.test.ts b/packages/core/src/tools/tool-registry.test.ts index 2bcc3e16a..ceb52e4d9 100644 --- a/packages/core/src/tools/tool-registry.test.ts +++ b/packages/core/src/tools/tool-registry.test.ts @@ -118,10 +118,6 @@ describe('ToolRegistry', () => { } as fs.Stats); config = new Config(baseConfigParams); toolRegistry = new ToolRegistry(config); - vi.spyOn(console, 'warn').mockImplementation(() => {}); - vi.spyOn(console, 'error').mockImplementation(() => {}); - vi.spyOn(console, 'debug').mockImplementation(() => {}); - vi.spyOn(console, 'log').mockImplementation(() => {}); mockMcpClientConnect.mockReset().mockResolvedValue(undefined); mockStdioTransportClose.mockReset(); diff --git a/packages/core/src/tools/tool-registry.ts b/packages/core/src/tools/tool-registry.ts index 4db7bd789..2a241e66f 100644 --- a/packages/core/src/tools/tool-registry.ts +++ b/packages/core/src/tools/tool-registry.ts @@ -23,9 +23,12 @@ import { parse } from 'shell-quote'; import { ToolErrorType } from './tool-error.js'; import { safeJsonStringify } from '../utils/safeJsonStringify.js'; import type { EventEmitter } from 'node:events'; +import { createDebugLogger } from '../utils/debugLogger.js'; type ToolParams = Record; +const debugLogger = createDebugLogger('TOOL_REGISTRY'); + class DiscoveredToolInvocation extends BaseToolInvocation< ToolParams, ToolResult @@ -198,7 +201,7 @@ export class ToolRegistry { tool = tool.asFullyQualifiedTool(); } else { // Decide on behavior: throw error, log warning, or allow overwrite - console.warn( + debugLogger.warn( `Tool with name "${tool.name}" is already registered. Overwriting.`, ); } @@ -356,8 +359,10 @@ export class ToolRegistry { } if (code !== 0) { - console.error(`Command failed with code ${code}`); - console.error(stderr); + debugLogger.error( + `Tool discovery command failed with code ${code}`, + ); + debugLogger.error(stderr); return reject( new Error(`Tool discovery command failed with exit code ${code}`), ); @@ -390,7 +395,7 @@ export class ToolRegistry { // register each function as a tool for (const func of functions) { if (!func.name) { - console.warn('Discovered a tool with no name. Skipping.'); + debugLogger.warn('Discovered a tool with no name. Skipping.'); continue; } const parameters = @@ -409,7 +414,7 @@ export class ToolRegistry { ); } } catch (e) { - console.error(`Tool discovery command "${discoveryCmd}" failed:`, e); + debugLogger.error(`Tool discovery command "${discoveryCmd}" failed:`, e); throw e; } } diff --git a/packages/core/src/tools/web-fetch.test.ts b/packages/core/src/tools/web-fetch.test.ts index d84d721fb..cfa7b593d 100644 --- a/packages/core/src/tools/web-fetch.test.ts +++ b/packages/core/src/tools/web-fetch.test.ts @@ -36,6 +36,7 @@ describe('WebFetchTool', () => { setApprovalMode: vi.fn(), getProxy: vi.fn(), getGeminiClient: mockGetGeminiClient, + getSessionId: vi.fn(() => 'test-session-id'), } as unknown as Config; }); diff --git a/packages/core/src/tools/web-fetch.ts b/packages/core/src/tools/web-fetch.ts index 7797659ed..51eb410d5 100644 --- a/packages/core/src/tools/web-fetch.ts +++ b/packages/core/src/tools/web-fetch.ts @@ -24,6 +24,7 @@ import { } from './tools.js'; import { DEFAULT_QWEN_MODEL } from '../config/models.js'; import { ToolNames, ToolDisplayNames } from './tool-names.js'; +import { createDebugLogger, type DebugLogger } from '../utils/debugLogger.js'; const URL_FETCH_TIMEOUT_MS = 10000; const MAX_CONTENT_LENGTH = 100000; @@ -49,11 +50,14 @@ class WebFetchToolInvocation extends BaseToolInvocation< WebFetchToolParams, ToolResult > { + private readonly debugLogger: DebugLogger; + constructor( private readonly config: Config, params: WebFetchToolParams, ) { super(params); + this.debugLogger = createDebugLogger('WEB_FETCH'); } private async executeDirectFetch(signal: AbortSignal): Promise { @@ -64,22 +68,24 @@ class WebFetchToolInvocation extends BaseToolInvocation< url = url .replace('github.com', 'raw.githubusercontent.com') .replace('/blob/', '/'); - console.debug( + this.debugLogger.debug( `[WebFetchTool] Converted GitHub blob URL to raw URL: ${url}`, ); } try { - console.debug(`[WebFetchTool] Fetching content from: ${url}`); + this.debugLogger.debug(`[WebFetchTool] Fetching content from: ${url}`); const response = await fetchWithTimeout(url, URL_FETCH_TIMEOUT_MS); if (!response.ok) { const errorMessage = `Request failed with status code ${response.status} ${response.statusText}`; - console.error(`[WebFetchTool] ${errorMessage}`); + this.debugLogger.error(`[WebFetchTool] ${errorMessage}`); throw new Error(errorMessage); } - console.debug(`[WebFetchTool] Successfully fetched content from ${url}`); + this.debugLogger.debug( + `[WebFetchTool] Successfully fetched content from ${url}`, + ); const html = await response.text(); const textContent = convert(html, { wordwrap: false, @@ -89,7 +95,7 @@ class WebFetchToolInvocation extends BaseToolInvocation< ], }).substring(0, MAX_CONTENT_LENGTH); - console.debug( + this.debugLogger.debug( `[WebFetchTool] Converted HTML to text (${textContent.length} characters)`, ); @@ -102,7 +108,7 @@ I have fetched the content from ${this.params.url}. Please use the following con ${textContent} ---`; - console.debug( + this.debugLogger.debug( `[WebFetchTool] Processing content with prompt: "${this.params.prompt}"`, ); @@ -114,7 +120,7 @@ ${textContent} ); const resultText = getResponseText(result) || ''; - console.debug( + this.debugLogger.debug( `[WebFetchTool] Successfully processed content from ${this.params.url}`, ); @@ -125,7 +131,7 @@ ${textContent} } catch (e) { const error = e as Error; const errorMessage = `Error during fetch for ${url}: ${error.message}`; - console.error(`[WebFetchTool] ${errorMessage}`, error); + this.debugLogger.error(`[WebFetchTool] ${errorMessage}`, error); return { llmContent: `Error: ${errorMessage}`, returnDisplay: `Error: ${errorMessage}`, @@ -171,11 +177,11 @@ ${textContent} const isPrivate = isPrivateIp(this.params.url); if (isPrivate) { - console.debug( + this.debugLogger.debug( `[WebFetchTool] Private IP detected for ${this.params.url}, using direct fetch`, ); } else { - console.debug( + this.debugLogger.debug( `[WebFetchTool] Public URL detected for ${this.params.url}, using direct fetch`, ); } diff --git a/packages/core/src/tools/web-search/index.ts b/packages/core/src/tools/web-search/index.ts index b9aa83c53..e364eb646 100644 --- a/packages/core/src/tools/web-search/index.ts +++ b/packages/core/src/tools/web-search/index.ts @@ -18,6 +18,7 @@ import { ToolErrorType } from '../tool-error.js'; import type { Config } from '../../config/config.js'; import { ApprovalMode } from '../../config/config.js'; import { getErrorMessage } from '../../utils/errors.js'; +import { createDebugLogger } from '../../utils/debugLogger.js'; import { buildContentWithSources } from './utils.js'; import { TavilyProvider } from './providers/tavily-provider.js'; import { GoogleProvider } from './providers/google-provider.js'; @@ -32,6 +33,8 @@ import type { } from './types.js'; import { ToolNames, ToolDisplayNames } from '../tool-names.js'; +const debugLogger = createDebugLogger('WEB_SEARCH'); + class WebSearchToolInvocation extends BaseToolInvocation< WebSearchToolParams, WebSearchToolResult @@ -110,7 +113,7 @@ class WebSearchToolInvocation extends BaseToolInvocation< providers.set(config.type, provider); } } catch (error) { - console.warn(`Failed to create ${config.type} provider:`, error); + debugLogger.warn(`Failed to create ${config.type} provider:`, error); } } @@ -255,7 +258,7 @@ class WebSearchToolInvocation extends BaseToolInvocation< }; } catch (error: unknown) { const errorMessage = `Error during web search: ${getErrorMessage(error)}`; - console.error(errorMessage, error); + debugLogger.error(errorMessage, error); return { llmContent: errorMessage, returnDisplay: 'Error performing web search.', diff --git a/packages/core/src/tools/write-file.ts b/packages/core/src/tools/write-file.ts index c13c95539..997fcd61f 100644 --- a/packages/core/src/tools/write-file.ts +++ b/packages/core/src/tools/write-file.ts @@ -38,6 +38,9 @@ import { FileOperationEvent } from '../telemetry/types.js'; import { FileOperation } from '../telemetry/metrics.js'; import { getSpecificMimeType } from '../utils/fileUtils.js'; import { getLanguageFromFilePath } from '../utils/language-detection.js'; +import { createDebugLogger } from '../utils/debugLogger.js'; + +const debugLogger = createDebugLogger('WRITE_FILE'); /** * Parameters for the WriteFile tool @@ -329,7 +332,7 @@ class WriteFileToolInvocation extends BaseToolInvocation< // Include stack trace in debug mode for better troubleshooting if (this.config.getDebugMode() && error.stack) { - console.error('Write file error stack:', error.stack); + debugLogger.debug('Write file error stack:', error.stack); } } else if (error instanceof Error) { errorMsg = `Error writing to file: ${error.message}`; diff --git a/packages/core/src/utils/debugLogger.test.ts b/packages/core/src/utils/debugLogger.test.ts index b063064a3..af7d04f48 100644 --- a/packages/core/src/utils/debugLogger.test.ts +++ b/packages/core/src/utils/debugLogger.test.ts @@ -9,6 +9,7 @@ import { createDebugLogger, isDebugLoggingDegraded, resetDebugLoggingState, + setDebugLogSession, type DebugLogSession, } from './debugLogger.js'; import { promises as fs } from 'node:fs'; @@ -31,20 +32,31 @@ describe('debugLogger', () => { getSessionId: () => 'test-session-123', }; + const previousDebugLogFileEnv = process.env['QWEN_DEBUG_LOG_FILE']; + beforeEach(() => { + process.env['QWEN_DEBUG_LOG_FILE'] = '1'; vi.clearAllMocks(); vi.useFakeTimers(); vi.setSystemTime(new Date('2026-01-24T10:30:00.000Z')); resetDebugLoggingState(); + setDebugLogSession(mockSession); }); afterEach(() => { vi.useRealTimers(); + setDebugLogSession(null); + if (previousDebugLogFileEnv === undefined) { + delete process.env['QWEN_DEBUG_LOG_FILE']; + } else { + process.env['QWEN_DEBUG_LOG_FILE'] = previousDebugLogFileEnv; + } }); describe('createDebugLogger', () => { - it('returns no-op logger when session is null', () => { - const logger = createDebugLogger(null); + it('returns no-op logger when session is unset', () => { + setDebugLogSession(null); + const logger = createDebugLogger(); // Should not throw logger.debug('test'); logger.info('test'); @@ -53,14 +65,8 @@ describe('debugLogger', () => { expect(fs.appendFile).not.toHaveBeenCalled(); }); - it('returns no-op logger when session is undefined', () => { - const logger = createDebugLogger(undefined); - logger.debug('test'); - expect(fs.appendFile).not.toHaveBeenCalled(); - }); - it('writes debug log with correct format', async () => { - const logger = createDebugLogger(mockSession); + const logger = createDebugLogger(); logger.debug('Hello world'); await vi.runAllTimersAsync(); @@ -76,7 +82,7 @@ describe('debugLogger', () => { }); it('writes log with tag when provided', async () => { - const logger = createDebugLogger(mockSession, 'STARTUP'); + const logger = createDebugLogger('STARTUP'); logger.info('Server started'); await vi.runAllTimersAsync(); @@ -89,7 +95,7 @@ describe('debugLogger', () => { }); it('writes different log levels correctly', async () => { - const logger = createDebugLogger(mockSession); + const logger = createDebugLogger(); logger.debug('debug message'); logger.info('info message'); @@ -106,7 +112,7 @@ describe('debugLogger', () => { }); it('formats multiple arguments', async () => { - const logger = createDebugLogger(mockSession); + const logger = createDebugLogger(); logger.debug('Count:', 42, 'items'); await vi.runAllTimersAsync(); @@ -119,7 +125,7 @@ describe('debugLogger', () => { }); it('formats Error objects with stack trace', async () => { - const logger = createDebugLogger(mockSession); + const logger = createDebugLogger(); const error = new Error('Something went wrong'); logger.error('Failed:', error); @@ -131,7 +137,7 @@ describe('debugLogger', () => { }); it('formats objects using util.inspect', async () => { - const logger = createDebugLogger(mockSession); + const logger = createDebugLogger(); logger.debug('Data:', { foo: 'bar', count: 123 }); await vi.runAllTimersAsync(); @@ -150,7 +156,7 @@ describe('debugLogger', () => { it('returns true when mkdir fails', async () => { vi.mocked(fs.mkdir).mockRejectedValueOnce(new Error('Permission denied')); - const logger = createDebugLogger(mockSession); + const logger = createDebugLogger(); logger.debug('test'); await vi.runAllTimersAsync(); @@ -161,7 +167,7 @@ describe('debugLogger', () => { it('returns true when appendFile fails', async () => { vi.mocked(fs.appendFile).mockRejectedValueOnce(new Error('Disk full')); - const logger = createDebugLogger(mockSession); + const logger = createDebugLogger(); logger.debug('test'); await vi.runAllTimersAsync(); @@ -174,7 +180,7 @@ describe('debugLogger', () => { new Error('Temporary error'), ); - const logger = createDebugLogger(mockSession); + const logger = createDebugLogger(); logger.debug('first write fails'); await vi.runAllTimersAsync(); @@ -194,7 +200,7 @@ describe('debugLogger', () => { it('resets the degraded state', async () => { vi.mocked(fs.appendFile).mockRejectedValueOnce(new Error('Disk full')); - const logger = createDebugLogger(mockSession); + const logger = createDebugLogger(); logger.debug('test'); await vi.runAllTimersAsync(); diff --git a/packages/core/src/utils/debugLogger.ts b/packages/core/src/utils/debugLogger.ts index fc3a31c6b..8c9e60eae 100644 --- a/packages/core/src/utils/debugLogger.ts +++ b/packages/core/src/utils/debugLogger.ts @@ -5,6 +5,7 @@ */ import { promises as fs } from 'node:fs'; +import { AsyncLocalStorage } from 'node:async_hooks'; import util from 'node:util'; import { Storage } from '../config/storage.js'; @@ -23,6 +24,19 @@ export interface DebugLogger { let ensureDebugDirPromise: Promise | null = null; let hasWriteFailure = false; +let globalSession: DebugLogSession | null = null; +const sessionContext = new AsyncLocalStorage(); + +function isDebugLogFileEnabled(): boolean { + const value = process.env['QWEN_DEBUG_LOG_FILE']; + if (!value) return true; + const normalized = value.trim().toLowerCase(); + return !['0', 'false', 'off', 'no'].includes(normalized); +} + +function getActiveSession(): DebugLogSession | null { + return sessionContext.getStore() ?? globalSession; +} function ensureDebugDirExists(): Promise { if (!ensureDebugDirPromise) { @@ -68,6 +82,10 @@ function writeLog( tag: string | undefined, args: unknown[], ): void { + if (!isDebugLogFileEnabled()) { + return; + } + const sessionId = session.getSessionId(); const logFilePath = Storage.getDebugLogPath(sessionId); const message = formatArgs(args); @@ -98,28 +116,58 @@ export function resetDebugLoggingState(): void { } /** - * Creates a debug logger that writes to a session-specific log file. + * Sets the process-wide debug log session used by createDebugLogger(). * - * Log files are written to `~/.qwen/debug/.txt`. - * Write failures are silently ignored to avoid disrupting the user. + * This is the default session used when there is no async-local session bound + * via runWithDebugLogSession(). */ -export function createDebugLogger( +export function setDebugLogSession( session: DebugLogSession | null | undefined, - tag?: string, -): DebugLogger { - if (!session) { - return { - debug: () => {}, - info: () => {}, - warn: () => {}, - error: () => {}, - }; - } +) { + globalSession = session ?? null; +} +/** + * Runs a function with a session bound to the current async context. + * + * This is optional; createDebugLogger() falls back to the process-wide session + * set via setDebugLogSession(). + */ +export function runWithDebugLogSession( + session: DebugLogSession, + fn: () => T, +): T { + return sessionContext.run(session, fn); +} + +/** + * Creates a debug logger that writes to the current debug log session. + * + * Session resolution order: + * 1) async-local session (runWithDebugLogSession) + * 2) process-wide session (setDebugLogSession) + */ +export function createDebugLogger(tag?: string): DebugLogger { return { - debug: (...args: unknown[]) => writeLog(session, 'DEBUG', tag, args), - info: (...args: unknown[]) => writeLog(session, 'INFO', tag, args), - warn: (...args: unknown[]) => writeLog(session, 'WARN', tag, args), - error: (...args: unknown[]) => writeLog(session, 'ERROR', tag, args), + debug: (...args: unknown[]) => { + const session = getActiveSession(); + if (!session) return; + writeLog(session, 'DEBUG', tag, args); + }, + info: (...args: unknown[]) => { + const session = getActiveSession(); + if (!session) return; + writeLog(session, 'INFO', tag, args); + }, + warn: (...args: unknown[]) => { + const session = getActiveSession(); + if (!session) return; + writeLog(session, 'WARN', tag, args); + }, + error: (...args: unknown[]) => { + const session = getActiveSession(); + if (!session) return; + writeLog(session, 'ERROR', tag, args); + }, }; } diff --git a/packages/core/src/utils/memoryDiscovery.test.ts b/packages/core/src/utils/memoryDiscovery.test.ts index f66418cdc..8842e0311 100644 --- a/packages/core/src/utils/memoryDiscovery.test.ts +++ b/packages/core/src/utils/memoryDiscovery.test.ts @@ -85,7 +85,6 @@ describe('loadServerHierarchicalMemory', () => { const { fileCount } = await loadServerHierarchicalMemory( cwd, [], - false, new FileDiscoveryService(projectRoot), [], false, // untrusted @@ -109,7 +108,6 @@ describe('loadServerHierarchicalMemory', () => { const { fileCount, memoryContent } = await loadServerHierarchicalMemory( cwd, [], - false, new FileDiscoveryService(projectRoot), [], false, // untrusted @@ -124,7 +122,6 @@ describe('loadServerHierarchicalMemory', () => { const result = await loadServerHierarchicalMemory( cwd, [], - false, new FileDiscoveryService(projectRoot), [], DEFAULT_FOLDER_TRUST, @@ -145,7 +142,6 @@ describe('loadServerHierarchicalMemory', () => { const result = await loadServerHierarchicalMemory( cwd, [], - false, new FileDiscoveryService(projectRoot), [], DEFAULT_FOLDER_TRUST, @@ -169,7 +165,6 @@ describe('loadServerHierarchicalMemory', () => { const result = await loadServerHierarchicalMemory( cwd, [], - false, new FileDiscoveryService(projectRoot), [], DEFAULT_FOLDER_TRUST, @@ -197,7 +192,6 @@ describe('loadServerHierarchicalMemory', () => { const result = await loadServerHierarchicalMemory( cwd, [], - false, new FileDiscoveryService(projectRoot), [], DEFAULT_FOLDER_TRUST, @@ -222,7 +216,6 @@ describe('loadServerHierarchicalMemory', () => { const result = await loadServerHierarchicalMemory( cwd, [], - false, new FileDiscoveryService(projectRoot), [], DEFAULT_FOLDER_TRUST, @@ -248,7 +241,6 @@ describe('loadServerHierarchicalMemory', () => { const result = await loadServerHierarchicalMemory( cwd, [], - false, new FileDiscoveryService(projectRoot), [], DEFAULT_FOLDER_TRUST, @@ -273,7 +265,6 @@ describe('loadServerHierarchicalMemory', () => { const result = await loadServerHierarchicalMemory( cwd, [], - false, new FileDiscoveryService(projectRoot), [], DEFAULT_FOLDER_TRUST, @@ -311,7 +302,6 @@ describe('loadServerHierarchicalMemory', () => { const result = await loadServerHierarchicalMemory( cwd, [], - false, new FileDiscoveryService(projectRoot), [], DEFAULT_FOLDER_TRUST, @@ -333,7 +323,6 @@ describe('loadServerHierarchicalMemory', () => { const result = await loadServerHierarchicalMemory( cwd, [], - false, new FileDiscoveryService(projectRoot), [extensionFilePath], DEFAULT_FOLDER_TRUST, @@ -357,7 +346,6 @@ describe('loadServerHierarchicalMemory', () => { const result = await loadServerHierarchicalMemory( cwd, [includedDir], - false, new FileDiscoveryService(projectRoot), [], DEFAULT_FOLDER_TRUST, @@ -389,7 +377,6 @@ describe('loadServerHierarchicalMemory', () => { const result = await loadServerHierarchicalMemory( cwd, createdFiles.map((f) => path.dirname(f)), - false, new FileDiscoveryService(projectRoot), [], DEFAULT_FOLDER_TRUST, @@ -422,7 +409,6 @@ describe('loadServerHierarchicalMemory', () => { const result = await loadServerHierarchicalMemory( parentDir, [childDir, parentDir], // Deliberately include duplicates - false, new FileDiscoveryService(projectRoot), [], DEFAULT_FOLDER_TRUST, diff --git a/packages/core/src/utils/memoryDiscovery.ts b/packages/core/src/utils/memoryDiscovery.ts index ab45ef7e2..f0e86d903 100644 --- a/packages/core/src/utils/memoryDiscovery.ts +++ b/packages/core/src/utils/memoryDiscovery.ts @@ -12,19 +12,9 @@ import { getAllGeminiMdFilenames } from '../tools/memoryTool.js'; import type { FileDiscoveryService } from '../services/fileDiscoveryService.js'; import { processImports } from './memoryImportProcessor.js'; import { QWEN_DIR } from './paths.js'; +import { createDebugLogger } from './debugLogger.js'; -// Simple console logger, similar to the one previously in CLI's config.ts -// TODO: Integrate with a more robust server-side logger if available/appropriate. -const logger = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - debug: (...args: any[]) => - console.debug('[DEBUG] [MemoryDiscovery]', ...args), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - warn: (...args: any[]) => console.warn('[WARN] [MemoryDiscovery]', ...args), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - error: (...args: any[]) => - console.error('[ERROR] [MemoryDiscovery]', ...args), -}; +const logger = createDebugLogger('MEMORY_DISCOVERY'); interface GeminiFileContent { filePath: string; @@ -79,7 +69,6 @@ async function getGeminiMdFilePathsInternal( currentWorkingDirectory: string, includeDirectoriesToReadGemini: readonly string[], userHomePath: string, - debugMode: boolean, fileService: FileDiscoveryService, extensionContextFilePaths: string[] = [], folderTrust: boolean, @@ -100,7 +89,6 @@ async function getGeminiMdFilePathsInternal( getGeminiMdFilePathsInternalForEachDir( dir, userHomePath, - debugMode, fileService, extensionContextFilePaths, folderTrust, @@ -128,7 +116,6 @@ async function getGeminiMdFilePathsInternal( async function getGeminiMdFilePathsInternalForEachDir( dir: string, userHomePath: string, - debugMode: boolean, fileService: FileDiscoveryService, extensionContextFilePaths: string[] = [], folderTrust: boolean, @@ -148,10 +135,9 @@ async function getGeminiMdFilePathsInternalForEachDir( try { await fs.access(globalMemoryPath, fsSync.constants.R_OK); allPaths.add(globalMemoryPath); - if (debugMode) - logger.debug( - `Found readable global ${geminiMdFilename}: ${globalMemoryPath}`, - ); + logger.debug( + `Found readable global ${geminiMdFilename}: ${globalMemoryPath}`, + ); } catch { // It's okay if it's not found. } @@ -167,10 +153,9 @@ async function getGeminiMdFilePathsInternalForEachDir( await fs.access(homeContextPath, fsSync.constants.R_OK); if (homeContextPath !== globalMemoryPath) { allPaths.add(homeContextPath); - if (debugMode) - logger.debug( - `Found readable home ${geminiMdFilename}: ${homeContextPath}`, - ); + logger.debug( + `Found readable home ${geminiMdFilename}: ${homeContextPath}`, + ); } } catch { // Not found, which is okay @@ -179,14 +164,12 @@ async function getGeminiMdFilePathsInternalForEachDir( // FIX: Only perform the workspace search (upward scan from CWD to project root) // if a valid currentWorkingDirectory is provided and it's not the home directory. const resolvedCwd = path.resolve(dir); - if (debugMode) - logger.debug( - `Searching for ${geminiMdFilename} starting from CWD: ${resolvedCwd}`, - ); + logger.debug( + `Searching for ${geminiMdFilename} starting from CWD: ${resolvedCwd}`, + ); const projectRoot = await findProjectRoot(resolvedCwd); - if (debugMode) - logger.debug(`Determined project root: ${projectRoot ?? 'None'}`); + logger.debug(`Determined project root: ${projectRoot ?? 'None'}`); const upwardPaths: string[] = []; let currentDir = resolvedCwd; @@ -226,18 +209,16 @@ async function getGeminiMdFilePathsInternalForEachDir( const finalPaths = Array.from(allPaths); - if (debugMode) - logger.debug( - `Final ordered ${getAllGeminiMdFilenames()} paths to read: ${JSON.stringify( - finalPaths, - )}`, - ); + logger.debug( + `Final ordered ${getAllGeminiMdFilenames()} paths to read: ${JSON.stringify( + finalPaths, + )}`, + ); return finalPaths; } async function readGeminiMdFiles( filePaths: string[], - debugMode: boolean, importFormat: 'flat' | 'tree' = 'tree', ): Promise { // Process files in parallel with concurrency limit to prevent EMFILE errors @@ -255,15 +236,13 @@ async function readGeminiMdFiles( const processedResult = await processImports( content, path.dirname(filePath), - debugMode, undefined, undefined, importFormat, ); - if (debugMode) - logger.debug( - `Successfully read and processed imports: ${filePath} (Length: ${processedResult.content.length})`, - ); + logger.debug( + `Successfully read and processed imports: ${filePath} (Length: ${processedResult.content.length})`, + ); return { filePath, content: processedResult.content }; } catch (error: unknown) { @@ -276,7 +255,7 @@ async function readGeminiMdFiles( `Warning: Could not read ${getAllGeminiMdFilenames()} file at ${filePath}. Error: ${message}`, ); } - if (debugMode) logger.debug(`Failed to read: ${filePath}`); + logger.debug(`Failed to read: ${filePath}`); return { filePath, content: null }; // Still include it with null content } }, @@ -333,16 +312,14 @@ export interface LoadServerHierarchicalMemoryResponse { export async function loadServerHierarchicalMemory( currentWorkingDirectory: string, includeDirectoriesToReadGemini: readonly string[], - debugMode: boolean, fileService: FileDiscoveryService, extensionContextFilePaths: string[] = [], folderTrust: boolean, importFormat: 'flat' | 'tree' = 'tree', ): Promise { - if (debugMode) - logger.debug( - `Loading server hierarchical memory for CWD: ${currentWorkingDirectory} (importFormat: ${importFormat})`, - ); + logger.debug( + `Loading server hierarchical memory for CWD: ${currentWorkingDirectory} (importFormat: ${importFormat})`, + ); // For the server, homedir() refers to the server process's home. // This is consistent with how MemoryTool already finds the global path. @@ -351,20 +328,15 @@ export async function loadServerHierarchicalMemory( currentWorkingDirectory, includeDirectoriesToReadGemini, userHomePath, - debugMode, fileService, extensionContextFilePaths, folderTrust, ); if (filePaths.length === 0) { - if (debugMode) logger.debug('No QWEN.md files found in hierarchy.'); + logger.debug('No QWEN.md files found in hierarchy.'); return { memoryContent: '', fileCount: 0 }; } - const contentsWithPaths = await readGeminiMdFiles( - filePaths, - debugMode, - importFormat, - ); + const contentsWithPaths = await readGeminiMdFiles(filePaths, importFormat); // Pass CWD for relative path display in concatenated content const combinedInstructions = concatenateInstructions( contentsWithPaths, @@ -378,14 +350,12 @@ export async function loadServerHierarchicalMemory( memoryFilenames.has(path.basename(item.filePath)), ).length; - if (debugMode) - logger.debug( - `Combined instructions length: ${combinedInstructions.length}`, - ); - if (debugMode && combinedInstructions.length > 0) + logger.debug(`Combined instructions length: ${combinedInstructions.length}`); + if (combinedInstructions.length > 0) { logger.debug( `Combined instructions (snippet): ${combinedInstructions.substring(0, 500)}...`, ); + } return { memoryContent: combinedInstructions, fileCount, // Only count the context files diff --git a/packages/core/src/utils/memoryImportProcessor.test.ts b/packages/core/src/utils/memoryImportProcessor.test.ts index c7d23da0d..4dd7e3b02 100644 --- a/packages/core/src/utils/memoryImportProcessor.test.ts +++ b/packages/core/src/utils/memoryImportProcessor.test.ts @@ -119,7 +119,7 @@ describe('memoryImportProcessor', () => { mockedFs.access.mockResolvedValue(undefined); mockedFs.readFile.mockResolvedValue(importedContent); - const result = await processImports(content, basePath, true); + const result = await processImports(content, basePath); // Use marked to find HTML comments (import markers) const comments = findMarkdownComments(result.content); @@ -153,7 +153,7 @@ describe('memoryImportProcessor', () => { mockedFs.access.mockResolvedValue(undefined); mockedFs.readFile.mockResolvedValue(importedContent); - const result = await processImports(content, basePath, true); + const result = await processImports(content, basePath); // Use marked to find import comments const comments = findMarkdownComments(result.content); @@ -200,7 +200,7 @@ describe('memoryImportProcessor', () => { currentFile: testPath('test', 'path', 'main.md'), // Simulate we're processing main.md }; - const result = await processImports(content, basePath, true, importState); + const result = await processImports(content, basePath, importState); // The circular import should be detected when processing the nested import expect(result.content).toContain( @@ -219,7 +219,7 @@ describe('memoryImportProcessor', () => { }), ); - const result = await processImports(content, basePath, true); + const result = await processImports(content, basePath); // Content should be preserved as-is when file doesn't exist expect(result.content).toBe(content); @@ -236,16 +236,13 @@ describe('memoryImportProcessor', () => { Object.assign(new Error('Permission denied'), { code: 'EACCES' }), ); - const result = await processImports(content, basePath, true); + const result = await processImports(content, basePath); // Should show error comment for non-ENOENT errors expect(result.content).toContain( '', ); - expect(console.error).toHaveBeenCalledWith( - '[ERROR] [ImportProcessor]', - 'Failed to import ./permission-denied.md: Permission denied', - ); + expect(console.error).not.toHaveBeenCalled(); }); it('should respect max depth limit', async () => { @@ -262,12 +259,9 @@ describe('memoryImportProcessor', () => { currentDepth: 1, }; - const result = await processImports(content, basePath, true, importState); + const result = await processImports(content, basePath, importState); - expect(console.warn).toHaveBeenCalledWith( - '[WARN] [ImportProcessor]', - 'Maximum import depth (1) reached. Stopping import processing.', - ); + expect(console.warn).not.toHaveBeenCalled(); expect(result.content).toBe(content); }); @@ -282,7 +276,7 @@ describe('memoryImportProcessor', () => { .mockResolvedValueOnce(nestedContent) .mockResolvedValueOnce(innerContent); - const result = await processImports(content, basePath, true); + const result = await processImports(content, basePath); expect(result.content).toContain(''); expect(result.content).toContain(''); @@ -297,7 +291,7 @@ describe('memoryImportProcessor', () => { mockedFs.access.mockResolvedValue(undefined); mockedFs.readFile.mockResolvedValue(importedContent); - const result = await processImports(content, basePath, true); + const result = await processImports(content, basePath); expect(result.content).toContain( '', @@ -315,7 +309,7 @@ describe('memoryImportProcessor', () => { .mockResolvedValueOnce(firstContent) .mockResolvedValueOnce(secondContent); - const result = await processImports(content, basePath, true); + const result = await processImports(content, basePath); expect(result.content).toContain(''); expect(result.content).toContain(''); @@ -343,7 +337,6 @@ describe('memoryImportProcessor', () => { const result = await processImports( content, basePath, - true, undefined, projectRoot, ); @@ -383,7 +376,6 @@ describe('memoryImportProcessor', () => { const result = await processImports( content, basePath, - true, undefined, projectRoot, ); @@ -432,7 +424,6 @@ describe('memoryImportProcessor', () => { const result = await processImports( content, basePath, - true, undefined, projectRoot, ); @@ -458,7 +449,6 @@ describe('memoryImportProcessor', () => { const result = await processImports( content, basePath, - true, undefined, projectRoot, ); @@ -488,7 +478,7 @@ describe('memoryImportProcessor', () => { ); // 中文路径 doesn't exist mockedFs.readFile.mockResolvedValue(importedContent); - const result = await processImports(content, basePath, true); + const result = await processImports(content, basePath); // Should import valid.md expect(result.content).toContain(importedContent); @@ -509,7 +499,6 @@ describe('memoryImportProcessor', () => { const result = await processImports( content, basePath, - true, undefined, projectRoot, ); @@ -533,7 +522,6 @@ describe('memoryImportProcessor', () => { const result = await processImports( content, basePath, - true, undefined, projectRoot, ); @@ -548,7 +536,6 @@ describe('memoryImportProcessor', () => { const result = await processImports( content, basePath, - true, undefined, projectRoot, ); @@ -571,7 +558,7 @@ describe('memoryImportProcessor', () => { .mockResolvedValueOnce(simpleContent) .mockResolvedValueOnce(innerContent); - const result = await processImports(content, basePath, true); + const result = await processImports(content, basePath); // Use marked to find and validate import comments const comments = findMarkdownComments(result.content); @@ -639,7 +626,6 @@ describe('memoryImportProcessor', () => { const result = await processImports( content, basePath, - true, undefined, projectRoot, 'flat', @@ -715,7 +701,6 @@ describe('memoryImportProcessor', () => { const result = await processImports( content, basePath, - true, // followImports undefined, // allowedPaths projectRoot, 'flat', // outputFormat @@ -747,7 +732,6 @@ describe('memoryImportProcessor', () => { const result = await processImports( content, basePath, - true, undefined, projectRoot, 'flat', diff --git a/packages/core/src/utils/memoryImportProcessor.ts b/packages/core/src/utils/memoryImportProcessor.ts index 7b535969d..0b48a3a1b 100644 --- a/packages/core/src/utils/memoryImportProcessor.ts +++ b/packages/core/src/utils/memoryImportProcessor.ts @@ -8,18 +8,9 @@ import * as fs from 'node:fs/promises'; import * as path from 'node:path'; import { isSubpath } from './paths.js'; import { marked, type Token } from 'marked'; +import { createDebugLogger } from './debugLogger.js'; -// Simple console logger for import processing -const logger = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - debug: (...args: any[]) => - console.debug('[DEBUG] [ImportProcessor]', ...args), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - warn: (...args: any[]) => console.warn('[WARN] [ImportProcessor]', ...args), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - error: (...args: any[]) => - console.error('[ERROR] [ImportProcessor]', ...args), -}; +const logger = createDebugLogger('IMPORT_PROCESSOR'); /** * Interface for tracking import processing state to prevent circular imports @@ -201,7 +192,6 @@ function findCodeRegions(content: string): Array<[number, number]> { * Supports @path/to/file syntax for importing content from other files * @param content - The content to process for imports * @param basePath - The directory path where the current file is located - * @param debugMode - Whether to enable debug logging * @param importState - State tracking for circular import prevention * @param projectRoot - The project root directory for allowed directories * @param importFormat - The format of the import tree @@ -210,7 +200,6 @@ function findCodeRegions(content: string): Array<[number, number]> { export async function processImports( content: string, basePath: string, - debugMode: boolean = false, importState: ImportState = { processedFiles: new Set(), maxDepth: 5, @@ -224,11 +213,9 @@ export async function processImports( } if (importState.currentDepth >= importState.maxDepth) { - if (debugMode) { - logger.warn( - `Maximum import depth (${importState.maxDepth}) reached. Stopping import processing.`, - ); - } + logger.warn( + `Maximum import depth (${importState.maxDepth}) reached. Stopping import processing.`, + ); return { content, importTree: { path: importState.currentFile || 'unknown' }, @@ -306,7 +293,7 @@ export async function processImports( } catch (error) { // If file doesn't exist, silently skip this import (it's not a real import) // Only log warnings for other types of errors - if (!isFileNotFoundError(error) && debugMode) { + if (!isFileNotFoundError(error)) { logger.warn( `Failed to import ${fullPath}: ${hasMessage(error) ? error.message : 'Unknown error'}`, ); @@ -377,7 +364,6 @@ export async function processImports( const imported = await processImports( fileContent, path.dirname(fullPath), - debugMode, newImportState, projectRoot, importFormat, diff --git a/packages/core/src/utils/retry.ts b/packages/core/src/utils/retry.ts index 9e9412af1..4a0672ae9 100644 --- a/packages/core/src/utils/retry.ts +++ b/packages/core/src/utils/retry.ts @@ -10,6 +10,9 @@ import { isQwenQuotaExceededError, isQwenThrottlingError, } from './quotaErrorDetection.js'; +import { createDebugLogger } from './debugLogger.js'; + +const debugLogger = createDebugLogger('RETRY'); export interface HttpError extends Error { status?: number; @@ -141,7 +144,7 @@ export async function retryWithBackoff( consecutive429Count = 0; } - console.debug('consecutive429Count', consecutive429Count); + debugLogger.debug('consecutive429Count', consecutive429Count); // Check if we've exhausted retries or shouldn't retry if (attempt >= maxAttempts || !shouldRetryOnError(error as Error)) { diff --git a/packages/core/test-setup.ts b/packages/core/test-setup.ts index 520e618ad..8d2e7f74a 100644 --- a/packages/core/test-setup.ts +++ b/packages/core/test-setup.ts @@ -11,6 +11,12 @@ if (process.env['NO_COLOR'] !== undefined) { import { setSimulate429 } from './src/utils/testUtils.js'; +// Avoid writing per-session debug log files during tests. +// Unit tests can opt-in by overriding this env var. +if (process.env['QWEN_DEBUG_LOG_FILE'] === undefined) { + process.env['QWEN_DEBUG_LOG_FILE'] = '0'; +} + // Disable 429 simulation globally for all tests setSimulate429(false);