("/v1/providers/cursor-cli/auth-status")
+ .then(setCliAuth)
+ .catch(() => setCliAuth({ logged_in: false, error: t("cursorCli.authCheckFailed") }))
+ .finally(() => setLoading(false));
+ }, [http, t]);
+
+ useEffect(() => {
+ if (open) {
+ checkAuth();
+ } else {
+ setCliAuth(null);
+ }
+ }, [open, checkAuth]);
+
+ return (
+
+
+ {t("cursorCli.description")} agent{" "}
+ {t("cursorCli.descriptionSuffix")}
+
+ {loading ? (
+
+
+ {t("cursorCli.checkingAuth")}
+
+ ) : cliAuth?.logged_in ? (
+
+
+
+
+
+ {t("cursorCli.authenticatedAs")} {cliAuth.email}
+ {cliAuth.subscription_type && (
+ ({cliAuth.subscription_type})
+ )}
+
+
+
+
+
+ {t("cursorCli.switchAccount")}
+
+
{t("cursorCli.switchAccountInstructions")}
+
agent logout && agent login
+
+ {t("cursorCli.switchAccountRecheck")} {" "}
+ {t("cursorCli.switchAccountRecheckSuffix")}
+
+
+
+
+ ) : cliAuth ? (
+
+
+
+
+
{t("cursorCli.notAuthenticated")}
+
+
+
+
{t("cursorCli.runOnServer")}
+
+ agent login
+
+ {cliAuth.error &&
{cliAuth.error}
}
+
+ ) : null}
+
+ );
+}
diff --git a/ui/web/src/pages/providers/provider-detail/provider-advanced-dialog.tsx b/ui/web/src/pages/providers/provider-detail/provider-advanced-dialog.tsx
index ade377bc5..7dc209708 100644
--- a/ui/web/src/pages/providers/provider-detail/provider-advanced-dialog.tsx
+++ b/ui/web/src/pages/providers/provider-detail/provider-advanced-dialog.tsx
@@ -20,6 +20,7 @@ import {
import { ConfigGroupHeader } from "@/components/shared/config-group-header";
import { PROVIDER_TYPES } from "@/constants/providers";
import { CLISection } from "../provider-cli-section";
+import { CursorCLISection } from "../provider-cursor-cli-section";
import { OAuthSection } from "../provider-oauth-section";
import type { ProviderData, ProviderInput } from "@/types/provider";
@@ -52,8 +53,9 @@ export function ProviderAdvancedDialog({
const isACP = provider.provider_type === "acp";
const isCLI = provider.provider_type === "claude_cli";
+ const isCursorCLI = provider.provider_type === "cursor_cli";
const isOAuth = provider.provider_type === "chatgpt_oauth";
- const isStandard = !isACP && !isCLI && !isOAuth;
+ const isStandard = !isACP && !isCLI && !isCursorCLI && !isOAuth;
const typeInfo = PROVIDER_TYPES.find((pt) => pt.value === provider.provider_type);
@@ -241,6 +243,17 @@ export function ProviderAdvancedDialog({
>
)}
+ {/* Cursor CLI */}
+ {isCursorCLI && (
+ <>
+
+
+ >
+ )}
+
{/* OAuth */}
{isOAuth && (
<>
diff --git a/ui/web/src/pages/providers/provider-detail/provider-overview-helpers.ts b/ui/web/src/pages/providers/provider-detail/provider-overview-helpers.ts
index 4e2217059..e83759c9c 100644
--- a/ui/web/src/pages/providers/provider-detail/provider-overview-helpers.ts
+++ b/ui/web/src/pages/providers/provider-detail/provider-overview-helpers.ts
@@ -3,7 +3,7 @@ import type { ChatGPTOAuthRoutingConfig } from "@/types/agent";
import { normalizeReasoningEffort, normalizeReasoningFallback } from "@/types/provider";
// Provider types that don't use API keys
-export const NO_API_KEY_TYPES = new Set(["claude_cli", "acp", "chatgpt_oauth"]);
+export const NO_API_KEY_TYPES = new Set(["claude_cli", "cursor_cli", "acp", "chatgpt_oauth"]);
// Provider types that don't support embedding
export const NO_EMBEDDING_TYPES = new Set([
diff --git a/ui/web/src/pages/providers/provider-form-dialog.tsx b/ui/web/src/pages/providers/provider-form-dialog.tsx
index 86b275807..3112e4a5c 100644
--- a/ui/web/src/pages/providers/provider-form-dialog.tsx
+++ b/ui/web/src/pages/providers/provider-form-dialog.tsx
@@ -27,6 +27,7 @@ import { slugify } from "@/lib/slug";
import { DEFAULT_CODEX_OAUTH_ALIAS, PROVIDER_TYPES, suggestUniqueProviderAlias } from "@/constants/providers";
import { OAuthSection } from "./provider-oauth-section";
import { CLISection } from "./provider-cli-section";
+import { CursorCLISection } from "./provider-cursor-cli-section";
import { ACPSection } from "./provider-acp-section";
import { Loader2 } from "lucide-react";
import { providerCreateSchema, type ProviderCreateFormData } from "@/schemas/provider.schema";
@@ -66,8 +67,11 @@ export function ProviderFormDialog({ open, onOpenChange, onSubmit, existingProvi
const name = watch("name");
const hasClaudeCLI = existingProviders.some((p) => p.provider_type === "claude_cli");
+ const hasCursorCLI = existingProviders.some((p) => p.provider_type === "cursor_cli");
+
const isOAuth = providerType === "chatgpt_oauth";
const isCLI = providerType === "claude_cli";
+ const isCursorCLI = providerType === "cursor_cli";
const isACP = providerType === "acp";
// Reset form when dialog opens
@@ -140,6 +144,7 @@ export function ProviderFormDialog({ open, onOpenChange, onSubmit, existingProvi
}
+ {isCursorCLI && }
+
{isACP && (
)}
- {!isCLI && !isACP && (
+ {!isCLI && !isCursorCLI && !isACP && (
<>
@@ -287,9 +294,10 @@ export function ProviderFormDialog({ open, onOpenChange, onSubmit, existingProvi
);
}
-function ProviderTypeSelect({ value, hasClaudeCLI, alreadyAddedLabel, providerTypeLabel, onChange }: {
+function ProviderTypeSelect({ value, hasClaudeCLI, hasCursorCLI, alreadyAddedLabel, providerTypeLabel, onChange }: {
value: string;
hasClaudeCLI: boolean;
+ hasCursorCLI: boolean;
alreadyAddedLabel: string;
providerTypeLabel: string;
onChange: (value: string) => void;
@@ -306,12 +314,15 @@ function ProviderTypeSelect({ value, hasClaudeCLI, alreadyAddedLabel, providerTy
{pt.label}
{pt.value === "claude_cli" && hasClaudeCLI && (
{alreadyAddedLabel}
)}
+ {pt.value === "cursor_cli" && hasCursorCLI && (
+ {alreadyAddedLabel}
+ )}
))}
diff --git a/ui/web/src/pages/providers/provider-utils.tsx b/ui/web/src/pages/providers/provider-utils.tsx
index a21001cc4..70d884ff2 100644
--- a/ui/web/src/pages/providers/provider-utils.tsx
+++ b/ui/web/src/pages/providers/provider-utils.tsx
@@ -12,6 +12,7 @@ const SPECIAL_VARIANTS: Record = {
anthropic_native: "default",
chatgpt_oauth: "default",
claude_cli: "outline",
+ cursor_cli: "outline",
acp: "outline",
};
@@ -123,7 +124,7 @@ export function ProviderApiKeyBadge({
);
}
- if (provider.provider_type === "claude_cli") {
+ if (provider.provider_type === "claude_cli" || provider.provider_type === "cursor_cli") {
return (
{t("card.authenticated")}
diff --git a/ui/web/src/pages/setup/step-provider.tsx b/ui/web/src/pages/setup/step-provider.tsx
index 0a0a0b012..281c8cd0e 100644
--- a/ui/web/src/pages/setup/step-provider.tsx
+++ b/ui/web/src/pages/setup/step-provider.tsx
@@ -17,6 +17,7 @@ import {
import { PROVIDER_TYPES, suggestUniqueProviderAlias } from "@/constants/providers";
import { useProviders } from "@/pages/providers/hooks/use-providers";
import { CLISection } from "@/pages/providers/provider-cli-section";
+import { CursorCLISection } from "@/pages/providers/provider-cursor-cli-section";
import { OAuthSection } from "@/pages/providers/provider-oauth-section";
import { slugify } from "@/lib/slug";
import type { ProviderData, ProviderInput } from "@/types/provider";
@@ -45,8 +46,10 @@ export function StepProvider({ onComplete, existingProvider }: StepProviderProps
const isOAuth = providerType === "chatgpt_oauth";
const isCLI = providerType === "claude_cli";
+ const isCursorCLI = providerType === "cursor_cli";
// Local Ollama uses no API key — the server accepts any non-empty Bearer value internally
const isOllama = providerType === "ollama";
+ const noApiKeyProvider = isCLI || isCursorCLI || isOllama;
const handleTypeChange = (value: string) => {
setProviderType(value);
@@ -97,7 +100,7 @@ export function StepProvider({ onComplete, existingProvider }: StepProviderProps
const handleSubmit = async () => {
if (isOAuth) return;
- if (!isEditing && !isCLI && !isOllama && !apiKey.trim()) { setError(t("provider.errors.apiKeyRequired")); return; }
+ if (!isEditing && !noApiKeyProvider && !apiKey.trim()) { setError(t("provider.errors.apiKeyRequired")); return; }
setLoading(true);
setError("");
try {
@@ -116,7 +119,7 @@ export function StepProvider({ onComplete, existingProvider }: StepProviderProps
name: name.trim(),
provider_type: providerType,
api_base: apiBase.trim() || undefined,
- api_key: isCLI || isOllama || isOAuth ? undefined : apiKey.trim(),
+ api_key: noApiKeyProvider || isOAuth ? undefined : apiKey.trim(),
enabled: true,
}) as ProviderData;
onComplete(provider);
@@ -139,6 +142,8 @@ export function StepProvider({ onComplete, existingProvider }: StepProviderProps
? t("provider.descriptionOauth")
: isCLI
? t("provider.descriptionCli")
+ : isCursorCLI
+ ? t("provider.descriptionCursorCli")
: t("provider.description")}
@@ -195,6 +200,8 @@ export function StepProvider({ onComplete, existingProvider }: StepProviderProps
>
) : isCLI ? (
+ ) : isCursorCLI ? (
+
) : (
<>
@@ -228,7 +235,7 @@ export function StepProvider({ onComplete, existingProvider }: StepProviderProps
{!isOAuth && (
-