Skip to content
← Public packages

@kody/cursor

Cursor API SDK for cloud agents, runs, artifacts, workers, models, repositories, and raw requests.

src/lib/client.ts

156 lines · 4.3 KB · TypeScript
import { kody } from 'kody:runtime'

/** 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'

export class CursorApiError extends Error {
	status: number
	body: unknown

	constructor(status: number, body: unknown, message?: string) {
		const detail =
			body && typeof body === 'object' && 'message' in body
				? String((body as { message: unknown }).message)
				: ''
		const suffix = detail ? `: ${detail}` : ''
		const rateNote = status === 429 ? ' (rate limited — retry with backoff)' : ''
		super(message || `Cursor API ${status}${suffix}${rateNote}`)
		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() : ''
}

function secretEntries(result: unknown): Array<{ name?: string; scope?: string }> {
	if (
		result &&
		typeof result === 'object' &&
		Array.isArray((result as { secrets?: unknown }).secrets)
	) {
		return (result as { secrets: Array<{ name?: string; scope?: string }> })
			.secrets
	}
	return Array.isArray(result) ? result : []
}

export async function ensureUserSecret(
	name: string,
	message?: string,
): Promise<void> {
	const listed = await kody.secretList({ scope: 'user' })
	const found = secretEntries(listed).some(
		(entry) => entry && entry.name === name && entry.scope === 'user',
	)
	if (!found) throw new Error(message || `Secret "${name}" was not found.`)
}

export async function ensureCursorSecret(): Promise<void> {
	await ensureUserSecret(
		CURSOR_SECRET_NAME,
		'Secret "cursorApiKey" was not found. Save your raw Cursor API key at cursor.com/dashboard → API Keys, then retry.',
	)
}

export type CursorRequestInput = {
	path: string
	method?: string
	query?: Record<string, string | number | boolean | undefined | null>
	body?: unknown
	headers?: Record<string, string>
	accept?: string
}

export type CursorRequestResult = {
	status: number
	body: unknown
	headers: Record<string, string>
}

/** Compact authenticated request to api.cursor.com (raw Response parsed). */
export async function cursorRequestRaw(
	input: CursorRequestInput,
): Promise<CursorRequestResult> {
	await ensureCursorSecret()

	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<string, string> = {}
	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<T = unknown>(
	input: CursorRequestInput,
): Promise<T> {
	const result = await cursorRequestRaw(input)
	if (result.status < 200 || result.status >= 300) {
		throw new CursorApiError(result.status, result.body)
	}
	return result.body as T
}