Skip to content

AGENTS.md

138 lines · 4.9 KB · Markdown
Raw

@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

ExportImport (after fork — use caller's scope)
root fanoutinstalled 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: 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.