# @kody/notify — agent notes Human setup and intent live in [`README.md`](./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. ```js 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. ```js // 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`: ```js 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**: missing **`chat:write`**. Add the user scope, reconnect at `https://kody.codes/connect/oauth?provider=slack`. - **Slack `invalid_auth`**: reconnect the same integration URL. - **Discord 401**: save/rotate `discordBotToken` at 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/notify` bucket — 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. - `html` is 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/new` or `/connect/oauth` URLs instead.