import { BoxApi } from './apis/BoxApi'; import type { Box, BoxEvent, DesktopResponse, PromptRequest, PromptRun } from './models'; export type WaitOptions = { timeoutMs?: number; intervalMs?: number; }; export type StreamEventsOptions = WaitOptions & { type?: string; limit?: number; includeExisting?: boolean; signal?: AbortSignal; }; export type StreamPromptOptions = WaitOptions & { signal?: AbortSignal; }; const terminalPromptStatuses = new Set(['finished', 'failed']); function delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } async function poll(options: WaitOptions | undefined, fn: () => Promise): Promise { const timeoutMs = options?.timeoutMs ?? 300_000; const intervalMs = options?.intervalMs ?? 2_000; const deadline = timeoutMs === 0 ? Number.POSITIVE_INFINITY : Date.now() + timeoutMs; for (;;) { const value = await fn(); if (value !== undefined) return value; if (Date.now() >= deadline) throw new Error('Timed out waiting for Box state'); await delay(intervalMs); } } export async function waitUntilReady(api: BoxApi, boxId: string, options?: WaitOptions): Promise { return poll(options, async () => { const box = (await api.get({ boxId })).box; if (['ready', 'idle', 'running'].indexOf(box.state) !== -1) return box; if (['archived', 'archiving', 'error'].indexOf(box.state) !== -1) throw new Error(`Box entered terminal state ${box.state}`); return undefined; }); } export async function waitUntilIdle(api: BoxApi, boxId: string, options?: WaitOptions): Promise { return poll({ timeoutMs: 600_000, intervalMs: 2_000, ...options }, async () => { const box = (await api.get({ boxId })).box; if (box.state === 'idle') return box; if (['archived', 'archiving', 'error'].indexOf(box.state) !== -1) throw new Error(`Box entered terminal state ${box.state}`); return undefined; }); } export async function waitForDesktop(api: BoxApi, boxId: string, options?: WaitOptions & { vnc?: number; publicAccess?: boolean }): Promise { return poll(options, async () => { const desktopRequest = options?.publicAccess ? { publicAccess: true } : {}; const desktop = await api.desktop({ boxId, vnc: (options?.vnc ?? 1) as 1, desktopRequest }); return desktop.desktopUrl && !desktop.provisioning ? desktop : undefined; }); } export async function waitForPrompt(api: BoxApi, boxId: string, promptId: string, options?: WaitOptions): Promise { return poll({ timeoutMs: 1_800_000, intervalMs: 2_000, ...options }, async () => { const run = (await api.promptRunStatus({ boxId, promptId })).promptRun; return terminalPromptStatuses.has(run.status) ? run : undefined; }); } export const waitForPromptDone = waitForPrompt; function encodeCursor(event: { timestamp?: number; id?: string }): string { const raw = `${event.timestamp}:${event.id}`; const bufferCtor = (globalThis as any).Buffer; if (bufferCtor) return bufferCtor.from(raw, 'utf8').toString('base64url'); return btoa(raw).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); } function eventStatus(event: BoxEvent): string | undefined { const data = (event as any).data; return typeof data?.status === 'string' ? data.status : undefined; } function throwIfAborted(signal?: AbortSignal): void { if (signal?.aborted) throw new Error('Box event stream aborted'); } /** * Long-poll Box events and yield each new event once. * * The v1 API does not require a separate SSE/WebSocket connection. This helper * polls `events({ sort: "asc" })`, advances a cursor after every event, and * yields response events as soon as they are persisted. Streaming response * partials have `event.data.isStreaming === true` (or `is_streaming` in raw * JSON); tool-call events are response events whose `event.data.tools` array is * present. */ export async function* streamEvents(api: BoxApi, boxId: string, options?: StreamEventsOptions): AsyncGenerator { const limit = options?.limit ?? 100; const intervalMs = options?.intervalMs ?? 1_000; const timeoutMs = options?.timeoutMs ?? 0; const deadline = timeoutMs === 0 ? Number.POSITIVE_INFINITY : Date.now() + timeoutMs; let cursor: string | undefined; if (!options?.includeExisting) { const existing = (await api.events({ boxId, limit: 200, sort: 'asc', type: options?.type })).events ?? []; const last = existing[existing.length - 1] as any; if (last?.id && last?.timestamp) cursor = encodeCursor(last); } for (;;) { throwIfAborted(options?.signal); const page = await api.events({ boxId, limit, cursor, sort: 'asc', type: options?.type }); for (const event of page.events ?? []) { cursor = encodeCursor(event as any); yield event; } if (Date.now() >= deadline) throw new Error('Timed out streaming Box events'); await delay(intervalMs); } } /** * Queue a prompt, then yield its prompt/response/tool events until the prompt * reaches `finished` or `failed`. */ export async function* streamPrompt( api: BoxApi, boxId: string, promptRequest: PromptRequest, options?: StreamPromptOptions, ): AsyncGenerator { const existing = (await api.events({ boxId, limit: 200, sort: 'asc' })).events ?? []; let cursor: string | undefined; const last = existing[existing.length - 1] as any; if (last?.id && last?.timestamp) cursor = encodeCursor(last); const response = await api.prompt({ boxId, promptRequest }); const promptId = response.promptId; const intervalMs = options?.intervalMs ?? 1_000; const timeoutMs = options?.timeoutMs ?? 1_800_000; const deadline = timeoutMs === 0 ? Number.POSITIVE_INFINITY : Date.now() + timeoutMs; for (;;) { throwIfAborted(options?.signal); const page = await api.events({ boxId, limit: 100, cursor, sort: 'asc', type: 'prompt,response,usage_limit,shield' }); for (const event of page.events ?? []) { cursor = encodeCursor(event as any); if ((event as any).taskId !== promptId) continue; yield event; if (event.type === 'prompt' && terminalPromptStatuses.has(eventStatus(event) ?? '')) return; } const run = (await api.promptRunStatus({ boxId, promptId })).promptRun; if (terminalPromptStatuses.has(run.status)) return; if (Date.now() >= deadline) throw new Error('Timed out streaming prompt events'); await delay(intervalMs); } } /** * Stop a Box, or delete it outright. * * The default stops the Box and keeps its snapshots, so it can be resumed or * forked later. `options.delete` is irreversible: the Box is force-stopped * without a final snapshot and every snapshot chain only it uses is deleted. * * The generated method is `deleteBox`, not `delete` -- `delete` is a reserved * word the generators rename, and calling a method the SDK does not actually * expose is what broke the TypeScript build (TS2339) and stopped it publishing. */ export async function stopAndRemove(api: BoxApi, boxId: string, options?: { delete?: boolean }) { if (options?.delete) return api.deleteBox({ boxId, xAsciiConfirmDelete: boxId }); return api.stop({ boxId }); } export async function readText(api: BoxApi, boxId: string, path: string): Promise { return (await api.readFile({ boxId, path, encoding: 'utf8' })).content; } export async function writeText(api: BoxApi, boxId: string, path: string, content: string) { return api.writeFile({ boxId, fileWriteRequest: { path, content, encoding: 'utf8' } }); } export async function execCommand(api: BoxApi, boxId: string, command: string, cwd?: string, timeoutSeconds = 30) { return api.command({ boxId, commandRequest: { command, cwd, timeoutSeconds } }); }