import { spawn } from "node:child_process" import { randomUUID } from "node:crypto" import { homedir } from "node:os" import { createInterface } from "node:readline" import type { Readable, Writable } from "node:stream" import type { ContextWindowUsageSnapshot } from "../shared/types" import { asNumber, asRecord, asString } from "../shared/json" import { normalizeToolCall } from "../shared/tools" import type { HarnessEvent, HarnessTurn } from "./harness-types" import { AsyncQueue } from "./async-queue" import { timestamped } from "./transcript" /** * Adapter for the Cursor CLI (`cursor-agent` binary). * * Unlike Claude (SDK) and Codex (persistent JSON-RPC `app-server`), Cursor runs * one headless process per turn: * * cursor-agent -p --output-format stream-json --force --model [--resume ] * * with the prompt written to stdin. It emits NDJSON on stdout. `--force` is required * in headless mode, otherwise the process blocks on a "Workspace Trust" prompt. * Auth is via the CURSOR_API_KEY environment variable (inherited from the parent). * * Stream event types (one JSON object per line): * - { type: "system", subtype: "init", session_id, model, cwd, apiKeySource } * - { type: "user", ... } (prompt echo — ignored) * - { type: "assistant", message: { content: [{ type: "text", text }] } } * - { type: "thinking", subtype: "delta"|"completed" } (reasoning — ignored) * - { type: "tool_call", subtype: "started"|"completed", call_id, tool_call: { ToolCall: {...} } } * - { type: "result", subtype: "success", is_error, duration_ms, result, session_id, usage } */ // Minimal child-process surface so tests can inject a fake without rebuilding ChildProcess. export interface CursorChildProcess { readonly stdin: Writable | null readonly stdout: Readable | null readonly stderr: Readable | null kill(signal?: NodeJS.Signals): boolean once(event: "close", listener: (code: number | null) => void): unknown once(event: "error", listener: (err: Error) => void): unknown } export type SpawnCursorAgent = (args: { cwd: string; argv: string[] }) => CursorChildProcess export interface StartCursorTurnArgs { cwd: string content: string /** Concrete model id to spawn, e.g. "composer-2.5" or "composer-2.5-fast". */ model: string /** Previous Cursor session id to resume, if any. */ sessionToken: string | null } export interface CursorModelListEntry { id: string label: string isDefault: boolean } // `cursor-agent --list-models` renders for humans: ANSI erase/cursor-move // sequences around a "Loading models…" spinner, then one line per model: // -