import http2 from "node:http2"; import { type JsonValue } from "@bufbuild/protobuf"; import type { McpToolDefinition } from "@oh-my-pi/pi-catalog/discovery/cursor-gen/agent_pb"; import { type AgentServerMessage, type ConversationStateStructure } from "@oh-my-pi/pi-catalog/discovery/cursor-gen/agent_pb"; import type { AssistantMessage, CursorExecHandlerResult, CursorExecHandlers, CursorToolResultHandler, Message, StreamFunction, StreamOptions, TextContent, ThinkingContent, ToolCall, ToolResultMessage } from "../types.js"; import { kCursorExecResolved, kStreamingBlockIndex, kStreamingBlockKind, kStreamingLastParseLen, kStreamingPartialJson } from "../utils/block-symbols.js"; import { AssistantMessageEventStream } from "../utils/event-stream.js"; export declare const CURSOR_API_URL = "https://api2.cursor.sh"; export declare const CURSOR_CLIENT_VERSION = "cli-2026.01.09-231024f"; export interface CursorOptions extends StreamOptions { customSystemPrompt?: string; conversationId?: string; execHandlers?: CursorExecHandlers; onToolResult?: CursorToolResultHandler; } export declare const streamCursor: StreamFunction<"cursor-agent">; export type ToolCallState = ToolCall & { [kStreamingBlockIndex]: number; [kStreamingPartialJson]?: string; [kStreamingLastParseLen]?: number; [kStreamingBlockKind]: "mcp" | "todo" | "cursor-exec"; [kCursorExecResolved]?: true; }; export interface BlockState { currentTextBlock: (TextContent & { [kStreamingBlockIndex]: number; }) | null; currentThinkingBlock: (ThinkingContent & { [kStreamingBlockIndex]: number; }) | null; currentToolCall: ToolCallState | null; firstTokenTime: number | undefined; setTextBlock: (b: (TextContent & { [kStreamingBlockIndex]: number; }) | null) => void; setThinkingBlock: (b: (ThinkingContent & { [kStreamingBlockIndex]: number; }) | null) => void; setToolCall: (t: ToolCallState | null) => void; setFirstTokenTime: () => void; } export interface UsageState { sawTokenDelta: boolean; } /** Exported for tests: drives one Cursor server message through the stream (exec waits mark the stream busy). */ export declare function handleServerMessage(msg: AgentServerMessage, output: AssistantMessage, stream: AssistantMessageEventStream, state: BlockState, blobStore: Map, h2Request: http2.ClientHttp2Stream, execHandlers: CursorExecHandlers | undefined, onToolResult: CursorToolResultHandler | undefined, usageState: UsageState, requestContextTools: McpToolDefinition[], onConversationCheckpoint?: (checkpoint: ConversationStateStructure) => void): Promise; /** Exported for tests: verifies handler is invoked with correct `this` when passed as bound. */ export declare function resolveExecHandler(args: TArgs, handler: ((args: TArgs) => Promise>) | undefined, onToolResult: CursorToolResultHandler | undefined, buildFromToolResult: (toolResult: ToolResultMessage) => TResult, buildRejected: (reason: string) => TResult, buildError: (error: string) => TResult): Promise<{ execResult: TResult; toolResult?: ToolResultMessage; }>; /** * Reject a Cursor exec-channel `grepArgs` frame whose `pattern` is empty or * whitespace-only. Returns an actionable error message when the pattern is * unusable (with a `glob`-aware hint when the model likely meant to list * files), or `null` when the pattern is valid and grep should run. * * Exported for tests. Cursor's model sometimes sends `pattern=""` together * with a non-empty `glob`, expecting grep to enumerate matching files; the * downstream coding-agent `grep` tool rejects that with a bare "Pattern must * not be empty", which the TUI renders as `?` in the tool preview (issue * #4574). Handling it at the Cursor exec dispatch keeps the synthesized * `toolCall` block off the persisted assistant message and gives the model a * specific recovery hint. */ export declare function emptyGrepPatternRejection(pattern: string | undefined, glob: string | undefined): string | null; /** * Merge the decoded completion-frame `McpArgs` map into the args assembled * from streamed `args_text_delta` snapshots. * * The completion frame is authoritative for the scalars it carries — but it * can omit oversized parameters entirely and can downgrade a structured value * to its raw string fallback when `decodeMcpArgValue` cannot parse it as * JSON. Overwriting the streamed args wholesale therefore loses data (e.g. * the task tool's `tasks` array on multi-subagent dispatches, issue #2615). * * Rules per key: * - completion key absent → keep the streamed value. * - completion is a string while the streamed value is structured (object or * array) → keep the streamed value (the completion frame downgraded it). * - otherwise → completion wins. */ export declare function mergeCursorMcpToolCallArgs(streamed: Record | undefined, completion: Record | undefined): Record; /** * Synthesize a completed `toolCall` content block for a Cursor exec-channel * native tool (`shell`, `read`, `write`, `grep`, `ls`, `delete`, `diagnostics`). * * Args arrive complete on the exec message, so the block opens and closes in * one step — no partial-JSON streaming path. Without this the persisted * assistant message carries only text/thinking blocks, and on replay the * following `toolResult` messages have no matching `toolCall.id` in * `renderSessionContext`, so they render as header-less `⎿` lines beneath the * last text block instead of proper tool components (issue #4348). * * The block is stamped with {@link kCursorExecResolved} so the shared * `agent-loop.ts` execution pass skips it — Cursor's server-driven exec * channel already ran the tool via the bridge and buffered the result, so * treating this block as runnable would re-execute the same side-effecting * tool a second time. * * Exported for tests to exercise ordering with adjacent text/thinking blocks. */ export declare function synthesizeCursorExecToolCall(output: AssistantMessage, stream: AssistantMessageEventStream, state: BlockState, toolCallId: string, toolName: string, args: Record): void; /** Exported for tests: drives one Cursor interaction update through the streaming state machine. */ export declare function processInteractionUpdate(update: any, output: AssistantMessage, stream: AssistantMessageEventStream, state: BlockState, usageState: UsageState): void; /** * Build `ConversationStateStructure.rootPromptMessagesJson` blob IDs for the * system prompt plus prior conversation history, as JSON blobs matching * Cursor's internal Vercel-AI-SDK-shaped message format. * * Cursor's server uses `rootPromptMessagesJson` (not `turns[]`) to build the * actual model prompt. `turns[]` is UI/display metadata. Without populating * this field, multi-turn conversations lose prior context — the model sees * only an empty placeholder where historical user turns should be. * The active user message is excluded because it is sent in the action. */ /** * Build one Cursor system-message JSON blob per ordered system prompt. Emitting separate blobs * (rather than a single `\n\n`-joined string) lets Cursor's blob cache hit independently per * entry: changing only the last prompt does not invalidate earlier blob ids, so the prefix * up to the changed prompt remains cached on the server side. * * When no system prompts are provided, returns a single default greeting so we never emit * an empty `rootPromptMessagesJson` head. */ export declare function buildCursorSystemPromptJsons(systemPrompt: readonly string[] | undefined): string[]; /** Exported for tests: decodes Cursor history blobs built from conversation messages. */ export declare function buildCursorHistoryForTest(messages: Message[], activeUserMessageIndex?: number): { rootPromptMessagesJson: unknown[]; turnUserMessagesJson: JsonValue[]; turnStepMessagesJson: JsonValue[][]; };