authorgravatar for 71498452+StiensWout@users.noreply.github.comWout Stiens <71498452+StiensWout@users.noreply.github.com> 2026-06-23 07:33:01+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-22 22:33:01-07:00
log3083d712eefb8009f7366ba53040eb2d477bfa33
tree0a147e9ebb2322bb8fbd93075f9e5f46a1808084
parent6672a1d21060c6c7b927726b6b91fbfe8c8337f3
signature Signed by PGP key B5690EEEBB952194

[codex] fix: clarify Cursor CLI setup error (#3519)

Co-authored-by: Codex <codex@openai.com>

2 files changed, 61 insertions(+), 12 deletions(-)

apps/server/src/provider/Layers/CursorProvider.test.ts+44-10
...@@ -4,6 +4,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices";...@@ -4,6 +4,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
4import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";5import * as FileSystem from "effect/FileSystem";
6import * as Path from "effect/Path";6import * as Path from "effect/Path";
7import type * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
7import { describe, expect, it } from "vite-plus/test";8import { describe, expect, it } from "vite-plus/test";
8import type * as EffectAcpSchema from "effect-acp/schema";9import type * as EffectAcpSchema from "effect-acp/schema";
9import type { CursorSettings } from "@t3tools/contracts";10import type { CursorSettings } from "@t3tools/contracts";
...@@ -24,7 +25,11 @@ import {...@@ -24,7 +25,11 @@ import {
24} from "./CursorProvider.ts";25} from "./CursorProvider.ts";
2526
26const runNode = <A, E>(27const runNode = <A, E>(
27 effect: Effect.Effect<A, E, FileSystem.FileSystem | Path.Path>,28 effect: Effect.Effect<
29 A,
30 E,
31 ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path
32 >,
28): Promise<A> => Effect.runPromise(effect.pipe(Effect.provide(NodeServices.layer)));33): Promise<A> => Effect.runPromise(effect.pipe(Effect.provide(NodeServices.layer)));
2934
30const resolveMockAgentPath = Effect.fn("resolveMockAgentPath")(function* () {35const resolveMockAgentPath = Effect.fn("resolveMockAgentPath")(function* () {
...@@ -293,6 +298,18 @@ const baseCursorSettings: CursorSettings = {...@@ -293,6 +298,18 @@ const baseCursorSettings: CursorSettings = {
293 apiEndpoint: "",298 apiEndpoint: "",
294 customModels: [],299 customModels: [],
295};300};
301const cursorAcpDiscoveryFailedMessage = [
302 "Cursor ACP model discovery failed.",
303 "Cursor CLI setup may be incomplete; install or enable the Cursor CLI, restart T3 Code, and try again.",
304 "See https://cursor.com/docs/cli/installation.",
305 "Check server logs for ACP details.",
306].join(" ");
307const missingCursorBinaryPath = "/definitely/not/installed/t3-cursor-agent";
308const cursorCliCommandMissingMessage = [
309 `Cursor CLI command \`${missingCursorBinaryPath}\` was not found.`,
310 `Install or enable the Cursor CLI, make sure \`${missingCursorBinaryPath}\` is on PATH, then restart T3 Code.`,
311 "See https://cursor.com/docs/cli/installation.",
312].join(" ");
296313
297describe("getCursorFallbackModels", () => {314describe("getCursorFallbackModels", () => {
298 it("does not publish any built-in cursor models before ACP discovery", () => {315 it("does not publish any built-in cursor models before ACP discovery", () => {
...@@ -338,12 +355,11 @@ describe("buildCursorProviderSnapshot", () => {...@@ -338,12 +355,11 @@ describe("buildCursorProviderSnapshot", () => {
338 auth: { status: "unauthenticated" },355 auth: { status: "unauthenticated" },
339 message: "Cursor Agent is not authenticated. Run `agent login` and try again.",356 message: "Cursor Agent is not authenticated. Run `agent login` and try again.",
340 },357 },
341 discoveryWarning: "Cursor ACP model discovery failed. Check server logs for details.",358 discoveryWarning: cursorAcpDiscoveryFailedMessage,
342 }),359 }),
343 ).toMatchObject({360 ).toMatchObject({
344 status: "error",361 status: "error",
345 message:362 message: `Cursor Agent is not authenticated. Run \`agent login\` and try again. ${cursorAcpDiscoveryFailedMessage}`,
346 "Cursor Agent is not authenticated. Run `agent login` and try again. Cursor ACP model discovery failed. Check server logs for details.",
347 models: [363 models: [
348 {364 {
349 slug: "claude-sonnet-4-6",365 slug: "claude-sonnet-4-6",
...@@ -411,10 +427,28 @@ describe("buildCursorCapabilitiesFromConfigOptions", () => {...@@ -411,10 +427,28 @@ describe("buildCursorCapabilitiesFromConfigOptions", () => {
411});427});
412428
413describe("checkCursorProviderStatus", () => {429describe("checkCursorProviderStatus", () => {
430 it("reports the install docs when the Cursor CLI command is missing", async () => {
431 const provider = await runNode(
432 checkCursorProviderStatus({
433 enabled: true,
434 binaryPath: missingCursorBinaryPath,
435 apiEndpoint: "",
436 customModels: [],
437 }),
438 );
439
440 expect(provider).toMatchObject({
441 installed: false,
442 status: "error",
443 auth: { status: "unknown" },
444 message: cursorCliCommandMissingMessage,
445 });
446 });
447
414 it("passes the injected environment to ACP model discovery", async () => {448 it("passes the injected environment to ACP model discovery", async () => {
415 const { requestLogPath, wrapperPath } = await runNode(makeProviderStatusEnvFixture());449 const { requestLogPath, wrapperPath } = await runNode(makeProviderStatusEnvFixture());
416450
417 const provider = await Effect.runPromise(451 const provider = await runNode(
418 checkCursorProviderStatus(452 checkCursorProviderStatus(
419 {453 {
420 enabled: true,454 enabled: true,
...@@ -426,7 +460,7 @@ describe("checkCursorProviderStatus", () => {...@@ -426,7 +460,7 @@ describe("checkCursorProviderStatus", () => {
426 ...process.env,460 ...process.env,
427 T3_ACP_REQUEST_LOG_PATH: requestLogPath,461 T3_ACP_REQUEST_LOG_PATH: requestLogPath,
428 },462 },
429 ).pipe(Effect.provide(NodeServices.layer)),463 ),
430 );464 );
431465
432 expect(provider.models.map((model) => model.slug)).toEqual([466 expect(provider.models.map((model) => model.slug)).toEqual([
...@@ -443,13 +477,13 @@ describe("discoverCursorModelsViaAcp", () => {...@@ -443,13 +477,13 @@ describe("discoverCursorModelsViaAcp", () => {
443 it("keeps the ACP probe runtime alive long enough to discover models", async () => {477 it("keeps the ACP probe runtime alive long enough to discover models", async () => {
444 const wrapperPath = await runNode(makeMockAgentWrapper());478 const wrapperPath = await runNode(makeMockAgentWrapper());
445479
446 const models = await Effect.runPromise(480 const models = await runNode(
447 discoverCursorModelsViaAcp({481 discoverCursorModelsViaAcp({
448 enabled: true,482 enabled: true,
449 binaryPath: wrapperPath,483 binaryPath: wrapperPath,
450 apiEndpoint: "",484 apiEndpoint: "",
451 customModels: [],485 customModels: [],
452 }).pipe(Effect.provide(NodeServices.layer), Effect.scoped),486 }).pipe(Effect.scoped),
453 );487 );
454488
455 expect(models.map((model) => model.slug)).toEqual([489 expect(models.map((model) => model.slug)).toEqual([
...@@ -465,13 +499,13 @@ describe("discoverCursorModelsViaAcp", () => {...@@ -465,13 +499,13 @@ describe("discoverCursorModelsViaAcp", () => {
465 makeExitLogFixture("cursor-provider-exit-log-"),499 makeExitLogFixture("cursor-provider-exit-log-"),
466 );500 );
467501
468 await Effect.runPromise(502 await runNode(
469 discoverCursorModelsViaAcp({503 discoverCursorModelsViaAcp({
470 enabled: true,504 enabled: true,
471 binaryPath: wrapperPath,505 binaryPath: wrapperPath,
472 apiEndpoint: "",506 apiEndpoint: "",
473 customModels: [],507 customModels: [],
474 }).pipe(Effect.provide(NodeServices.layer)),508 }),
475 );509 );
476510
477 const exitLog = await runNode(waitForFileContent(exitLogPath));511 const exitLog = await runNode(waitForFileContent(exitLogPath));
apps/server/src/provider/Layers/CursorProvider.ts+17-2
...@@ -62,6 +62,13 @@ const EMPTY_CAPABILITIES: ModelCapabilities = createModelCapabilities({...@@ -62,6 +62,13 @@ const EMPTY_CAPABILITIES: ModelCapabilities = createModelCapabilities({
6262
63const CURSOR_ACP_MODEL_DISCOVERY_TIMEOUT_MS = 15_000;63const CURSOR_ACP_MODEL_DISCOVERY_TIMEOUT_MS = 15_000;
64const CURSOR_PARAMETERIZED_MODEL_PICKER_MIN_VERSION_DATE = 2026_04_08;64const CURSOR_PARAMETERIZED_MODEL_PICKER_MIN_VERSION_DATE = 2026_04_08;
65const CURSOR_CLI_INSTALLATION_DOCS_URL = "https://cursor.com/docs/cli/installation";
66const CURSOR_ACP_MODEL_DISCOVERY_FAILED_MESSAGE = [
67 "Cursor ACP model discovery failed.",
68 "Cursor CLI setup may be incomplete; install or enable the Cursor CLI, restart T3 Code, and try again.",
69 `See ${CURSOR_CLI_INSTALLATION_DOCS_URL}.`,
70 "Check server logs for ACP details.",
71].join(" ");
65export const CURSOR_PARAMETERIZED_MODEL_PICKER_CAPABILITIES = {72export const CURSOR_PARAMETERIZED_MODEL_PICKER_CAPABILITIES = {
66 _meta: {73 _meta: {
67 parameterizedModelPicker: true,74 parameterizedModelPicker: true,
...@@ -608,6 +615,14 @@ function joinProviderMessages(...messages: ReadonlyArray<string | undefined>): s...@@ -608,6 +615,14 @@ function joinProviderMessages(...messages: ReadonlyArray<string | undefined>): s
608 return parts.length > 0 ? parts.join(" ") : undefined;615 return parts.length > 0 ? parts.join(" ") : undefined;
609}616}
610617
618function buildCursorCliCommandMissingMessage(binaryPath: string): string {
619 return [
620 `Cursor CLI command \`${binaryPath}\` was not found.`,
621 `Install or enable the Cursor CLI, make sure \`${binaryPath}\` is on PATH, then restart T3 Code.`,
622 `See ${CURSOR_CLI_INSTALLATION_DOCS_URL}.`,
623 ].join(" ");
624}
625
611export function buildCursorProviderSnapshot(input: {626export function buildCursorProviderSnapshot(input: {
612 readonly checkedAt: string;627 readonly checkedAt: string;
613 readonly cursorSettings: CursorSettings;628 readonly cursorSettings: CursorSettings;
...@@ -1020,7 +1035,7 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")(...@@ -1020,7 +1035,7 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")(
1020 status: "error",1035 status: "error",
1021 auth: { status: "unknown" },1036 auth: { status: "unknown" },
1022 message: isCommandMissingCause(error)1037 message: isCommandMissingCause(error)
1023 ? "Cursor Agent CLI (`agent`) is not installed or not on PATH."1038 ? buildCursorCliCommandMissingMessage(cursorSettings.binaryPath)
1024 : "Failed to execute Cursor Agent CLI health check.",1039 : "Failed to execute Cursor Agent CLI health check.",
1025 },1040 },
1026 });1041 });
...@@ -1079,7 +1094,7 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")(...@@ -1079,7 +1094,7 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")(
1079 yield* Effect.logWarning("Cursor ACP model discovery failed", {1094 yield* Effect.logWarning("Cursor ACP model discovery failed", {
1080 errorTag: causeErrorTag(discoveryExit.cause),1095 errorTag: causeErrorTag(discoveryExit.cause),
1081 });1096 });
1082 discoveryWarning = "Cursor ACP model discovery failed. Check server logs for details.";1097 discoveryWarning = CURSOR_ACP_MODEL_DISCOVERY_FAILED_MESSAGE;
1083 } else if (Option.isNone(discoveryExit.value)) {1098 } else if (Option.isNone(discoveryExit.value)) {
1084 discoveryWarning = `Cursor ACP model discovery timed out after ${CURSOR_ACP_MODEL_DISCOVERY_TIMEOUT_MS}ms.`;1099 discoveryWarning = `Cursor ACP model discovery timed out after ${CURSOR_ACP_MODEL_DISCOVERY_TIMEOUT_MS}ms.`;
1085 } else if (discoveryExit.value.value.length === 0) {1100 } else if (discoveryExit.value.value.length === 0) {