/** User-scoped Cursor API key (raw key from dashboard → API Keys). */ export const CURSOR_SECRET_NAME = 'cursorApiKey' /** Bearer auth; Cursor also accepts Basic with key as username. */ const CURSOR_AUTH = 'Bearer {{secret:cursorApiKey}}' export const CURSOR_BASE_URL = 'https://api.cursor.com' /** Pull a human-readable detail from Cursor error bodies. */ export function cursorErrorDetail(body: unknown): string { if (typeof body === 'string') return body.trim().slice(0, 500) if (!body || typeof body !== 'object') return '' const root = body as Record const nested = root.error && typeof root.error === 'object' ? (root.error as Record) : null const message = (typeof root.message === 'string' && root.message.trim()) || (nested && typeof nested.message === 'string' && nested.message.trim()) || '' const code = (typeof root.code === 'string' && root.code.trim()) || (nested && typeof nested.code === 'string' && nested.code.trim()) || '' if (message && code && !message.includes(code)) return `${code}: ${message}` return message || code } export class CursorApiError extends Error { status: number body: unknown constructor(status: number, body: unknown, message?: string) { const detail = cursorErrorDetail(body) const suffix = detail ? `: ${detail}` : '' const rateNote = status === 429 ? ' (rate limited — retry with backoff)' : '' const authNote = status === 401 ? ' (check that the "cursorApiKey" user secret is saved — cursor.com/dashboard → API Keys — with host approval for api.cursor.com)' : '' super(message || `Cursor API ${status}${suffix}${rateNote}${authNote}`) this.name = 'CursorApiError' this.status = status this.body = body } } export function clampInt( value: unknown, min: number, max: number, fallback: number, ): number { const number = Number(value) if (!Number.isFinite(number)) return fallback return Math.min(max, Math.max(min, Math.floor(number))) } export function trimString(value: unknown): string { return typeof value === 'string' ? value.trim() : '' } // NOTE: there is deliberately no secret_list pre-check here. Webhook-dispatched // package runs see a FILTERED secret list, so "is the secret saved?" cannot be // answered reliably from every runtime context — a pre-check that throws there // blocks callers whose {{secret:...}} placeholder would have resolved fine at // fetch time (this bit @kentcdodds/sentry-triage in production). Missing keys // now surface as a 401 CursorApiError with the setup hint instead. export type CursorRequestInput = { path: string method?: string query?: Record body?: unknown headers?: Record accept?: string } export type CursorRequestResult = { status: number body: unknown headers: Record } /** Compact authenticated request to api.cursor.com (raw Response parsed). */ export async function cursorRequestRaw( input: CursorRequestInput, ): Promise { const url = new URL(trimString(input.path), CURSOR_BASE_URL) if (input.query) { for (const [key, value] of Object.entries(input.query)) { if (value === undefined || value === null || value === '') continue url.searchParams.set(key, String(value)) } } const headers = new Headers({ Accept: input.accept || 'application/json', Authorization: CURSOR_AUTH, 'User-Agent': 'kody-cursor/2.0', }) if (input.headers) { for (const [key, value] of Object.entries(input.headers)) { if (value) headers.set(key, value) } } const init: RequestInit = { method: trimString(input.method) || 'GET', headers, } if (input.body !== undefined) { if (!headers.has('Content-Type')) { headers.set('Content-Type', 'application/json') } init.body = typeof input.body === 'string' ? input.body : JSON.stringify(input.body) } const response = await fetch(url.toString(), init) const responseHeaders: Record = {} response.headers.forEach((value, key) => { responseHeaders[key] = value }) const text = await response.text() let body: unknown = null if (response.status !== 204 && text.trim()) { const contentType = response.headers.get('content-type') || '' if (contentType.includes('application/json')) { try { body = JSON.parse(text) } catch { body = text } } else { body = text } } return { status: response.status, body, headers: responseHeaders } } export async function cursorApi( input: CursorRequestInput, ): Promise { const result = await cursorRequestRaw(input) if (result.status < 200 || result.status >= 300) { throw new CursorApiError(result.status, result.body) } return result.body as T }