import { clampInt, cursorApi, cursorRequestRaw, trimString } from './lib/client.ts'
import type { PromptInput } from './agents.ts'
export type CreateRunParams = {
agentId: string
prompt: PromptInput | string
mcpServers?: unknown[]
mode?: 'agent' | 'plan'
}
export type ListRunsParams = {
agentId: string
limit?: number
cursor?: string
}
export type RunSummary = {
id: string
agentId?: string
status?: string
createdAt?: string
updatedAt?: string
durationMs?: number
result?: string
git?: {
branches?: Array<{
repoUrl?: string
branch?: string
prUrl?: string
}>
}
[key: string]: unknown
}
export type ListRunsResult = {
items: RunSummary[]
nextCursor?: string
[key: string]: unknown
}
export type CreateRunResult = {
run: RunSummary
[key: string]: unknown
}
function normalizePrompt(
prompt: PromptInput | string | undefined,
): PromptInput {
if (typeof prompt === 'string') return { text: trimString(prompt) }
if (prompt && typeof prompt === 'object' && trimString(prompt.text)) {
return prompt
}
throw new Error('prompt.text is required')
}
function requireAgentId(agentId: string | undefined): string {
const id = trimString(agentId)
if (!id) throw new Error('agentId is required')
return id
}
/**
* Send a follow-up prompt to an existing active agent.
*
* @param params.agentId - Target agent id.
* @param params.prompt - Follow-up instruction text (or `{ text, images? }`).
* @returns The newly created run.
* @example
* import { createRun } from 'kody:@kentcdodds/cursor/runs'
* const { run } = await createRun({
* agentId: 'bc-00000000-0000-0000-0000-000000000001',
* prompt: 'Also add troubleshooting steps',
* })
* // => { run: { id: 'run-...', status: 'CREATING' } }
*/
export async function createRun(
params: CreateRunParams,
): Promise<CreateRunResult> {
const agentId = requireAgentId(params.agentId)
const body: Record<string, unknown> = {
prompt: normalizePrompt(params.prompt),
}
if (params.mcpServers) body.mcpServers = params.mcpServers
if (params.mode) body.mode = params.mode
return cursorApi<CreateRunResult>({
method: 'POST',
path: `/v1/agents/${agentId}/runs`,
body,
})
}
/**
* List runs for an agent, newest first.
*
* @param params.agentId - Agent id.
* @param params.limit - Page size (1–100); default 20.
* @returns Run summaries and optional `nextCursor`.
* @example
* import { listRuns } from 'kody:@kentcdodds/cursor/runs'
* const { items } = await listRuns({ agentId: 'bc-...', limit: 10 })
* // => { items: [{ id: 'run-...', status: 'FINISHED' }] }
*/
export async function listRuns(
params: ListRunsParams,
): Promise<ListRunsResult> {
const agentId = requireAgentId(params.agentId)
const query: Record<string, string | number> = {}
if (params.limit !== undefined) {
query.limit = clampInt(params.limit, 1, 100, 20)
}
if (params.cursor) query.cursor = params.cursor
return cursorApi<ListRunsResult>({
path: `/v1/agents/${agentId}/runs`,
query,
})
}
/**
* Retrieve status and result for a specific run.
*
* @param params.agentId - Agent id.
* @param params.runId - Run id.
* @returns Run details including terminal result and git branches.
* @example
* import { getRun } from 'kody:@kentcdodds/cursor/runs'
* const run = await getRun({ agentId: 'bc-...', runId: 'run-...' })
* // => { id: 'run-...', status: 'FINISHED', result: '...' }
*/
export async function getRun(params: {
agentId: string
runId: string
}): Promise<RunSummary> {
const agentId = requireAgentId(params.agentId)
const runId = trimString(params.runId)
if (!runId) throw new Error('runId is required')
return cursorApi<RunSummary>({
path: `/v1/agents/${agentId}/runs/${runId}`,
})
}
/**
* Cancel the active run for an agent.
*
* @param params.agentId - Agent id.
* @param params.runId - Run id to cancel.
* @returns Confirmation with run id.
* @example
* import { cancelRun } from 'kody:@kentcdodds/cursor/runs'
* await cancelRun({ agentId: 'bc-...', runId: 'run-...' })
* // => { id: 'run-...' }
*/
export async function cancelRun(params: { agentId: string; runId: string }) {
const agentId = requireAgentId(params.agentId)
const runId = trimString(params.runId)
if (!runId) throw new Error('runId is required')
return cursorApi({
method: 'POST',
path: `/v1/agents/${agentId}/runs/${runId}/cancel`,
})
}
export type GetAgentUsageParams = {
agentId: string
runId?: string
}
/**
* Retrieve token usage for an agent, optionally scoped to one run.
*
* @param params.agentId - Agent id.
* @param params.runId - Optional run id to scope usage.
* @returns Total and per-run token usage.
* @example
* import { getAgentUsage } from 'kody:@kentcdodds/cursor/runs'
* const usage = await getAgentUsage({ agentId: 'bc-...' })
* // => { totalUsage: { totalTokens: 76390 }, runs: [...] }
*/
export async function getAgentUsage(params: GetAgentUsageParams) {
const agentId = requireAgentId(params.agentId)
const query: Record<string, string> = {}
if (params.runId) query.runId = params.runId
return cursorApi({
path: `/v1/agents/${agentId}/usage`,
query,
})
}
export type ReadRunStreamParams = {
agentId: string
runId: string
parseEvents?: boolean
lastEventId?: string
}
export type StreamEvent = {
event?: string
id?: string
data: unknown
}
/**
* Buffer-read the SSE stream for a run (full body, not incremental).
*
* @param params.agentId - Agent id.
* @param params.runId - Run id.
* @param params.parseEvents - When true, parse SSE into event objects.
* @returns Raw stream text or parsed events. Buffers the entire response.
* @example
* import { readRunStream } from 'kody:@kentcdodds/cursor/runs'
* const stream = await readRunStream({
* agentId: 'bc-...',
* runId: 'run-...',
* parseEvents: true,
* })
* // => { events: [{ event: 'assistant', data: { text: '...' } }] }
*/
export async function readRunStream(params: ReadRunStreamParams) {
const agentId = requireAgentId(params.agentId)
const runId = trimString(params.runId)
if (!runId) throw new Error('runId is required')
const headers: Record<string, string> = {}
if (params.lastEventId) headers['Last-Event-ID'] = params.lastEventId
const result = await cursorRequestRaw({
path: `/v1/agents/${agentId}/runs/${runId}/stream`,
accept: 'text/event-stream',
headers,
})
const text = typeof result.body === 'string' ? result.body : ''
const retention = result.headers['x-cursor-stream-retention-seconds'] || null
if (!params.parseEvents) {
return {
status: result.status,
text,
retentionSeconds: retention,
}
}
const events: StreamEvent[] = []
const blocks = text.split(/\n\n+/)
for (const block of blocks) {
if (!block.trim()) continue
const lines = block.split('\n')
let event: string | undefined
let id: string | undefined
let dataLine = ''
for (const line of lines) {
if (line.startsWith('event:')) event = line.slice(6).trim()
else if (line.startsWith('id:')) id = line.slice(3).trim()
else if (line.startsWith('data:')) dataLine += line.slice(5).trim()
}
let data: unknown = dataLine
if (dataLine) {
try {
data = JSON.parse(dataLine)
} catch {
data = dataLine
}
}
events.push({ event, id, data })
}
return {
status: result.status,
events,
retentionSeconds: retention,
}
}
/** Default export: list runs for an agent. */
export default listRuns