import createGuildChannel from 'kody:@kentcdodds/discord/create-guild-channel'
import getConfig from 'kody:@kentcdodds/discord/get-config'
import {
channelSlug,
getMappingByBotId,
getMappingByName,
readCategoryId,
writeChannelMapping,
} from './discord-map.ts'
import {
buildSaveUrl,
deleteAlias,
isSecretPending,
normalizeName,
parseBotRecord,
parseWebhookUrl,
requireId,
requireName,
savedPackageId,
secretNameFor,
urlHostOf,
writeBotRecord,
type BotRecord,
} from './registry.ts'
import { packageStorage } from 'kody:runtime'
export type RegisterInput = {
/** Grok Bot UUID. */
id: string
/** Human name used as a case-insensitive alias. */
name: string
/** HTTPS webhook URL. Host must be exactly api2.cursor.sh. */
url: string
}
export type RegisterResult = {
id: string
name: string
secretName: string
urlHost: string
saveUrl: string
pendingSecret: boolean
channelId: string | null
}
async function ensureDiscordChannel(input: {
id: string
name: string
}): Promise<string> {
const slug = channelSlug(input.name)
const byBot = await getMappingByBotId(input.id)
if (byBot) return byBot.channelId
const byName = slug ? await getMappingByName(slug) : null
if (byName) {
if (byName.botId !== input.id) {
await writeChannelMapping({
channelId: byName.channelId,
botId: input.id,
name: slug || byName.name,
})
}
return byName.channelId
}
const categoryId = await readCategoryId()
const config = (await getConfig()) as { serverId?: string }
const created = (await createGuildChannel({
guildId: config.serverId,
name: slug || channelSlug(input.name),
parentId: categoryId,
type: 0,
})) as { channelId?: string; id?: string; name?: string }
const channelId = String(created.channelId || created.id || '').trim()
if (!channelId) {
throw new Error('create-guild-channel did not return a channel id.')
}
await writeChannelMapping({
channelId,
botId: input.id,
name: slug || created.name || input.name,
})
return channelId
}
/**
* Register or update a Grok Bot webhook roster row.
*
* Stores id, name, aliases, and URL in packageStorage. Never accepts or
* persists a sender key. After register, open `saveUrl` and paste the key
* there. Ensures a text channel under the Grok Bot Discord category (reuses
* the existing slug when present) and stores `channelId` on the roster row.
*
* @param input.id - Grok Bot UUID
* @param input.name - Human name used as an alias
* @param input.url - HTTPS webhook on api2.cursor.sh
* @returns Registration result with saveUrl, pendingSecret, and channelId
* @example
* import register from 'kody:@kentcdodds/grok-bot/register'
* const result = await register({
* id: '11111111-1111-1111-1111-111111111111',
* name: 'ship-pr',
* url: 'https://api2.cursor.sh/automations/webhook/example',
* })
* // => { id, name, secretName, urlHost: 'api2.cursor.sh', saveUrl, pendingSecret, channelId }
*/
export default async function register(
input: RegisterInput = {} as RegisterInput,
): Promise<RegisterResult> {
const id = requireId(input?.id)
const name = requireName(input?.name)
const webhook = parseWebhookUrl(input?.url)
const now = new Date().toISOString()
const secretName = secretNameFor(id)
const normalized = normalizeName(name)
const bucket = packageStorage()
const existing = parseBotRecord(await bucket.get(`bot:${id}`))
const aliases = existing
? Array.from(new Set([...existing.aliases.filter((alias) => alias !== normalizeName(existing.name)), normalized]))
: [normalized]
if (existing) {
const previous = normalizeName(existing.name)
if (previous && previous !== normalized) {
await deleteAlias(previous)
}
}
const channelId = await ensureDiscordChannel({ id, name })
const record: BotRecord = {
id,
name,
aliases,
url: webhook.toString(),
secretName,
channelId,
createdAt: existing?.createdAt ?? now,
updatedAt: now,
}
await writeBotRecord(record)
await writeChannelMapping({ channelId, botId: id, name: channelSlug(name) || name })
const saveUrl = buildSaveUrl({
secretName,
name,
packageId: savedPackageId(),
})
return {
id,
name,
secretName,
urlHost: urlHostOf(record.url),
saveUrl,
pendingSecret: await isSecretPending(secretName),
channelId: record.channelId || channelId,
}
}