import {
buildSaveUrl,
isSecretPending,
missingBotError,
missingSecretError,
requireBotQuery,
requirePrompt,
resolveBot,
savedPackageId,
} from './registry.ts'
export type WakeInput = {
/** Bot id or name (case-insensitive). */
bot: string
/** Required non-empty prompt Kent is sending to the bot. */
prompt: string
/** Resolve and validate the secret without POSTing. */
dryRun?: boolean
/** Discord channel the wake was requested from. Passed through in the JSON body. */
discordChannelId?: string
/** Discord message that should receive the bot reply. Passed through in the JSON body. */
discordMessageId?: string
}
export type WakeResult =
| {
ok: boolean
status: number
bot: { id: string; name: string }
woke: boolean
}
| {
ok: true
dryRun: true
bot: { id: string; name: string }
secretName: string
}
/**
* Wake a registered Grok Bot by POSTing `{ id, name, prompt }` to its webhook.
* Optional `discordChannelId` / `discordMessageId` are included in the JSON
* body so the bot can reply in Discord.
*
* Refuses to POST when the sender-key secret is missing. Use `dryRun: true`
* to resolve the bot and confirm the secret exists without sending.
*
* @param input.bot - Bot id or name
* @param input.prompt - Non-empty prompt
* @param input.dryRun - Validate only; do not POST
* @returns Slim wake result; never a webhook response body
* @example
* import wake from 'kody:@kentcdodds/grok-bot/wake'
* const preview = await wake({
* bot: 'ship-pr',
* prompt: 'Summarize what you shipped today.',
* dryRun: true,
* })
* // => { ok: true, dryRun: true, bot: { id, name }, secretName }
*/
export default async function wake(
input: WakeInput = {} as WakeInput,
): Promise<WakeResult> {
const botQuery = requireBotQuery(input?.bot)
const prompt = requirePrompt(input?.prompt)
const discordChannelId =
typeof input?.discordChannelId === 'string' ? input.discordChannelId.trim() : ''
const discordMessageId =
typeof input?.discordMessageId === 'string' ? input.discordMessageId.trim() : ''
const record = await resolveBot(botQuery)
if (!record) throw missingBotError(botQuery)
const saveUrl = buildSaveUrl({
secretName: record.secretName,
name: record.name,
packageId: savedPackageId(),
})
if (await isSecretPending(record.secretName)) {
throw missingSecretError({
name: record.name,
secretName: record.secretName,
saveUrl,
})
}
if (input?.dryRun === true) {
return {
ok: true,
dryRun: true,
bot: { id: record.id, name: record.name },
secretName: record.secretName,
}
}
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), 8000)
try {
const response = await fetch(record.url, {
method: 'POST',
headers: {
Authorization: `Bearer {{secret:${record.secretName}}}`,
'X-Automation-Key': `{{secret:${record.secretName}}}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: record.id,
name: record.name,
prompt,
...(discordChannelId ? { discordChannelId } : {}),
...(discordMessageId ? { discordMessageId } : {}),
}),
signal: controller.signal,
})
const ok = response.status === 200
return {
ok,
status: response.status,
bot: { id: record.id, name: record.name },
woke: ok,
}
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
throw new Error(
`Wake timed out after 8s for "${record.name}" (${record.id}). One try, no retry.`,
)
}
throw error
} finally {
clearTimeout(timer)
}
}