import { calloutsFromSections, deliveryPreview, failedSection, isDryRun, longDate, metricsFromSections, notifyBody, reportMarkdown, resolveDate, resolveOutput, resolveSources, resolveTimezone, } from './format.ts' import { calendarSection } from './sources/calendar.ts' import { healthSection } from './sources/health.ts' import { homeSection } from './sources/home.ts' import { inboxSection } from './sources/inbox.ts' import { weatherSection } from './sources/weather.ts' import { deliverReport } from './output.ts' import { loadConfig, readReport, saveReport } from './storage.ts' import { assertNever, type BriefingInput, type BriefingReport, type BriefingResult, type BriefingSection, type SourceId, } from './types.ts' const SOURCE_CONCURRENCY = 2 async function runSource( source: SourceId, input: BriefingInput, date: string, timezone: string, ): Promise { switch (source) { case 'calendar': return await calendarSection(input, date, timezone) case 'weather': return await weatherSection(input) case 'inbox': return await inboxSection(input) case 'health': return await healthSection(input) case 'home': return await homeSection(input) default: return assertNever(source, 'source') } } export async function buildMorningReport(input: BriefingInput = {}): Promise { const timezone = resolveTimezone(input) const date = resolveDate(input, timezone) const sources = resolveSources(input) const location = typeof input.location === 'string' ? input.location.trim() : undefined type Settled = | { status: 'fulfilled'; value: BriefingSection; ms: number } | { status: 'rejected'; reason: unknown; ms: number } const settled: Settled[] = new Array(sources.length) let nextIndex = 0 async function worker() { while (nextIndex < sources.length) { const index = nextIndex++ const started = Date.now() try { const value = await runSource(sources[index], input, date, timezone) settled[index] = { status: 'fulfilled', value, ms: Date.now() - started } } catch (reason) { settled[index] = { status: 'rejected', reason, ms: Date.now() - started } } } } await Promise.all(Array.from({ length: Math.min(SOURCE_CONCURRENCY, sources.length) }, worker)) const sections = settled.map((result, index) => result.status === 'fulfilled' ? result.value : failedSection(sources[index], result.reason), ) const report: BriefingReport = { date, timezone, title: location ? `${longDate(date, timezone)} ยท ${location}` : longDate(date, timezone), generatedAt: new Date().toISOString(), location, sources, metrics: metricsFromSections(sections), callouts: calloutsFromSections(sections), sections, sourceHealth: settled.map((result, index) => ({ source: sources[index], ok: result.status === 'fulfilled', detail: result.status === 'rejected' ? String(result.reason instanceof Error ? result.reason.message : result.reason) : undefined, ms: result.ms, })), markdown: '', notifyText: '', } report.markdown = reportMarkdown(report) report.notifyText = notifyBody(report) return report } export async function buildAndStoreReport(input: BriefingInput = {}): Promise { const report = await buildMorningReport(input) await saveReport(report) return report } /** * Build today's briefing from the composed sources and optionally email or notify. * * @param input.dryRun - Preview subject/body without calling `email_send`. * @param input.output - `email` (full briefing) or `notify` (short callouts). * @param input.sources - Defaults to calendar + weather. Add inbox, health, home. * @example * import runMorningBriefing from 'kody:@kody/morning-briefing' * const result = await runMorningBriefing({ dryRun: true, location: 'Denver, CO' }) * // => { date: '2026-08-22', delivered: false, preview: { subject: '...', text: '...' } } */ export async function runMorningBriefing(input: BriefingInput = {}): Promise { const output = resolveOutput(input) const report = await buildAndStoreReport(input) return await deliverReport({ report, output, dryRun: isDryRun(input), }) } /** * Scheduled/no-arg wrapper: load stored config, then run the same implementation. * Does not send when the stored config sets dryRun: true. */ export async function runScheduledMorningBriefing(): Promise { const config = await loadConfig() return await runMorningBriefing(config) } export async function getStoredReport(input: BriefingInput = {}): Promise { const timezone = resolveTimezone(input) const date = resolveDate(input, timezone) const stored = await readReport(date) if (stored) return stored return await buildAndStoreReport({ ...input, date, timezone }) } export function previewDelivery(report: BriefingReport, output: ReturnType) { return deliveryPreview(report, output) }