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 <cursoragent@cursor.com>
This commit is contained in:
Daan Schouteden
2026-06-03 10:42:54 +02:00
parent e9aafdf8c4
commit 376c499c81
2 changed files with 61 additions and 43 deletions
+49 -36
View File
@@ -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<ReviewResult> {
const sdk = (await import("@cursor/sdk")) as any;
const Agent = sdk.Agent;
if (!Agent) {
throw new Error("Cursor SDK Agent API is unavailable");
}
const agent = new Agent({
apiKey: input.apiKey,
runtime: "cloud",
model: input.model
});
const repoUrl = buildRepoUrl(input.giteaBaseUrl, input.owner, input.repo);
try {
const result = await withTimeout(
agent.prompt({
prompt: input.prompt
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"
);
const text = extractText(result);
const parsed = JSON.parse(text) as unknown;
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;
}
}
function extractText(result: unknown): string {
if (typeof result === "string") {
function buildRepoUrl(baseUrl: string, owner: string, repo: string): string {
return `${baseUrl.replace(/\/$/, "")}/${owner}/${repo}`;
}
function extractResponseText(result: string | undefined): string {
if (!result?.trim()) {
throw new Error("Cursor review returned empty result text");
}
return result;
}
}
if (result && typeof result === "object") {
const maybe = result as Record<string, unknown>;
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;
}
}
}
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<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
+6 -1
View File
@@ -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,