From 376c499c81dd5ac359023ffc92a40ec72c8ad80e Mon Sep 17 00:00:00 2001 From: Daan Schouteden Date: Wed, 3 Jun 2026 10:42:54 +0200 Subject: [PATCH] Fix Cursor SDK invocation to use static Agent.prompt API. Use cloud repo config with Gitea URL and head ref, parse RunResult output correctly, and log cursor run IDs for debugging. Co-authored-by: Cursor --- src/cursor/review-agent.ts | 97 +++++++++++++++++++++----------------- src/run/review-runner.ts | 7 ++- 2 files changed, 61 insertions(+), 43 deletions(-) diff --git a/src/cursor/review-agent.ts b/src/cursor/review-agent.ts index 025dd05..a7031ec 100644 --- a/src/cursor/review-agent.ts +++ b/src/cursor/review-agent.ts @@ -1,3 +1,5 @@ +import { Agent, CursorAgentError } from "@cursor/sdk"; +import { log } from "../logging/logger.js"; import { parseReviewResult, ReviewResult } from "./review-schema.js"; type RunReviewInput = { @@ -5,57 +7,68 @@ type RunReviewInput = { prompt: string; timeoutMs: number; model?: string; + giteaBaseUrl: string; + owner: string; + repo: string; + headRef: string; + correlationId?: string; }; export async function runCursorReview(input: RunReviewInput): Promise { - const sdk = (await import("@cursor/sdk")) as any; - const Agent = sdk.Agent; - if (!Agent) { - throw new Error("Cursor SDK Agent API is unavailable"); + const repoUrl = buildRepoUrl(input.giteaBaseUrl, input.owner, input.repo); + + try { + const result = await withTimeout( + Agent.prompt(input.prompt, { + apiKey: input.apiKey, + model: { id: input.model ?? "composer-2.5" }, + cloud: { + repos: [{ url: repoUrl, startingRef: input.headRef }], + skipReviewerRequest: true + } + }), + input.timeoutMs, + "Cursor review timed out" + ); + + log("info", "Cursor review run finished", { + correlation_id: input.correlationId, + cursor_run_id: result.id, + status: result.status, + duration_ms: result.durationMs + }); + + if (result.status === "error") { + throw new Error(`Cursor review run failed (${result.id})`); + } + + const text = extractResponseText(result.result); + const parsed = parseJsonFromText(text); + return parseReviewResult(parsed); + } catch (error) { + if (error instanceof CursorAgentError) { + throw new Error(`Cursor SDK startup failed: ${error.message}`); + } + throw error; } - - const agent = new Agent({ - apiKey: input.apiKey, - runtime: "cloud", - model: input.model - }); - - const result = await withTimeout( - agent.prompt({ - prompt: input.prompt - }), - input.timeoutMs, - "Cursor review timed out" - ); - - const text = extractText(result); - const parsed = JSON.parse(text) as unknown; - return parseReviewResult(parsed); } -function extractText(result: unknown): string { - if (typeof result === "string") { - return result; - } +function buildRepoUrl(baseUrl: string, owner: string, repo: string): string { + return `${baseUrl.replace(/\/$/, "")}/${owner}/${repo}`; +} - if (result && typeof result === "object") { - const maybe = result as Record; - if (typeof maybe.output_text === "string") { - return maybe.output_text; - } - if (typeof maybe.text === "string") { - return maybe.text; - } - if (Array.isArray(maybe.messages)) { - const last = maybe.messages.at(-1) as any; - const content = last?.content; - if (typeof content === "string") { - return content; - } - } +function extractResponseText(result: string | undefined): string { + if (!result?.trim()) { + throw new Error("Cursor review returned empty result text"); } + return result; +} - throw new Error("Could not extract Cursor response text"); +function parseJsonFromText(text: string): unknown { + const trimmed = text.trim(); + const fenceMatch = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i); + const candidate = fenceMatch?.[1]?.trim() ?? trimmed; + return JSON.parse(candidate); } async function withTimeout(promise: Promise, timeoutMs: number, message: string): Promise { diff --git a/src/run/review-runner.ts b/src/run/review-runner.ts index 7d08155..8a83ce9 100644 --- a/src/run/review-runner.ts +++ b/src/run/review-runner.ts @@ -102,7 +102,12 @@ export async function runReview(input: { apiKey: input.env.CURSOR_API_KEY, prompt, timeoutMs: input.env.REVIEW_TIMEOUT_MS, - model: repoConfig.model + model: repoConfig.model, + giteaBaseUrl: input.env.GITEA_BASE_URL, + owner, + repo, + headRef: pull.head.ref, + correlationId: input.correlationId }), retries: 2, initialDelayMs: 500,