← Public packages
@kentcdodds/grok-bot
Wake a named Grok Bot from Kody by posting a prompt to that bot's own Cursor webhook.
src/registry.ts
574 lines · 17.8 KB · TypeScriptimport { plainClone, storageGet } from './plain.ts'
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
needsHostApproval: boolean
urlHost: string
channelId?: string
emailAddress?: string
approvalUrl?: string
}
export type WakeSecretMeta = {
name: string
scope: string
package_id: string | null
allowed_hosts: string[]
description: string | null
updated_at: string | null
}
export type SecretUseStatus = {
exists: boolean
usableScope: string | null
needsHostApproval: boolean
approvalUrl: string | null
allowedHosts: 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 {
// Always grok-bot — secrets/saveUrl must not inherit a dependent's packageContext.
return SAVED_PACKAGE_ID
}
/** True when this code is executing under a different package's isolate. */
export function isForeignPackageIsolate(): boolean {
const fromContext = packageContext?.packageId?.trim()
return Boolean(fromContext && 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 buildHostApprovalUrl(input: {
scope: string
secretName: string
}): string {
return `https://kody.codes/account/secrets/${encodeURIComponent(input.scope)}/${encodeURIComponent(input.secretName)}?allowed-host=${WEBHOOK_HOST}`
}
/**
* Known per-bot wake sender keys. New bots need a matching kody.credentials
* slot and a case here before wake can stamp-seal after republish.
*/
export const KNOWN_GROK_BOT_WAKE_SECRETS = [
'grokBotWake.021d6409-01e7-4af0-a59c-4cbf2785cb50',
'grokBotWake.57072205-edee-46ed-b4fa-a24b657ef369',
'grokBotWake.58b84955-b713-4814-b0ca-b7982b4b5829',
'grokBotWake.60f91f03-6b17-4b1a-a590-529613c79977',
'grokBotWake.6519d644-b6ac-4e61-989c-3d96d0b63cca',
'grokBotWake.71e7550e-746d-417f-b253-05165975ff69',
'grokBotWake.775984a5-71ac-4e8c-bba3-b93de8ff6a06',
'grokBotWake.a08ff635-d146-4205-bb62-d538e9723d77',
'grokBotWake.a2600d4e-d242-4709-8a9f-f756ad2d7e82',
'grokBotWake.b5cf16d8-dde6-4d3d-b5b8-4452bfade775',
'grokBotWake.f835c77a-c93f-424e-a0f1-f191c2235e19',
] as const
export type KnownGrokBotWakeSecret = (typeof KNOWN_GROK_BOT_WAKE_SECRETS)[number]
/**
* Return a literal stamped `{{secret:…}}` placeholder for a known wake key.
* Scope suffixes are removed — org credentials assigned to the package replace
* package/user `|scope=` buckets for the credential stamp.
*/
export function secretPlaceholder(secretName: string, _scope?: string | null): string {
switch (secretName) {
case 'grokBotWake.021d6409-01e7-4af0-a59c-4cbf2785cb50':
return '{{secret:grokBotWake.021d6409-01e7-4af0-a59c-4cbf2785cb50}}'
case 'grokBotWake.57072205-edee-46ed-b4fa-a24b657ef369':
return '{{secret:grokBotWake.57072205-edee-46ed-b4fa-a24b657ef369}}'
case 'grokBotWake.58b84955-b713-4814-b0ca-b7982b4b5829':
return '{{secret:grokBotWake.58b84955-b713-4814-b0ca-b7982b4b5829}}'
case 'grokBotWake.60f91f03-6b17-4b1a-a590-529613c79977':
return '{{secret:grokBotWake.60f91f03-6b17-4b1a-a590-529613c79977}}'
case 'grokBotWake.6519d644-b6ac-4e61-989c-3d96d0b63cca':
return '{{secret:grokBotWake.6519d644-b6ac-4e61-989c-3d96d0b63cca}}'
case 'grokBotWake.71e7550e-746d-417f-b253-05165975ff69':
return '{{secret:grokBotWake.71e7550e-746d-417f-b253-05165975ff69}}'
case 'grokBotWake.775984a5-71ac-4e8c-bba3-b93de8ff6a06':
return '{{secret:grokBotWake.775984a5-71ac-4e8c-bba3-b93de8ff6a06}}'
case 'grokBotWake.a08ff635-d146-4205-bb62-d538e9723d77':
return '{{secret:grokBotWake.a08ff635-d146-4205-bb62-d538e9723d77}}'
case 'grokBotWake.a2600d4e-d242-4709-8a9f-f756ad2d7e82':
return '{{secret:grokBotWake.a2600d4e-d242-4709-8a9f-f756ad2d7e82}}'
case 'grokBotWake.b5cf16d8-dde6-4d3d-b5b8-4452bfade775':
return '{{secret:grokBotWake.b5cf16d8-dde6-4d3d-b5b8-4452bfade775}}'
case 'grokBotWake.f835c77a-c93f-424e-a0f1-f191c2235e19':
return '{{secret:grokBotWake.f835c77a-c93f-424e-a0f1-f191c2235e19}}'
default:
throw new Error(
`Wake secret "${secretName}" is not stamp-declared. Add a kody.credentials slot and a secretPlaceholder case, then republish.`,
)
}
}
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 storageGet((key) => packageStorage().get(key), 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 storageGet((key) => bucket.get(key), botKey(raw)))
if (byId) return byId
}
const idFromAlias = await readAlias(normalizeName(raw))
if (idFromAlias) {
const byAlias = parseBotRecord(
await storageGet((key) => bucket.get(key), botKey(idFromAlias)),
)
if (byAlias) return byAlias
}
return parseBotRecord(await storageGet((key) => bucket.get(key), botKey(raw)))
}
export async function writeBotRecord(record: BotRecord): Promise<void> {
const bucket = packageStorage()
const payload = JSON.stringify({
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,
})
await bucket.set(botKey(record.id), payload)
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 normalizeWakeSecrets(result: unknown): WakeSecretMeta[] {
if (!result || typeof result !== 'object') return []
const secrets = (result as { secrets?: unknown }).secrets
if (!Array.isArray(secrets)) return []
return secrets.flatMap((item) => {
if (!item || typeof item !== 'object') return []
const row = item as Record<string, unknown>
const name = typeof row.name === 'string' ? row.name : ''
if (!name.startsWith('grokBotWake.')) return []
const hosts = row.allowed_hosts ?? row.allowedHosts
return [
{
name,
scope: typeof row.scope === 'string' ? row.scope : '',
package_id:
typeof row.package_id === 'string'
? row.package_id
: typeof row.packageId === 'string'
? row.packageId
: null,
allowed_hosts: Array.isArray(hosts)
? hosts.filter((host): host is string => typeof host === 'string')
: [],
description: typeof row.description === 'string' ? row.description : null,
updated_at:
typeof row.updated_at === 'string'
? row.updated_at
: typeof row.updatedAt === 'string'
? row.updatedAt
: null,
},
]
})
}
/**
* Direct `secretList` only. In the grok-bot package isolate (jobs, webhooks,
* exports serving in package context) this returns package-scoped `grokBotWake.*`
* keys. From ad hoc execute it usually does not — callers use stamp-optimistic
* package-bucket fallbacks instead of subscription dispatch.
*/
export async function listWakeSecretsFromSecretList(): Promise<WakeSecretMeta[] | null> {
try {
const seen = new Map<string, WakeSecretMeta>()
const listings = [plainClone(await kody.secretList({}))]
for (const scope of ['package', 'user'] as const) {
try {
listings.push(plainClone(await kody.secretList({ scope })))
} catch {
// Package-scoped listing may not be visible from execute yet.
}
}
for (const listed of listings) {
for (const secret of normalizeWakeSecrets(plainClone(listed))) {
seen.set(`${secret.scope}:${secret.name}`, secret)
}
}
return [...seen.values()]
} catch {
return null
}
}
function hasPackageWakeSecrets(secrets: WakeSecretMeta[]): boolean {
return secrets.some(
(secret) => secret.scope === 'package' && secret.name.startsWith('grokBotWake.'),
)
}
function sortWakeSecrets(secrets: WakeSecretMeta[]): WakeSecretMeta[] {
return [...secrets].sort(
(a, b) => a.name.localeCompare(b.name) || a.scope.localeCompare(b.scope),
)
}
function metaFromPackageBucket(secretName: string, botName?: string): WakeSecretMeta {
return {
name: secretName,
scope: 'package',
package_id: SAVED_PACKAGE_ID,
allowed_hosts: [WEBHOOK_HOST],
description: botName
? `Grok Bot wake sender key for ${botName}`
: 'Grok Bot wake sender key',
updated_at: null,
}
}
/**
* Stamp-aligned / package-bucket existence for a single wake key.
* Prefer authoritative listings; when listing is blind (execute), fall back to
* stamp-optimistic package scope for wake (no subscription dispatch).
*/
export async function packageWakeSecretExists(
secretName: string,
): Promise<boolean | null> {
const name = secretName.trim()
if (!name) return false
const listed = await listWakeSecretsFromSecretList()
if (listed && listed.some((secret) => secret.name === name)) return true
if (listed && hasPackageWakeSecrets(listed)) return false
// Unbound listing: stamp fetch may still resolve package keys.
if (name.startsWith('grokBotWake.')) return null
return false
}
export async function listWakeSecrets(): Promise<WakeSecretMeta[] | null> {
const fromList = await listWakeSecretsFromSecretList()
if (fromList === null) return null
return sortWakeSecrets(fromList)
}
export function secretUseStatusFrom(
secrets: WakeSecretMeta[] | null,
secretName: string,
): SecretUseStatus {
if (!secrets) {
return {
exists: false,
usableScope: null,
needsHostApproval: false,
approvalUrl: null,
allowedHosts: [],
}
}
const matches = secrets.filter((secret) => secret.name === secretName)
if (!matches.length) {
return {
exists: false,
usableScope: null,
needsHostApproval: false,
approvalUrl: null,
allowedHosts: [],
}
}
const usable = matches.find((secret) => secret.allowed_hosts.includes(WEBHOOK_HOST))
const first = matches[0]
return {
exists: true,
usableScope: usable?.scope ?? null,
needsHostApproval: !usable,
approvalUrl: first
? buildHostApprovalUrl({ scope: first.scope || 'user', secretName })
: null,
allowedHosts: Array.from(new Set(matches.flatMap((secret) => secret.allowed_hosts))),
}
}
export async function getSecretUseStatus(secretName: string): Promise<SecretUseStatus> {
const listed = await listWakeSecrets()
const fromList = secretUseStatusFrom(listed, secretName)
if (fromList.exists) return fromList
// Listing blind (execute): stamp-aligned fetch still resolves package-scoped
// grokBotWake.* keys. Do not false-alarm "not saved".
if (secretName.startsWith('grokBotWake.')) {
return secretUseStatusFrom([metaFromPackageBucket(secretName)], secretName)
}
return fromList
}
export async function listKnownSecretNames(): Promise<Set<string> | null> {
const secrets = await listWakeSecrets()
if (!secrets) return null
return new Set(secrets.map((secret) => secret.name))
}
export async function isSecretPending(secretName: string): Promise<boolean> {
const status = await getSecretUseStatus(secretName)
return !status.exists
}
/**
* For `./secret-status`: direct secretList / listWakeSecrets only (no dispatch).
*/
export async function listWakeSecretsForStatus(): Promise<WakeSecretMeta[]> {
return (await listWakeSecrets()) ?? []
}
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 bucket = packageStorage()
let listed: unknown = null
try {
listed = plainClone(await bucket.list({ prefix: BOT_PREFIX }))
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
if (!/Proxy could not be serialized/i.test(message)) throw error
listed = null
}
const records: BotRecord[] = []
const seen = new Set<string>()
const keys: string[] = []
for (const entry of listEntries(listed)) {
const key = typeof entry.key === 'string' ? entry.key : ''
if (key && key.startsWith(BOT_PREFIX)) keys.push(key)
}
// Prefer per-key storageGet: list values for object rows can be Proxies.
for (const key of keys) {
const record = parseBotRecord(await storageGet((k) => bucket.get(k), key))
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.`,
)
}
export function hostApprovalError(input: {
name: string
secretName: string
approvalUrl: string
}): Error {
return new Error(
`Wake is refused until ${WEBHOOK_HOST} is approved for "${input.name}" sender key ${input.secretName}. Open ${input.approvalUrl} and approve that host. Do not POST until the host is approved.`,
)
}