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,