#!/usr/bin/env node import { writeFile } from 'node:fs/promises'; import { parseArgs } from 'node:util'; import { queryAuditLogs } from '../src/audit/export.js'; import { renderPhase0BaselineMarkdown, summarizePhase0Baseline, type Phase0BaselineSummaryOptions, } from '../src/audit/phase0BaselineSummary.js'; const DEFAULT_EVENT_TYPES = ['run.state', 'run.cancel', 'reaction.match', 'reaction.skip'] as const; function usage(): string { return [ 'Usage: node --import tsx/esm scripts/summarize-phase0-baseline.ts --audit [options]', '', 'Options:', ' --audit Path to audit.log (required)', ' --since Start time filter', ' --until End time filter', ' --session Restrict to session IDs', ' --channel Restrict to channels', ' --sender Restrict to senders', ' --source Restrict to sources', ' --max-sessions Limit session rows in output (default: 20)', ' --max-channels Limit channel rows in output (default: 20)', ' --max-skip-reasons Limit skip reason rows in output (default: 10)', ' --format Output format (default: markdown)', ' --out Write output to file instead of stdout', ].join('\n'); } function parseTime(value: string | undefined, flag: string): number | undefined { if (!value) { return undefined; } if (/^\d+$/.test(value)) { const asNumber = Number(value); if (Number.isFinite(asNumber)) { return asNumber; } } const parsed = Date.parse(value); if (!Number.isFinite(parsed)) { throw new Error(`Invalid ${flag} value "${value}". Use ISO-8601 or epoch milliseconds.`); } return parsed; } function parseCsv(value: string | undefined): string[] | undefined { if (!value) { return undefined; } const values = value .split(',') .map((item) => item.trim()) .filter((item) => item.length > 0); return values.length > 0 ? values : undefined; } function parseOptionalNumber(raw: string | undefined, flag: string): number | undefined { if (!raw) { return undefined; } const parsed = Number(raw); if (!Number.isFinite(parsed)) { throw new Error(`Invalid ${flag} value "${raw}". Expected a number.`); } return parsed; } function parseSources(raw: string | undefined): Array<'gateway' | 'channel'> | undefined { const values = parseCsv(raw); if (!values) { return undefined; } const parsed: Array<'gateway' | 'channel'> = []; for (const value of values) { if (value === 'gateway' || value === 'channel') { parsed.push(value); continue; } throw new Error(`Invalid source "${value}".`); } return parsed; } async function main(): Promise { const { values } = parseArgs({ options: { audit: { type: 'string' }, since: { type: 'string' }, until: { type: 'string' }, session: { type: 'string' }, channel: { type: 'string' }, sender: { type: 'string' }, source: { type: 'string' }, 'max-sessions': { type: 'string' }, 'max-channels': { type: 'string' }, 'max-skip-reasons': { type: 'string' }, format: { type: 'string' }, out: { type: 'string' }, help: { type: 'boolean', short: 'h' }, }, strict: true, allowPositionals: false, }); if (values.help) { process.stdout.write(`${usage()}\n`); return; } if (!values.audit) { throw new Error('--audit is required.'); } const format = values.format ?? 'markdown'; if (format !== 'markdown' && format !== 'json') { throw new Error(`Invalid --format value "${format}".`); } const summaryOptions: Phase0BaselineSummaryOptions = { sessionIds: parseCsv(values.session), channels: parseCsv(values.channel), senders: parseCsv(values.sender), sources: parseSources(values.source), maxSessions: parseOptionalNumber(values['max-sessions'], '--max-sessions') ?? 20, maxChannels: parseOptionalNumber(values['max-channels'], '--max-channels') ?? 20, maxSkipReasons: parseOptionalNumber(values['max-skip-reasons'], '--max-skip-reasons') ?? 10, }; const startTime = parseTime(values.since, '--since'); const endTime = parseTime(values.until, '--until'); const events = await queryAuditLogs(values.audit, { start_time: startTime, end_time: endTime, event_types: [...DEFAULT_EVENT_TYPES], }); const summary = summarizePhase0Baseline(events, summaryOptions); const output = format === 'json' ? JSON.stringify({ generated_at: new Date().toISOString(), event_count: events.length, filters: { since_ms: startTime, until_ms: endTime, }, options: summaryOptions, summary, }, null, 2) : renderPhase0BaselineMarkdown(summary, summaryOptions); if (values.out) { await writeFile(values.out, `${output}\n`, 'utf-8'); } else { process.stdout.write(`${output}\n`); } } main().catch((error) => { const message = error instanceof Error ? error.message : String(error); process.stderr.write(`${message}\n\n${usage()}\n`); process.exitCode = 1; });