Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kentcdodds/grok-bot

src/registry.ts

316 lines · 8.8 KB · TypeScript
import { kody, packageContext, packageStorage } from 'kody:runtime'

/** Saved package UUID from package_get_git_remote create. Fallback when packageContext is null. */
export const SAVED_PACKAGE_ID = '5bddf97f-09c7-41eb-8402-a7102432dd2e'

export const WEBHOOK_HOST = 'api2.cursor.sh'
export const BOT_PREFIX = 'bot:'
export const ALIAS_PREFIX = 'alias:'

const UUID_LIKE =
	/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i

export type BotRecord = {
	id: string
	name: string
	aliases: string[]
	url: string
	secretName: string
	channelId?: string
	createdAt: string
	updatedAt: string
}

export type BotSummary = {
	id: string
	name: string
	aliases: string[]
	secretName: string
	pendingSecret: boolean
	urlHost: string
	channelId?: string
}

export function normalizeName(name: string): string {
	return name.trim().toLowerCase()
}

export function isUuidLike(value: string): boolean {
	return UUID_LIKE.test(value.trim())
}

export function secretNameFor(id: string): string {
	return `grokBotWake.${id}`
}

export function botKey(id: string): string {
	return `${BOT_PREFIX}${id}`
}

export function aliasKey(normalized: string): string {
	return `${ALIAS_PREFIX}${normalized}`
}

export function savedPackageId(): string {
	const fromContext = packageContext?.packageId?.trim()
	return fromContext || SAVED_PACKAGE_ID
}

export function requireId(id: unknown): string {
	if (typeof id !== 'string' || id.trim() === '') {
		throw new Error('id is required and must be a UUID.')
	}
	const trimmed = id.trim()
	if (!isUuidLike(trimmed)) {
		throw new Error('id must be a UUID-like value (8-4-4-4-12 hex).')
	}
	return trimmed
}

export function requireName(name: unknown): string {
	if (typeof name !== 'string' || name.trim() === '') {
		throw new Error('name is required.')
	}
	return name.trim()
}

export function requirePrompt(prompt: unknown): string {
	if (typeof prompt !== 'string' || prompt.trim() === '') {
		throw new Error('prompt is required.')
	}
	return prompt
}

export function requireBotQuery(bot: unknown): string {
	if (typeof bot !== 'string' || bot.trim() === '') {
		throw new Error('bot is required (id or name).')
	}
	return bot.trim()
}

export function parseWebhookUrl(url: unknown): URL {
	if (typeof url !== 'string' || url.trim() === '') {
		throw new Error('url is required and must be https://api2.cursor.sh/…')
	}
	let parsed: URL
	try {
		parsed = new URL(url.trim())
	} catch {
		throw new Error('url must be a valid https URL on api2.cursor.sh')
	}
	if (parsed.protocol !== 'https:') {
		throw new Error('url must use https.')
	}
	if (parsed.hostname !== WEBHOOK_HOST) {
		throw new Error(`url host must be exactly ${WEBHOOK_HOST}.`)
	}
	return parsed
}

export function buildSaveUrl(input: {
	secretName: string
	name: string
	packageId: string
}): string {
	const url = new URL('https://kody.codes/account/secrets/new')
	url.searchParams.set('name', input.secretName)
	url.searchParams.set(
		'description',
		`Grok Bot wake sender key for ${input.name}`,
	)
	url.searchParams.set('allowedHosts', WEBHOOK_HOST)
	url.searchParams.set('scope', 'package')
	url.searchParams.set('packageId', input.packageId)
	return url.toString()
}

export function parseBotRecord(value: unknown): BotRecord | null {
	let parsed = value
	if (typeof parsed === 'string') {
		try {
			parsed = JSON.parse(parsed)
		} catch {
			return null
		}
	}
	if (!parsed || typeof parsed !== 'object') return null
	const row = parsed as Record<string, unknown>
	const id = typeof row.id === 'string' ? row.id.trim() : ''
	const name = typeof row.name === 'string' ? row.name.trim() : ''
	const url = typeof row.url === 'string' ? row.url.trim() : ''
	const channelId = typeof row.channelId === 'string' ? row.channelId.trim() : ''
	if (!id || !name) return null
	const aliases = Array.isArray(row.aliases)
		? row.aliases
				.filter((item): item is string => typeof item === 'string')
				.map((item) => normalizeName(item))
				.filter(Boolean)
		: [normalizeName(name)]
	const uniqueAliases = Array.from(new Set(aliases))
	if (!uniqueAliases.includes(normalizeName(name))) {
		uniqueAliases.push(normalizeName(name))
	}
	return {
		id,
		name,
		aliases: uniqueAliases,
		url,
		secretName: secretNameFor(id),
		...(channelId ? { channelId } : {}),
		createdAt:
			typeof row.createdAt === 'string' ? row.createdAt : new Date().toISOString(),
		updatedAt:
			typeof row.updatedAt === 'string' ? row.updatedAt : new Date().toISOString(),
	}
}

