import { cursorRequest } from './request.ts' import { CursorApiError, trimString } from './lib/client.ts' export type GetAgentUsageParams = { agentId: string runId?: string } export type AgentUsageTokens = { inputTokens?: number outputTokens?: number cacheReadTokens?: number cacheWriteTokens?: number totalTokens?: number [key: string]: unknown } export type AgentUsageCost = { rawCostCents: number chargedCents: number rawUsd: number chargedUsd: number } export type AgentUsageRun = { id?: string usage?: AgentUsageTokens cost?: AgentUsageCost [key: string]: unknown } export type AgentUsageResult = { agentId: string totalUsage?: AgentUsageTokens cost?: AgentUsageCost runs: AgentUsageRun[] } function asRecord(value: unknown): Record | null { return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record) : null } function asNumber(value: unknown): number | undefined { return typeof value === 'number' && Number.isFinite(value) ? value : undefined } function centsToUsd(cents: number): number { return cents / 100 } /** * Slim API `cost` (cents) into billed dollars. Returns undefined when the * billed record is missing — callers must not invent a dollar amount. */ function slimCost(value: unknown): AgentUsageCost | undefined { const record = asRecord(value) if (!record) return undefined const rawCostCents = asNumber(record.rawCostCents) const chargedCents = asNumber(record.chargedCents) if (rawCostCents === undefined && chargedCents === undefined) return undefined const raw = rawCostCents ?? 0 const charged = chargedCents ?? 0 return { rawCostCents: raw, chargedCents: charged, rawUsd: centsToUsd(raw), chargedUsd: centsToUsd(charged), } } function slimUsage(value: unknown): AgentUsageTokens | undefined { const record = asRecord(value) if (!record) return undefined return record as AgentUsageTokens } function slimRun(value: unknown): AgentUsageRun | null { const record = asRecord(value) if (!record) return null const run: AgentUsageRun = {} if (typeof record.id === 'string') run.id = record.id const usage = slimUsage(record.usage ?? record.totalUsage) if (usage) run.usage = usage const cost = slimCost(record.cost) if (cost) run.cost = cost return run } /** * Retrieve billed token usage for a Cursor Cloud Agent. * * Wraps `GET /v1/agents/{id}/usage` (optional `?runId=`). Dollars come from * the API `cost` record (`rawCostCents` / `chargedCents`) — never estimate * from token counts. `chargedCents === 0` means the tokens were plan-included. * * @param params.agentId - Cloud agent id (`bc-…`). * @param params.runId - Optional run id to scope usage. * @returns Slimmed usage: `{ agentId, totalUsage, cost?, runs }`. * @example * import { getAgentUsage } from 'kody:@kentcdodds/cursor/agent-usage' * const usage = await getAgentUsage({ agentId: 'bc-…' }) * // => { agentId: 'bc-…', totalUsage: { totalTokens: 21456045 }, * // cost: { rawCostCents: 2500.9084, chargedCents: 0, rawUsd: 25.009084, chargedUsd: 0 }, * // runs: […] } * * @see https://cursor.com/docs/cloud-agent/api/endpoints */ export async function getAgentUsage( params: GetAgentUsageParams, ): Promise { const agentId = trimString(params.agentId) if (!agentId) throw new Error('agentId is required') const query: Record = {} const runId = trimString(params.runId) if (runId) query.runId = runId const result = await cursorRequest({ path: `/v1/agents/${encodeURIComponent(agentId)}/usage`, query, }) if (result.status < 200 || result.status >= 300) { throw new CursorApiError(result.status, result.body) } const body = asRecord(result.body) ?? {} const runs = Array.isArray(body.runs) ? body.runs.map(slimRun).filter((run): run is AgentUsageRun => run !== null) : [] const slimmed: AgentUsageResult = { agentId, runs, } const totalUsage = slimUsage(body.totalUsage) if (totalUsage) slimmed.totalUsage = totalUsage const cost = slimCost(body.cost) if (cost) slimmed.cost = cost return slimmed } /** Default export: billed agent usage (tokens + API cost record). */ export default getAgentUsage