import { cursorApi, trimString } from './lib/client.ts'
export type ArtifactItem = {
path: string
sizeBytes?: number
updatedAt?: string
[key: string]: unknown
}
export type ListArtifactsResult = {
items: ArtifactItem[]
[key: string]: unknown
}
function requireAgentId(agentId: string | undefined): string {
const id = trimString(agentId)
if (!id) throw new Error('agentId is required')
return id
}
/**
* List artifacts produced by an agent.
*
* @param params.agentId - Agent id.
* @returns Artifact paths relative to the workspace `artifacts/` directory.
* @example
* import { listArtifacts } from 'kody:@kentcdodds/cursor/artifacts'
* const { items } = await listArtifacts({ agentId: 'bc-...' })
* // => { items: [{ path: 'artifacts/screenshot.png', sizeBytes: 12345 }] }
*/
export async function listArtifacts(params: {
agentId: string
}): Promise<ListArtifactsResult> {
const agentId = requireAgentId(params.agentId)
return cursorApi<ListArtifactsResult>({
path: `/v1/agents/${agentId}/artifacts`,
})
}
const TEXT_TYPES = new Set([
'text/plain',
'text/html',
'text/css',
'text/csv',
'application/json',
'application/xml',
'text/markdown',
'application/javascript',
])
function isTextualContentType(contentType: string): boolean {
const base = contentType.split(';')[0].trim().toLowerCase()
return TEXT_TYPES.has(base) || base.startsWith('text/')
}
export type DownloadArtifactResult =
| { contentType: string; text: string }
| { contentType: string; base64: string }
/**
* Download an artifact by path (fetches presigned URL content).
*
* @param params.agentId - Agent id.
* @param params.path - Relative artifact path from listArtifacts.
* @returns Text for textual content, or base64 for binary.
* @example
* import { downloadArtifact } from 'kody:@kentcdodds/cursor/artifacts'
* const file = await downloadArtifact({
* agentId: 'bc-...',
* path: 'artifacts/screenshot.png',
* })
* // => { contentType: 'image/png', base64: '...' }
*/
export async function downloadArtifact(params: {
agentId: string
path: string
}): Promise<DownloadArtifactResult> {
const agentId = requireAgentId(params.agentId)
const path = trimString(params.path)
if (!path) throw new Error('path is required')
const presign = (await cursorApi<{ url?: string; expiresAt?: string }>({
path: `/v1/agents/${agentId}/artifacts/download`,
query: { path },
})) as { url?: string }
const url = trimString(presign.url)
if (!url) throw new Error('Artifact download URL was not returned')
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Artifact fetch ${response.status}`)
}
const contentType =
response.headers.get('content-type') || 'application/octet-stream'
const buffer = await response.arrayBuffer()
if (isTextualContentType(contentType)) {
const text = new TextDecoder().decode(buffer)
return { contentType, text }
}
const bytes = new Uint8Array(buffer)
let binary = ''
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i])
}
return { contentType, base64: btoa(binary) }
}
/** Default export: list artifacts for an agent. */
export default listArtifacts