async function readAlias(normalized: string): Promise<string | null> {
	const raw = await packageStorage().get(aliasKey(normalized))
	if (typeof raw === 'string' && raw.trim()) return raw.trim()
	if (raw && typeof raw === 'object' && 'id' in raw) {
		const id = (raw as { id?: unknown }).id
		return typeof id === 'string' ? id.trim() : null
	}
	return null
}

export async function resolveBot(bot: string): Promise<BotRecord | null> {
	const raw = bot.trim()
	if (!raw) return null
	const bucket = packageStorage()
	if (isUuidLike(raw)) {
		const byId = parseBotRecord(await bucket.get(botKey(raw)))
		if (byId) return byId
	}
	const idFromAlias = await readAlias(normalizeName(raw))
	if (idFromAlias) {
		const byAlias = parseBotRecord(await bucket.get(botKey(idFromAlias)))
		if (byAlias) return byAlias
	}
	return parseBotRecord(await bucket.get(botKey(raw)))
}

export async function writeBotRecord(record: BotRecord): Promise<void> {
	const bucket = packageStorage()
	await bucket.set(botKey(record.id), {
		id: record.id,
		name: record.name,
		aliases: record.aliases,
		url: record.url,
		secretName: record.secretName,
		...(record.channelId ? { channelId: record.channelId } : {}),
		createdAt: record.createdAt,
		updatedAt: record.updatedAt,
	})
	for (const alias of record.aliases) {
		await bucket.set(aliasKey(alias), record.id)
	}
}

export async function deleteAlias(normalized: string): Promise<void> {
	await packageStorage().delete(aliasKey(normalized))
}

export async function deleteBotRecord(record: BotRecord): Promise<void> {
	const bucket = packageStorage()
	await bucket.delete(botKey(record.id))
	for (const alias of record.aliases) {
		await bucket.delete(aliasKey(alias))
	}
}

function secretNamesFrom(result: unknown): string[] {
	if (!result || typeof result !== 'object') return []
	const secrets = (result as { secrets?: unknown }).secrets
	if (!Array.isArray(secrets)) return []
	return secrets
		.map((item) =>
			item && typeof item === 'object' && typeof (item as { name?: unknown }).name === 'string'
				? (item as { name: string }).name
				: '',
		)
		.filter(Boolean)
}

export async function listKnownSecretNames(): Promise<Set<string> | null> {
	try {
		const names = new Set<string>()
		const all = secretNamesFrom(await kody.secret_list({}))
		for (const name of all) names.add(name)
		try {
			const scoped = secretNamesFrom(await kody.secret_list({ scope: 'package' }))
			for (const name of scoped) names.add(name)
		} catch {
			// Package-scoped listing may not be visible from execute yet.
		}
		return names
	} catch {
		return null
	}
}

export async function isSecretPending(secretName: string): Promise<boolean> {
	const names = await listKnownSecretNames()
	if (!names) return true
	return !names.has(secretName)
}

function listEntries(listed: unknown): Array<{ key?: string; value?: unknown }> {
	if (Array.isArray(listed)) {
		return listed.filter((item) => item && typeof item === 'object') as Array<{
			key?: string
			value?: unknown
		}>
	}
	if (listed && typeof listed === 'object') {
		const entries = (listed as { entries?: unknown }).entries
		if (Array.isArray(entries)) {
			return entries.filter((item) => item && typeof item === 'object') as Array<{
				key?: string
				value?: unknown
			}>
		}
	}
	return []
}

export async function listBotRecords(): Promise<BotRecord[]> {
	const listed = await packageStorage().list({ prefix: BOT_PREFIX })
	const records: BotRecord[] = []
	const seen = new Set<string>()
	for (const entry of listEntries(listed)) {
		const key = typeof entry.key === 'string' ? entry.key : ''
		const value = 'value' in entry ? entry.value : entry
		if (key && !key.startsWith(BOT_PREFIX)) continue
		const record = parseBotRecord(value)
		if (!record || seen.has(record.id)) continue
		seen.add(record.id)
		records.push(record)
	}
	return records
}

export function urlHostOf(url: string): string {
	if (!url) return ''
	try {
		return new URL(url).hostname
	} catch {
		return WEBHOOK_HOST
	}
}

export function missingBotError(bot: string): Error {
	return new Error(
		`No Grok Bot is registered for "${bot}". Call register({ id, name, url }) first.`,
	)
}

export function missingSecretError(input: {
	name: string
	secretName: string
	saveUrl: string
}): Error {
	return new Error(
		`Wake is refused until the sender key is saved for "${input.name}" as ${input.secretName}. Open ${input.saveUrl} and paste the key there (never in chat). Do not POST until that secret exists.`,
	)
}