@kody/notify
Fan out a short notification to your email, Slack, Discord, or Telegram from saved channel config.
AGENTS.md
138 lines · 4.9 KB · Markdown@kody/notify — agent notes
Human setup and intent live in README.md. This file is for
agents: fork/invoke, configure dry-runs, smoke snippets, and edge cases.
Secrets by name only — never paste tokens. Never copy channel, guild, or
chat ids from Kent, @kody, or another account. Do not disable live webhooks
or jobs (this package has none).
Critical: fork before configure
Live @kody/notify storage is the platform package bucket. Always
install/fork into the caller's account, then call their export. Prefer
resolving the installed specifier via packageList + packageGet, then
computed import(specifier). A static kody:@kody/notify import writes the
wrong bucket.
import { kody } from 'kody:runtime'
async function notify(params) {
const saved = await kody.packageList({})
const packageId = saved.packages.find((pkg) => pkg.kody_id === 'notify')?.package_id
if (!packageId) throw new Error('Install or fork notify first.')
const installed = await kody.packageGet({ package_id: packageId })
const specifier = installed.exports.find((entry) => entry.subpath === '.')?.import_specifier
if (!specifier) throw new Error('Installed notify has no root export.')
const importSpec = typeof specifier === 'string' && specifier.trim().startsWith('@')
? `kody:${specifier.trim()}`
: specifier
const mod = await import(importSpec)
return await mod.default(params)
}
export default async function main() {
return await notify({
subject: 'Deploy finished',
text: 'web@sha shipped.',
dryRun: true,
})
}Import path
| Export | Import (after fork — use caller's scope) |
|---|---|
| root fanout | installed notify root export (resolve via packageGet) |
There is a single root export. Do not hard-code @kody/notify for configure
or live send against a user's destinations.
Configure (dryRun first)
dryRun: true previews and skips writing configure. Pass dryRun: true
while drafting destination ids; drop it only when the user confirmed the ids
should persist.
// Email on (default) — preview only
await notify({ configure: { email: { enabled: true } }, dryRun: true })
// Slack — their channel id only
await notify({
configure: { slack: { enabled: true, channel: 'C0123456789', integration: 'slack' } },
dryRun: true,
})
// Discord — their channel snowflake + secret name
await notify({
configure: {
discord: { enabled: true, channelId: '123456789012345678', secretName: 'discordBotToken' },
},
dryRun: true,
})
// Telegram — their chatId + secret name
await notify({
configure: { telegram: { enabled: true, chatId: '123456789', secretName: 'telegramBotToken' } },
dryRun: true,
})Smoke test (no send)
Invoke in the package runtime so packageStorage and secret mounts
resolve. Call with dryRun: true:
import { kody } from 'kody:runtime'
export default async function main() {
const saved = await kody.packageList({})
const packageId = saved.packages.find((pkg) => pkg.kody_id === 'notify')?.package_id
if (!packageId) throw new Error('Install or fork notify first.')
const installed = await kody.packageGet({ package_id: packageId })
const specifier = installed.exports.find((entry) => entry.subpath === '.')?.import_specifier
if (!specifier) throw new Error('Installed notify has no root export.')
const importSpec = typeof specifier === 'string' && specifier.trim().startsWith('@')
? `kody:${specifier.trim()}`
: specifier
const mod = await import(importSpec)
return await mod.default({
subject: 'Notify smoke',
text: 'dryRun fanout preview',
dryRun: true,
})
}Success: dryRun === true, saved === false, and results includes an email
preview (status preview). It must not call email_send or post to
Slack/Discord/Telegram. Discord/Telegram dry-run may call users/@me /
getMe when those secrets already exist.
A live send (no dryRun) requires explicit user confirmation of the text and
destinations.
Troubleshooting
- Slack
missing_scope/ 403: missingchat:write. Add the user scope, reconnect athttps://kody.codes/connect/oauth?provider=slack. - Slack
invalid_auth: reconnect the same integration URL. - Discord 401: save/rotate
discordBotTokenat the secrets URL in README. - Discord 403: bot not in the server or missing Send Messages.
- Telegram 401/403: bot token or chat membership; see README steps.
- Wrong destinations after configure: you likely wrote the platform
@kody/notifybucket — fork first, then invoke the installed copy.
Edge cases
- Email is enabled by default; Slack/Discord/Telegram stay off until enabled and a destination id is stored.
htmlis email-only.- Multi-account:
configure.slack.integration,configure.discord.secretName,configure.telegram.secretName— never hard-code personal aliases. - Never paste tokens into chat; hand prefilled
/account/secrets/newor/connect/oauthURLs instead.