import { Agent, type AgentOptions, type RunResult } from "@cursor/sdk";import * as Effect from "effect/Effect";import * as Option from "effect/Option";import * as Schema from "effect/Schema"; import { type CursorSettings, type ModelSelection } from "@t3tools/contracts";import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git";import { extractJsonObject } from "@t3tools/shared/schemaJson"; import { TextGenerationError } from "@t3tools/contracts";import * as TextGeneration from "./TextGeneration.ts";import { buildBranchNamePrompt, buildCommitMessagePrompt, buildPrContentPrompt, buildThreadTitlePrompt,} from "./TextGenerationPrompts.ts";import { sanitizeCommitSubject, sanitizePrTitle, sanitizeThreadTitle,} from "./TextGenerationUtils.ts";import { cursorSdkModelSelection } from "../provider/cursorSdkModel.ts"; const CURSOR_TIMEOUT_MS = 180_000; const isTextGenerationError = Schema.is(TextGenerationError);type CursorTextGenerationOperation = | "generateCommitMessage" | "generatePrContent" | "generateBranchName" | "generateThreadTitle"; function emptyCursorSdkResultDetail(result: RunResult): string { switch (result.status) { case "cancelled": return "Cursor SDK request was cancelled."; case "error": return "Cursor SDK request finished with an error and no output."; case "finished": return "Cursor SDK returned empty output."; }} /** * Build a Cursor text-generation closure bound to a specific `CursorSettings` * payload. See `makeCodexAdapter` for the overall per-instance rationale. */export const makeCursorTextGeneration = Effect.fn("makeCursorTextGeneration")(( cursorSettings: CursorSettings, environment?: NodeJS.ProcessEnv,) => { const resolvedEnvironment = environment ?? process.env; const resolveCursorApiKey = (operation: CursorTextGenerationOperation) => Effect.gen(function* () { if (!cursorSettings.enabled) { return yield* new TextGenerationError({ operation, detail: "Cursor is disabled in T3 Code settings.", }); } const apiKey = resolvedEnvironment.CURSOR_API_KEY?.trim(); if (!apiKey) { return yield* new TextGenerationError({ operation, detail: "Cursor API key is required. Add CURSOR_API_KEY in provider settings.", }); } return apiKey; }); const runCursorJson = <S extends Schema.Top>({ operation, cwd, prompt, outputSchemaJson, modelSelection, }: { operation: CursorTextGenerationOperation; cwd: string; prompt: string; outputSchemaJson: S; modelSelection: ModelSelection; }): Effect.Effect<S["Type"], TextGenerationError, S["DecodingServices"]> => Effect.gen(function* () { const apiKey = yield* resolveCursorApiKey(operation); const agentOptions = { apiKey, mode: "agent", model: cursorSdkModelSelection(modelSelection), local: { cwd, autoReview: false, sandboxOptions: { enabled: false }, enableAgentRetries: true, }, } satisfies AgentOptions; const promptResult = yield* Effect.tryPromise({ try: () => Agent.prompt(prompt, agentOptions), catch: (cause) => new TextGenerationError({ operation, detail: "Cursor SDK request failed.", cause, }), }).pipe( Effect.timeoutOption(CURSOR_TIMEOUT_MS), Effect.flatMap( Option.match({ onNone: () => Effect.fail( new TextGenerationError({ operation, detail: "Cursor SDK request timed out.", }), ), onSome: (value) => Effect.succeed(value), }), ), ); const rawResult = promptResult.result?.trim() ?? ""; if (!rawResult) { return yield* new TextGenerationError({ operation, detail: emptyCursorSdkResultDetail(promptResult), }); } const decodeOutput = Schema.decodeEffect(Schema.fromJsonString(outputSchemaJson)); return yield* decodeOutput(extractJsonObject(rawResult)).pipe( Effect.catchTags({ SchemaError: (cause) => Effect.fail( new TextGenerationError({ operation, detail: "Cursor SDK returned invalid structured output.", cause, }), ), }), ); }).pipe( Effect.mapError((cause) => isTextGenerationError(cause) ? cause : new TextGenerationError({ operation, detail: "Cursor SDK text generation failed.", cause, }), ), ); const generateCommitMessage: TextGeneration.TextGeneration["Service"]["generateCommitMessage"] = Effect.fn("CursorTextGeneration.generateCommitMessage")(function* (input) { const { prompt, outputSchema } = buildCommitMessagePrompt({ branch: input.branch, stagedSummary: input.stagedSummary, stagedPatch: input.stagedPatch, includeBranch: input.includeBranch === true, policy: input.policy, }); const generated = yield* runCursorJson({ operation: "generateCommitMessage", cwd: input.cwd, prompt, outputSchemaJson: outputSchema, modelSelection: input.modelSelection, }); return { subject: sanitizeCommitSubject(generated.subject), body: generated.body.trim(), ...("branch" in generated && typeof generated.branch === "string" ? { branch: sanitizeFeatureBranchName(generated.branch) } : {}), }; }); const generatePrContent: TextGeneration.TextGeneration["Service"]["generatePrContent"] = Effect.fn("CursorTextGeneration.generatePrContent")(function* (input) { const { prompt, outputSchema } = buildPrContentPrompt({ baseBranch: input.baseBranch, headBranch: input.headBranch, commitSummary: input.commitSummary, diffSummary: input.diffSummary, diffPatch: input.diffPatch, policy: input.policy, changeRequestTemplate: input.changeRequestTemplate, }); const generated = yield* runCursorJson({ operation: "generatePrContent", cwd: input.cwd, prompt, outputSchemaJson: outputSchema, modelSelection: input.modelSelection, }); return { title: sanitizePrTitle(generated.title), body: generated.body.trim(), }; }); const generateBranchName: TextGeneration.TextGeneration["Service"]["generateBranchName"] = Effect.fn("CursorTextGeneration.generateBranchName")(function* (input) { const { prompt, outputSchema } = buildBranchNamePrompt({ message: input.message, attachments: input.attachments, }); const generated = yield* runCursorJson({ operation: "generateBranchName", cwd: input.cwd, prompt, outputSchemaJson: outputSchema, modelSelection: input.modelSelection, }); return { branch: sanitizeBranchFragment(generated.branch), }; }); const generateThreadTitle: TextGeneration.TextGeneration["Service"]["generateThreadTitle"] = Effect.fn("CursorTextGeneration.generateThreadTitle")(function* (input) { const { prompt, outputSchema } = buildThreadTitlePrompt({ message: input.message, previousTitle: input.previousTitle, attachments: input.attachments, }); const generated = yield* runCursorJson({ operation: "generateThreadTitle", cwd: input.cwd, prompt, outputSchemaJson: outputSchema, modelSelection: input.modelSelection, }); return { title: sanitizeThreadTitle(generated.title), } satisfies TextGeneration.ThreadTitleGenerationResult; }); return Effect.succeed({ generateCommitMessage, generatePrContent, generateBranchName, generateThreadTitle, } satisfies TextGeneration.TextGeneration["Service"]);});
import { Agent, type AgentOptions, type RunResult } from "@cursor/sdk";
import * as Effect from "effect/Effect";
import * as Option from "effect/Option";
import * as Schema from "effect/Schema";
import { type CursorSettings, type ModelSelection } from "@t3tools/contracts";
import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git";
import { extractJsonObject } from "@t3tools/shared/schemaJson";
import { TextGenerationError } from "@t3tools/contracts";
import * as TextGeneration from "./TextGeneration.ts";
import {
buildBranchNamePrompt,
buildCommitMessagePrompt,
buildPrContentPrompt,
buildThreadTitlePrompt,
} from "./TextGenerationPrompts.ts";
import {
sanitizeCommitSubject,
sanitizePrTitle,
sanitizeThreadTitle,
} from "./TextGenerationUtils.ts";
import { cursorSdkModelSelection } from "../provider/cursorSdkModel.ts";
const CURSOR_TIMEOUT_MS = 180_000;
const isTextGenerationError = Schema.is(TextGenerationError);
type CursorTextGenerationOperation =
| "generateCommitMessage"
| "generatePrContent"
| "generateBranchName"
| "generateThreadTitle";
function emptyCursorSdkResultDetail(result: RunResult): string {
switch (result.status) {
case "cancelled":
return "Cursor SDK request was cancelled.";
case "error":
return "Cursor SDK request finished with an error and no output.";
case "finished":
return "Cursor SDK returned empty output.";
}
}
/**
* Build a Cursor text-generation closure bound to a specific `CursorSettings`
* payload. See `makeCodexAdapter` for the overall per-instance rationale.
*/
export const makeCursorTextGeneration = Effect.fn("makeCursorTextGeneration")((
cursorSettings: CursorSettings,
environment?: NodeJS.ProcessEnv,
) => {
const resolvedEnvironment = environment ?? process.env;
const resolveCursorApiKey = (operation: CursorTextGenerationOperation) =>
Effect.gen(function* () {
if (!cursorSettings.enabled) {
return yield* new TextGenerationError({
operation,
detail: "Cursor is disabled in T3 Code settings.",
});
}
const apiKey = resolvedEnvironment.CURSOR_API_KEY?.trim();
if (!apiKey) {
return yield* new TextGenerationError({
operation,
detail: "Cursor API key is required. Add CURSOR_API_KEY in provider settings.",
});
}
return apiKey;
});
const runCursorJson = <S extends Schema.Top>({
operation,
cwd,
prompt,
outputSchemaJson,
modelSelection,
}: {
operation: CursorTextGenerationOperation;
cwd: string;
prompt: string;
outputSchemaJson: S;
modelSelection: ModelSelection;
}): Effect.Effect<S["Type"], TextGenerationError, S["DecodingServices"]> =>
Effect.gen(function* () {
const apiKey = yield* resolveCursorApiKey(operation);
const agentOptions = {
apiKey,
mode: "agent",
model: cursorSdkModelSelection(modelSelection),
local: {
cwd,
autoReview: false,
sandboxOptions: { enabled: false },
enableAgentRetries: true,
},
} satisfies AgentOptions;
const promptResult = yield* Effect.tryPromise({
try: () => Agent.prompt(prompt, agentOptions),
catch: (cause) =>
new TextGenerationError({
operation,
detail: "Cursor SDK request failed.",
cause,
}),
}).pipe(
Effect.timeoutOption(CURSOR_TIMEOUT_MS),
Effect.flatMap(
Option.match({
onNone: () =>
Effect.fail(
new TextGenerationError({
operation,
detail: "Cursor SDK request timed out.",
}),
),
onSome: (value) => Effect.succeed(value),
}),
),
);
const rawResult = promptResult.result?.trim() ?? "";
if (!rawResult) {
return yield* new TextGenerationError({
operation,
detail: emptyCursorSdkResultDetail(promptResult),
});
}
const decodeOutput = Schema.decodeEffect(Schema.fromJsonString(outputSchemaJson));
return yield* decodeOutput(extractJsonObject(rawResult)).pipe(
Effect.catchTags({
SchemaError: (cause) =>
Effect.fail(
new TextGenerationError({
operation,
detail: "Cursor SDK returned invalid structured output.",
cause,
}),
),
}),
);
}).pipe(
Effect.mapError((cause) =>
isTextGenerationError(cause)
? cause
: new TextGenerationError({
operation,
detail: "Cursor SDK text generation failed.",
cause,
}),
),
);
const generateCommitMessage: TextGeneration.TextGeneration["Service"]["generateCommitMessage"] =
Effect.fn("CursorTextGeneration.generateCommitMessage")(function* (input) {
const { prompt, outputSchema } = buildCommitMessagePrompt({
branch: input.branch,
stagedSummary: input.stagedSummary,
stagedPatch: input.stagedPatch,
includeBranch: input.includeBranch === true,
policy: input.policy,
});
const generated = yield* runCursorJson({
operation: "generateCommitMessage",
cwd: input.cwd,
prompt,
outputSchemaJson: outputSchema,
modelSelection: input.modelSelection,
});
return {
subject: sanitizeCommitSubject(generated.subject),
body: generated.body.trim(),
...("branch" in generated && typeof generated.branch === "string"
? { branch: sanitizeFeatureBranchName(generated.branch) }
: {}),
};
});
const generatePrContent: TextGeneration.TextGeneration["Service"]["generatePrContent"] =
Effect.fn("CursorTextGeneration.generatePrContent")(function* (input) {
const { prompt, outputSchema } = buildPrContentPrompt({
baseBranch: input.baseBranch,
headBranch: input.headBranch,
commitSummary: input.commitSummary,
diffSummary: input.diffSummary,
diffPatch: input.diffPatch,
policy: input.policy,
changeRequestTemplate: input.changeRequestTemplate,
});
const generated = yield* runCursorJson({
operation: "generatePrContent",
cwd: input.cwd,
prompt,
outputSchemaJson: outputSchema,
modelSelection: input.modelSelection,
});
return {
title: sanitizePrTitle(generated.title),
body: generated.body.trim(),
};
});
const generateBranchName: TextGeneration.TextGeneration["Service"]["generateBranchName"] =
Effect.fn("CursorTextGeneration.generateBranchName")(function* (input) {
const { prompt, outputSchema } = buildBranchNamePrompt({
message: input.message,
attachments: input.attachments,
});
const generated = yield* runCursorJson({
operation: "generateBranchName",
cwd: input.cwd,
prompt,
outputSchemaJson: outputSchema,
modelSelection: input.modelSelection,
});
return {
branch: sanitizeBranchFragment(generated.branch),
};
});
const generateThreadTitle: TextGeneration.TextGeneration["Service"]["generateThreadTitle"] =
Effect.fn("CursorTextGeneration.generateThreadTitle")(function* (input) {
const { prompt, outputSchema } = buildThreadTitlePrompt({
message: input.message,
previousTitle: input.previousTitle,
attachments: input.attachments,
});
const generated = yield* runCursorJson({
operation: "generateThreadTitle",
cwd: input.cwd,
prompt,
outputSchemaJson: outputSchema,
modelSelection: input.modelSelection,
});
return {
title: sanitizeThreadTitle(generated.title),
} satisfies TextGeneration.ThreadTitleGenerationResult;
});
return Effect.succeed({
generateCommitMessage,
generatePrContent,
generateBranchName,
generateThreadTitle,
} satisfies TextGeneration.TextGeneration["Service"]);
});