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:
+55
-42
@@ -1,3 +1,5 @@
|
|||||||
|
import { Agent, CursorAgentError } from "@cursor/sdk";
|
||||||
|
import { log } from "../logging/logger.js";
|
||||||
import { parseReviewResult, ReviewResult } from "./review-schema.js";
|
import { parseReviewResult, ReviewResult } from "./review-schema.js";
|
||||||
|
|
||||||
type RunReviewInput = {
|
type RunReviewInput = {
|
||||||
@@ -5,57 +7,68 @@ type RunReviewInput = {
|
|||||||
prompt: string;
|
prompt: string;
|
||||||
timeoutMs: number;
|
timeoutMs: number;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
giteaBaseUrl: string;
|
||||||
|
owner: string;
|
||||||
|
repo: string;
|
||||||
|
headRef: string;
|
||||||
|
correlationId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function runCursorReview(input: RunReviewInput): Promise<ReviewResult> {
|
export async function runCursorReview(input: RunReviewInput): Promise<ReviewResult> {
|
||||||
const sdk = (await import("@cursor/sdk")) as any;
|
const repoUrl = buildRepoUrl(input.giteaBaseUrl, input.owner, input.repo);
|
||||||
const Agent = sdk.Agent;
|
|
||||||
if (!Agent) {
|
try {
|
||||||
throw new Error("Cursor SDK Agent API is unavailable");
|
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 {
|
function buildRepoUrl(baseUrl: string, owner: string, repo: string): string {
|
||||||
if (typeof result === "string") {
|
return `${baseUrl.replace(/\/$/, "")}/${owner}/${repo}`;
|
||||||
return result;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (result && typeof result === "object") {
|
function extractResponseText(result: string | undefined): string {
|
||||||
const maybe = result as Record<string, unknown>;
|
if (!result?.trim()) {
|
||||||
if (typeof maybe.output_text === "string") {
|
throw new Error("Cursor review returned empty result text");
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
|
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
|
||||||
|
|||||||
@@ -102,7 +102,12 @@ export async function runReview(input: {
|
|||||||
apiKey: input.env.CURSOR_API_KEY,
|
apiKey: input.env.CURSOR_API_KEY,
|
||||||
prompt,
|
prompt,
|
||||||
timeoutMs: input.env.REVIEW_TIMEOUT_MS,
|
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,
|
retries: 2,
|
||||||
initialDelayMs: 500,
|
initialDelayMs: 500,
|
||||||
|
|||||||
Reference in New Issue
Block a user