@kody/morning-briefing
README.md
192 lines · 7.2 KB · Markdown@kody/morning-briefing
Composable daily briefing from calendar, weather, and optional inbox, health, or home signals. Delivered by email or a short notify-self message. No Discord.
Intent
Give any signed-in account one reliable morning update they can compose
themselves. Calendar and weather are the default sources. Inbox, health (air
quality), and home are optional and degrade to a skipped or warning section when
auth or data is missing. Success is a structured report plus a dryRun preview
that never sends mail until you choose an output channel and turn dry-run off.
This package is the generic official briefing. It does not post to Discord, and it does not ship Kent-specific Kit, YouTube, GitHub, Tesla, or apartment wiring.
When to use
- Build today's briefing from the sources you have connected
- Preview the email or notify payload with
dryRun: true - Read a stored report for a date without rebuilding every source
- Schedule a disabled-by-default 07:00 UTC job after you save config
Compose sources
Pass sources to choose what runs. Default is calendar and weather.
import runMorningBriefing from 'kody:@kody/morning-briefing'
export default async function main() {
return await runMorningBriefing({
dryRun: true,
location: 'Denver, CO',
timezone: 'America/Denver',
sources: ['calendar', 'weather', 'inbox', 'health', 'home'],
output: 'email',
calendar: { accounts: ['personal'] },
inbox: { account: 'personal', query: 'is:unread newer_than:1d' },
home: { items: [{ title: 'Thermostat 70°', detail: 'Heat, occupied' }] },
})
}| Source | Default | What it reads | Auth |
|---|---|---|---|
calendar | yes | Today's events via @kody/google/calendar | Built-in Google OAuth (account, default personal → integration google) |
weather | yes | Daily forecast + current conditions via Open-Meteo | None. Pass location (city or place name) |
inbox | no | Unread Gmail via @kody/google/gmail | Bring-your-own Google OAuth with Gmail read scopes. Built-in Google is Calendar/send, not inbox read |
health | no | US AQI / PM2.5 via Open-Meteo Air Quality | None. Uses location (or health.location) |
home | no | Caller-supplied snapshot items | None in this package. Pass home.items, or skip until you have a home connector |
Sources run independently. A missing integration becomes a warning or skipped section; the rest of the briefing still builds.
Auth each source
Calendar (built-in Google). Connect the saved google integration:
https://kody.codes/connect/oauth?provider=googlePass calendar.accounts using @kody/google aliases (personal, business,
…). Every Calendar call needs an explicit account. Widen Calendar scopes from
the connect UI when a mailbox has more than the primary calendar.
Weather and health (zero auth). Public Open-Meteo geocoding, forecast, and
air-quality APIs. Attribute Open-Meteo (CC BY 4.0)
when you reuse the data. Set location on the run or persist it in package
config for the scheduled job.
Inbox (bring-your-own Google). Gmail read scopes are restricted and are
not on the built-in Google menu — this package still exports the inbox
source; do not hide it. Create a Google Cloud OAuth Web application client,
register redirect https://kody.codes/connect/oauth, enable the Gmail API and
Calendar API, publish the app to Production (Testing refresh tokens expire
after seven days), then connect:
https://kody.codes/connect/oauth?provider=google&authorizeUrl=https%3A%2F%2Faccounts.google.com%2Fo%2Foauth2%2Fv2%2Fauth&tokenUrl=https%3A%2F%2Foauth2.googleapis.com%2Ftoken&flow=confidential&scopes=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar.readonly%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fgmail.readonly&allowedHosts=www.googleapis.com%2Cgmail.googleapis.com&extraAuthorizeParams=%7B%22access_type%22%3A%22offline%22%2C%22prompt%22%3A%22consent%22%7DUse the personal (or other) @kody/google account alias that matches that
connection. A 403 / insufficient-scope error names gmail.readonly and this
URL. See coding_guide_get({ guide: "provider_google" }).
Home. This official package does not call a personal home MCP or Discord.
Pass home.items from your own home package, or omit home from sources.
This composer does not need an API-key secret. There is no
/account/secrets/new step for calendar, weather, health, or email output.
Hosts
www.googleapis.com— Calendargmail.googleapis.com— inbox (BYO only)geocoding-api.open-meteo.com,api.open-meteo.com,air-quality-api.open-meteo.com— weather and health (public)
Scopes
- Calendar:
https://www.googleapis.com/auth/calendar.readonly(orcalendar.events.readonly) - Inbox:
https://www.googleapis.com/auth/gmail.readonly(restricted; BYO client)
If Google returns 403 / insufficient-scope, the package throws (or records a warning section) naming the missing scope and the next connect URL. It does not fail silently.
Output: email or notify
output is email (default) or notify. Both use kody.email_send to your
own account address.
email— full markdown briefingnotify— short subject + callout-only body
dryRun: true builds and stores the report, returns preview.subject /
preview.text, and does not send. The scheduled wrapper uses the same
implementation; keep the job disabled until a dry-run looks right.
Persist config for the job
The no-argument job daily-morning-briefing loads JSON from package storage
table config key briefing (same fields as the run input, minus dryRun).
Save it once, invoke ./run-morning-briefing with no params to prove the
scheduler contract, then enable the job.
Exports
kody:@kody/morning-briefing— build, optionally deliver (dryRunsupported)kody:@kody/morning-briefing/run-morning-briefing— same function (job + invoke)kody:@kody/morning-briefing/build-report— build and store without sendingkody:@kody/morning-briefing/get-report— read a stored report, rebuild if missing
Smoke test
Weather-only (no OAuth):
import buildReport from 'kody:@kody/morning-briefing/build-report'
export default async function main() {
const report = await buildReport({
location: 'Austin',
sources: ['weather'],
})
return { date: report.date, sections: report.sections.map((s) => ({ id: s.id, status: s.status })) }
}Delivery dry run (does not send):
import runMorningBriefing from 'kody:@kody/morning-briefing/run-morning-briefing'
export default async function main() {
return await runMorningBriefing({
dryRun: true,
location: 'Austin',
sources: ['weather'],
output: 'email',
})
}Keyless invoke from the same account:
import { packages } from 'kody:runtime'
export default async function main() {
return await packages.invoke({
kodyId: 'morning-briefing',
exportName: './build-report',
params: { location: 'Austin', sources: ['weather'] },
})
}Example
import buildReport from 'kody:@kody/morning-briefing/build-report'
export default async function main() {
const report = await buildReport({
location: 'Denver, CO',
sources: ['weather'],
})
return { date: report.date, sections: report.sections.map((s) => s.id) }
}