refactor: migrate runtime console.log calls to structured logger (#397)
This commit is contained in:
@@ -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
33
scripts/no-console.sh
Executable 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."
|
||||
@@ -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'}]`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'}]`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user