import { writeFileSync } from 'node:fs'; import type { Command } from 'commander'; import type { LogCategory, LogLevel, LogTier } from './agent-logs.js'; interface FilterOptions { agent?: string; level?: string; category?: string; tier?: string; limit?: string; db?: string; } function parseLimit(raw: string | undefined, defaultVal = 50): number { if (!raw) return defaultVal; const n = parseInt(raw, 10); return Number.isFinite(n) && n > 0 ? n : defaultVal; } function buildQuery(opts: FilterOptions) { return { ...(opts.agent ? { sessionId: opts.agent } : {}), ...(opts.level ? { level: opts.level as LogLevel } : {}), ...(opts.category ? { category: opts.category as LogCategory } : {}), ...(opts.tier ? { tier: opts.tier as LogTier } : {}), limit: parseLimit(opts.limit), }; } async function openDb(connectionString: string) { const { createDb } = await import('@mosaicstack/db'); return createDb(connectionString); } function resolveConnectionString(opts: FilterOptions): string | undefined { return opts.db ?? process.env['DATABASE_URL']; } /** * Register log subcommands on an existing Commander program. * This avoids cross-package Commander version mismatches by using the * caller's Command instance directly. */ export function registerLogCommand(parent: Command): void { const log = parent.command('log').description('Query and manage agent logs'); // ─── tail ─────────────────────────────────────────────────────────────── log .command('tail') .description('Tail recent agent logs') .option('--agent ', 'Filter by agent/session ID') .option('--level ', 'Filter by log level (debug|info|warn|error)') .option('--category ', 'Filter by category (decision|tool_use|learning|error|general)') .option('--tier ', 'Filter by tier (hot|warm|cold)') .option('--limit ', 'Number of logs to return (default 50)', '50') .option('--db ', 'Database connection string (or set DATABASE_URL)') .action(async (opts: FilterOptions) => { const connStr = resolveConnectionString(opts); if (!connStr) { console.error('Database connection required: use --db or set DATABASE_URL'); process.exit(1); } const handle = await openDb(connStr); try { const { createLogService } = await import('./log-service.js'); const svc = createLogService(handle.db); const query = buildQuery(opts); const logs = await svc.logs.query(query); if (logs.length === 0) { console.log('No logs found.'); return; } for (const entry of logs) { const ts = new Date(entry.createdAt).toISOString(); console.log(`[${ts}] [${entry.level}] [${entry.category}] ${entry.content}`); } } finally { await handle.close(); } }); // ─── search ───────────────────────────────────────────────────────────── log .command('search ') .description('Full-text search over agent logs') .option('--agent ', 'Filter by agent/session ID') .option('--level ', 'Filter by log level (debug|info|warn|error)') .option('--category ', 'Filter by category (decision|tool_use|learning|error|general)') .option('--tier ', 'Filter by tier (hot|warm|cold)') .option('--limit ', 'Number of logs to return (default 50)', '50') .option('--db ', 'Database connection string (or set DATABASE_URL)') .action(async (query: string, opts: FilterOptions) => { const connStr = resolveConnectionString(opts); if (!connStr) { console.error('Database connection required: use --db or set DATABASE_URL'); process.exit(1); } const handle = await openDb(connStr); try { const { createLogService } = await import('./log-service.js'); const svc = createLogService(handle.db); const baseQuery = buildQuery(opts); const logs = await svc.logs.query(baseQuery); const lowerQ = query.toLowerCase(); const matched = logs.filter( (e) => e.content.toLowerCase().includes(lowerQ) || (e.metadata != null && JSON.stringify(e.metadata).toLowerCase().includes(lowerQ)), ); if (matched.length === 0) { console.log('No matching logs found.'); return; } for (const entry of matched) { const ts = new Date(entry.createdAt).toISOString(); console.log(`[${ts}] [${entry.level}] [${entry.category}] ${entry.content}`); } } finally { await handle.close(); } }); // ─── export ───────────────────────────────────────────────────────────── log .command('export ') .description('Export matching logs to an NDJSON file') .option('--agent ', 'Filter by agent/session ID') .option('--level ', 'Filter by log level (debug|info|warn|error)') .option('--category ', 'Filter by category (decision|tool_use|learning|error|general)') .option('--tier ', 'Filter by tier (hot|warm|cold)') .option('--limit ', 'Number of logs to export (default 50)', '50') .option('--db ', 'Database connection string (or set DATABASE_URL)') .action(async (outputPath: string, opts: FilterOptions) => { const connStr = resolveConnectionString(opts); if (!connStr) { console.error('Database connection required: use --db or set DATABASE_URL'); process.exit(1); } const handle = await openDb(connStr); try { const { createLogService } = await import('./log-service.js'); const svc = createLogService(handle.db); const query = buildQuery(opts); const logs = await svc.logs.query(query); const ndjson = logs.map((e) => JSON.stringify(e)).join('\n'); writeFileSync(outputPath, ndjson, 'utf8'); console.log(`Exported ${logs.length} log(s) to ${outputPath}`); } finally { await handle.close(); } }); // ─── level ────────────────────────────────────────────────────────────── log .command('level ') .description('Set runtime log level for the connected log service') .action((level: string) => { void level; console.log( 'Runtime log level adjustment is not supported in current mode (DB-backed log service).', ); process.exitCode = 0; }); }