From d283f837aca6a39f44fce81f9643905d2dcca60a Mon Sep 17 00:00:00 2001 From: Cameron Date: Thu, 26 Feb 2026 10:16:38 -0800 Subject: [PATCH] refactor: migrate runtime console.log calls to structured logger (#397) --- package.json | 1 + scripts/no-console.sh | 33 ++++++++++++++++++++++++++ src/channels/slack.ts | 6 ++--- src/channels/telegram.ts | 2 +- src/channels/whatsapp/inbound/media.ts | 8 +++---- src/core/bot.ts | 30 +++++++++++------------ src/looms/loom-loader.ts | 11 +++++---- src/main.ts | 2 +- src/tools/letta-api.ts | 10 ++++---- src/transcription/mistral.ts | 18 +++++++------- 10 files changed, 80 insertions(+), 41 deletions(-) create mode 100755 scripts/no-console.sh diff --git a/package.json b/package.json index c25c153..869ebe5 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "start": "node dist/main.js", "test": "vitest", "test:run": "vitest run --exclude 'e2e/**'", + "lint:console": "bash scripts/no-console.sh", "test:e2e": "vitest run e2e/", "skills": "tsx src/cli.ts skills", "skills:list": "tsx src/cli.ts skills list", diff --git a/scripts/no-console.sh b/scripts/no-console.sh new file mode 100755 index 0000000..d6a380d --- /dev/null +++ b/scripts/no-console.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Fail if runtime source code uses console.* directly. +# Runtime code should use createLogger() from src/logger.ts instead. +# +# Excluded: +# - CLI commands (src/cli*, onboard, setup) -- user-facing terminal output +# - Test files (*.test.ts, mock-*) -- test output +# - banner.ts -- ASCII art display +# - whatsapp/session.ts installConsoleFilters() -- intentional console interception (Baileys noise filter) +# - JSDoc examples (lines starting with ' *') + +set -euo pipefail + +hits=$(grep -rn 'console\.\(log\|error\|warn\|info\)(' src/ --include='*.ts' \ + | grep -v '/cli' \ + | grep -v '\.test\.' \ + | grep -v 'mock-channel' \ + | grep -v 'banner\.ts' \ + | grep -v 'session\.ts.*\(originalLog\|originalError\|originalWarn\|console\.\(log\|error\|warn\) =\)' \ + | grep -v 'setup\.ts' \ + | grep -v 'onboard\.ts' \ + | grep -v 'slack-wizard\.ts' \ + | grep -v 'cron/cli\.ts' \ + | grep -v ' \* ' \ + || true) + +if [ -n "$hits" ]; then + echo "ERROR: Found console.* calls in runtime code (use createLogger instead):" + echo "$hits" + exit 1 +fi + +echo "OK: No console.* in runtime code." diff --git a/src/channels/slack.ts b/src/channels/slack.ts index ae46bf8..2c2742e 100644 --- a/src/channels/slack.ts +++ b/src/channels/slack.ts @@ -200,14 +200,14 @@ export class SlackAdapter implements ChannelAdapter { const result = await transcribeAudio(buffer, audioFile.name || `audio.${ext}`); if (result.success && result.text) { - console.log(`[Slack] Transcribed audio: "${result.text.slice(0, 50)}..."`); + log.info(`Transcribed audio: "${result.text.slice(0, 50)}..."`); text = (text ? text + '\n' : '') + `[Voice message]: ${result.text}`; } else { - console.error(`[Slack] Transcription failed: ${result.error}`); + log.error(`Transcription failed: ${result.error}`); text = (text ? text + '\n' : '') + `[Voice message - transcription failed: ${result.error}]`; } } catch (error) { - console.error('[Slack] Error transcribing audio:', error); + log.error('Error transcribing audio:', error); text = (text ? text + '\n' : '') + `[Voice message - error: ${error instanceof Error ? error.message : 'unknown error'}]`; } } diff --git a/src/channels/telegram.ts b/src/channels/telegram.ts index 5a8de18..bc4214e 100644 --- a/src/channels/telegram.ts +++ b/src/channels/telegram.ts @@ -515,7 +515,7 @@ export class TelegramAdapter implements ChannelAdapter { lastMessageId = String(result.message_id); continue; } catch (e) { - console.warn(`[Telegram] ${msg.parseMode} send failed, falling back to default:`, e); + log.warn(`${msg.parseMode} send failed, falling back to default:`, e); // Fall through to default conversion path } } diff --git a/src/channels/whatsapp/inbound/media.ts b/src/channels/whatsapp/inbound/media.ts index f1e718f..90be2b1 100644 --- a/src/channels/whatsapp/inbound/media.ts +++ b/src/channels/whatsapp/inbound/media.ts @@ -171,7 +171,7 @@ export async function collectAttachments(params: { text: 'Voice messages require a transcription API key. See: https://github.com/letta-ai/lettabot#voice-messages' }); } catch (sendError) { - console.error('[WhatsApp] Failed to send transcription error message:', sendError); + log.error('Failed to send transcription error message:', sendError); } // Don't forward error to agent - return early const caption = mediaMessage.caption as string | undefined; @@ -191,14 +191,14 @@ export async function collectAttachments(params: { const result = await transcribeAudio(buffer, name); if (result.success && result.text) { - console.log(`[WhatsApp] Transcribed voice message: "${result.text.slice(0, 50)}..."`); + log.info(`Transcribed voice message: "${result.text.slice(0, 50)}..."`); voiceTranscription = `[Voice message]: ${result.text}`; } else { - console.error(`[WhatsApp] Transcription failed: ${result.error}`); + log.error(`Transcription failed: ${result.error}`); voiceTranscription = `[Voice message - transcription failed: ${result.error}]`; } } catch (error) { - console.error('[WhatsApp] Error transcribing voice message:', error); + log.error('Error transcribing voice message:', error); voiceTranscription = `[Voice message - error: ${error instanceof Error ? error.message : 'unknown error'}]`; } } diff --git a/src/core/bot.ts b/src/core/bot.ts index 3303d9d..6717379 100644 --- a/src/core/bot.ts +++ b/src/core/bot.ts @@ -723,7 +723,7 @@ export class LettaBot implements AgentSession { if (directive.type === 'send-file') { if (typeof adapter.sendFile !== 'function') { - console.warn(`[Bot] Directive send-file skipped: ${adapter.name} does not support sendFile`); + log.warn(`Directive send-file skipped: ${adapter.name} does not support sendFile`); continue; } @@ -733,7 +733,7 @@ export class LettaBot implements AgentSession { const allowedDir = resolve(this.config.workingDir, allowedDirConfig); const resolvedPath = resolve(this.config.workingDir, directive.path); if (!await isPathAllowed(resolvedPath, allowedDir)) { - console.warn(`[Bot] Directive send-file blocked: ${directive.path} is outside allowed directory ${allowedDir}`); + log.warn(`Directive send-file blocked: ${directive.path} is outside allowed directory ${allowedDir}`); continue; } @@ -741,7 +741,7 @@ export class LettaBot implements AgentSession { try { await access(resolvedPath, constants.R_OK); } catch { - console.warn(`[Bot] Directive send-file skipped: file not found or not readable at ${directive.path}`); + log.warn(`Directive send-file skipped: file not found or not readable at ${directive.path}`); continue; } @@ -750,11 +750,11 @@ export class LettaBot implements AgentSession { try { const fileStat = await stat(resolvedPath); if (fileStat.size > maxSize) { - console.warn(`[Bot] Directive send-file blocked: ${directive.path} is ${fileStat.size} bytes (max: ${maxSize})`); + log.warn(`Directive send-file blocked: ${directive.path} is ${fileStat.size} bytes (max: ${maxSize})`); continue; } } catch { - console.warn(`[Bot] Directive send-file skipped: could not stat ${directive.path}`); + log.warn(`Directive send-file skipped: could not stat ${directive.path}`); continue; } @@ -767,20 +767,20 @@ export class LettaBot implements AgentSession { threadId, }); acted = true; - console.log(`[Bot] Directive: sent file ${resolvedPath}`); + log.info(`Directive: sent file ${resolvedPath}`); // Optional cleanup: delete file after successful send. // Only honored when sendFileCleanup is enabled in config (defense-in-depth). if (directive.cleanup && this.config.sendFileCleanup) { try { await unlink(resolvedPath); - console.warn(`[Bot] Directive: cleaned up ${resolvedPath}`); + log.warn(`Directive: cleaned up ${resolvedPath}`); } catch (cleanupErr) { - console.warn('[Bot] Directive send-file cleanup failed:', cleanupErr instanceof Error ? cleanupErr.message : cleanupErr); + log.warn('Directive send-file cleanup failed:', cleanupErr instanceof Error ? cleanupErr.message : cleanupErr); } } } catch (err) { - console.warn('[Bot] Directive send-file failed:', err instanceof Error ? err.message : err); + log.warn('Directive send-file failed:', err instanceof Error ? err.message : err); } } @@ -1703,7 +1703,7 @@ export class LettaBot implements AgentSession { if (directives.length === 0) return; if (suppressDelivery) { - console.log(`[Bot] Listening mode: skipped ${directives.length} directive(s)`); + log.info(`Listening mode: skipped ${directives.length} directive(s)`); return; } @@ -1783,7 +1783,7 @@ export class LettaBot implements AgentSession { // not a substitute for an assistant response. Error handling and retry must // still fire even if reasoning was displayed. } catch (err) { - console.warn('[Bot] Failed to send reasoning display:', err instanceof Error ? err.message : err); + log.warn('Failed to send reasoning display:', err instanceof Error ? err.message : err); } } reasoningBuffer = ''; @@ -1813,7 +1813,7 @@ export class LettaBot implements AgentSession { const text = this.formatToolCallDisplay(streamMsg); await adapter.sendMessage({ chatId: msg.chatId, text, threadId: msg.threadId }); } catch (err) { - console.warn('[Bot] Failed to send tool call display:', err instanceof Error ? err.message : err); + log.warn('Failed to send tool call display:', err instanceof Error ? err.message : err); } } } else if (streamMsg.type === 'tool_result') { @@ -1952,7 +1952,7 @@ export class LettaBot implements AgentSession { lastErrorDetail?.message?.toLowerCase().includes('waiting for approval'); if (isApprovalConflict && !retried && this.store.agentId) { if (retryConvId) { - console.log('[Bot] Approval conflict detected -- attempting targeted recovery...'); + log.info('Approval conflict detected -- attempting targeted recovery...'); this.invalidateSession(retryConvKey); session = null; clearInterval(typingInterval); @@ -1960,10 +1960,10 @@ export class LettaBot implements AgentSession { this.store.agentId, retryConvId, true /* deepScan */ ); if (convResult.recovered) { - console.log(`[Bot] Approval recovery succeeded (${convResult.details}), retrying message...`); + log.info(`Approval recovery succeeded (${convResult.details}), retrying message...`); return this.processMessage(msg, adapter, true); } - console.warn(`[Bot] Approval recovery failed: ${convResult.details}`); + log.warn(`Approval recovery failed: ${convResult.details}`); } } diff --git a/src/looms/loom-loader.ts b/src/looms/loom-loader.ts index 9a09b43..fe47568 100644 --- a/src/looms/loom-loader.ts +++ b/src/looms/loom-loader.ts @@ -18,6 +18,9 @@ import { readdirSync, readFileSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { createLogger } from '../logger.js'; + +const log = createLogger('Looms'); export interface LoomMetadata { name: string; @@ -40,7 +43,7 @@ export function parseLoomFile(content: string, filename: string): Loom | null { // Also handle --- at the very start or with \r\n const altIndex = content.indexOf('\r\n---\r\n'); if (altIndex === -1) { - console.warn(`[Loom] Skipping ${filename}: no --- separator found`); + log.warn(`Skipping ${filename}: no --- separator found`); return null; } return parseLoomContent( @@ -69,7 +72,7 @@ function parseLoomContent(header: string, art: string, filename: string): Loom | } if (!meta.name || !meta.author) { - console.warn(`[Loom] Skipping ${filename}: missing required name or author in metadata`); + log.warn(`Skipping ${filename}: missing required name or author in metadata`); return null; } @@ -106,7 +109,7 @@ export function loadAllLooms(loomsDir?: string): Loom[] { try { files = readdirSync(dir).filter(f => f.endsWith('.txt')); } catch (err) { - console.warn(`[Loom] Could not read looms directory: ${dir}`); + log.warn(`Could not read looms directory: ${dir}`); return looms; } @@ -118,7 +121,7 @@ export function loadAllLooms(loomsDir?: string): Loom[] { looms.push(loom); } } catch (err) { - console.warn(`[Loom] Error reading ${file}:`, err); + log.warn(`Error reading ${file}:`, err); } } diff --git a/src/main.ts b/src/main.ts index 72794a5..0caf6da 100644 --- a/src/main.ts +++ b/src/main.ts @@ -600,7 +600,7 @@ async function main() { // Log memfs config (from either YAML or env var) if (resolvedMemfs !== undefined) { const source = agentConfig.features?.memfs !== undefined ? '' : ' (from LETTABOT_MEMFS env)'; - console.log(`[Agent:${agentConfig.name}] memfs: ${resolvedMemfs ? 'enabled' : 'disabled'}${source}`); + log.info(`Agent ${agentConfig.name}: memfs ${resolvedMemfs ? 'enabled' : 'disabled'}${source}`); } // Apply explicit agent ID from config (before store verification) diff --git a/src/tools/letta-api.ts b/src/tools/letta-api.ts index 704af5d..9c3d798 100644 --- a/src/tools/letta-api.ts +++ b/src/tools/letta-api.ts @@ -492,7 +492,7 @@ export async function getLatestRunError( if (conversationId && typeof run.conversation_id === 'string' && run.conversation_id !== conversationId) { - console.warn('[Letta API] Latest run lookup returned a different conversation, skipping enrichment'); + log.warn('Latest run lookup returned a different conversation, skipping enrichment'); return null; } @@ -506,10 +506,10 @@ export async function getLatestRunError( const isApprovalError = detail.toLowerCase().includes('waiting for approval') || detail.toLowerCase().includes('approve or deny'); - console.log(`[Letta API] Latest run error: ${detail.slice(0, 150)}${isApprovalError ? ' [approval]' : ''}`); + log.info(`Latest run error: ${detail.slice(0, 150)}${isApprovalError ? ' [approval]' : ''}`); return { message: detail, stopReason, isApprovalError }; } catch (e) { - console.warn('[Letta API] Failed to fetch latest run error:', e instanceof Error ? e.message : e); + log.warn('Failed to fetch latest run error:', e instanceof Error ? e.message : e); return null; } } @@ -538,7 +538,7 @@ async function listActiveConversationRunIds( } return runIds; } catch (e) { - console.warn('[Letta API] Failed to list active conversation runs:', e instanceof Error ? e.message : e); + log.warn('Failed to list active conversation runs:', e instanceof Error ? e.message : e); return []; } } @@ -639,7 +639,7 @@ export async function recoverOrphanedConversationApproval( // List recent messages from the conversation to find orphaned approvals. // Default: 50 (fast path). Deep scan: 500 (for conversations with many approvals). const scanLimit = deepScan ? 500 : 50; - console.log(`[Letta API] Scanning ${scanLimit} messages for orphaned approvals...`); + log.info(`Scanning ${scanLimit} messages for orphaned approvals...`); const messagesPage = await client.conversations.messages.list(conversationId, { limit: scanLimit }); const messages: Array> = []; for await (const msg of messagesPage) { diff --git a/src/transcription/mistral.ts b/src/transcription/mistral.ts index 0644b9d..6c51832 100644 --- a/src/transcription/mistral.ts +++ b/src/transcription/mistral.ts @@ -6,12 +6,14 @@ */ import { loadConfig } from '../config/index.js'; +import { createLogger } from '../logger.js'; import { execSync } from 'node:child_process'; import { writeFileSync, readFileSync, unlinkSync, mkdirSync, readdirSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import type { TranscriptionResult } from './openai.js'; +const log = createLogger('Mistral'); const MAX_FILE_SIZE = 20 * 1024 * 1024; const CHUNK_DURATION_SECONDS = 600; @@ -83,7 +85,7 @@ function convertAudioToMp3(audioBuffer: Buffer, inputExt: string): Buffer { timeout: 30000, }); const converted = readFileSync(outputPath); - console.log(`[Transcription] Converted ${audioBuffer.length} bytes → ${converted.length} bytes`); + log.info(`Converted ${audioBuffer.length} bytes → ${converted.length} bytes`); return converted; } finally { try { unlinkSync(inputPath); } catch {} @@ -151,13 +153,13 @@ async function transcribeInChunks(audioBuffer: Buffer, ext: string): Promise MAX_FILE_SIZE) { const finalExt = finalFilename.split('.').pop()?.toLowerCase() || 'ogg'; - console.log(`[Transcription] File too large (${(finalBuffer.length / 1024 / 1024).toFixed(1)}MB), splitting into chunks`); + log.info(`File too large (${(finalBuffer.length / 1024 / 1024).toFixed(1)}MB), splitting into chunks`); const text = await transcribeInChunks(finalBuffer, finalExt); return { success: true, text }; }