refactor: migrate runtime console.log calls to structured logger (#397)

This commit is contained in:
Cameron
2026-02-26 10:16:38 -08:00
committed by GitHub
parent 673cb5100e
commit d283f837ac
10 changed files with 80 additions and 41 deletions

View File

@@ -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",

33
scripts/no-console.sh Executable file
View File

@@ -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."

View File

@@ -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'}]`;
}
}

View File

@@ -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
}
}

View File

@@ -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'}]`;
}
}

View File

@@ -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}`);
}
}

View File

@@ -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);
}
}

View File

@@ -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)

View File

@@ -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<Record<string, unknown>> = [];
for await (const msg of messagesPage) {

View File

@@ -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<str
throw new Error('Failed to split audio into chunks');
}
console.log(`[Transcription] Split into ${chunkFiles.length} chunks`);
log.info(`Split into ${chunkFiles.length} chunks`);
const transcriptions: string[] = [];
for (let i = 0; i < chunkFiles.length; i++) {
const chunkPath = join(tempDir, chunkFiles[i]);
const chunkBuffer = readFileSync(chunkPath);
console.log(`[Transcription] Transcribing chunk ${i + 1}/${chunkFiles.length} (${(chunkBuffer.length / 1024).toFixed(0)}KB)`);
log.info(`Transcribing chunk ${i + 1}/${chunkFiles.length} (${(chunkBuffer.length / 1024).toFixed(0)}KB)`);
const text = await attemptTranscription(chunkBuffer, chunkFiles[i]);
if (text.trim()) {
transcriptions.push(text.trim());
@@ -165,7 +167,7 @@ async function transcribeInChunks(audioBuffer: Buffer, ext: string): Promise<str
}
const combined = transcriptions.join(' ');
console.log(`[Transcription] Combined ${transcriptions.length} chunks into ${combined.length} chars`);
log.info(`Combined ${transcriptions.length} chunks into ${combined.length} chars`);
return combined;
} finally {
try {
@@ -199,19 +201,19 @@ export async function transcribeAudio(
if (NEEDS_CONVERSION.includes(ext)) {
const mapped = FORMAT_MAP[ext];
if (mapped) {
console.log(`[Transcription] Trying .${ext} as .${mapped} (no conversion)`);
log.info(`Trying .${ext} as .${mapped} (no conversion)`);
finalFilename = filename.replace(/\.[^.]+$/, `.${mapped}`);
try {
const text = await attemptTranscription(finalBuffer, finalFilename);
return { success: true, text };
} catch {
console.log(`[Transcription] Rename approach failed for .${ext}`);
log.info(`Rename approach failed for .${ext}`);
}
}
if (isFfmpegAvailable()) {
console.log(`[Transcription] Converting .${ext} → .mp3 with ffmpeg`);
log.info(`Converting .${ext} → .mp3 with ffmpeg`);
finalBuffer = convertAudioToMp3(audioBuffer, ext);
finalFilename = filename.replace(/\.[^.]+$/, '.mp3');
} else {
@@ -226,7 +228,7 @@ export async function transcribeAudio(
// Check file size and chunk if needed
if (finalBuffer.length > 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 };
}