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<BriefingSection> {
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<BriefingReport> {
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<BriefingReport> {
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<BriefingResult> {
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<BriefingResult> {
const config = await loadConfig()
return await runMorningBriefing(config)
}
export async function getStoredReport(input: BriefingInput = {}): Promise<BriefingReport> {
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<typeof resolveOutput>) {
return deliveryPreview(report, output)
}