Merge pull request #1794 from paperclipai/fix/cursor-native-auth-check

fix(cursor): check native auth before warning about missing API key
This commit is contained in:
Devin Foley 2026-03-25 22:00:37 -07:00 committed by GitHub
commit 1a4ed8c953
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 133 additions and 7 deletions

View file

@ -12,6 +12,8 @@ import {
ensurePathInEnv,
runChildProcess,
} from "@paperclipai/adapter-utils/server-utils";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { DEFAULT_CURSOR_LOCAL_MODEL } from "../index.js";
import { parseCursorJsonl } from "./parse.js";
@ -49,6 +51,41 @@ function summarizeProbeDetail(stdout: string, stderr: string, parsedError: strin
return clean.length > max ? `${clean.slice(0, max - 1)}` : clean;
}
export interface CursorAuthInfo {
email: string | null;
displayName: string | null;
userId: number | null;
}
export function cursorConfigPath(cursorHome?: string): string {
return path.join(cursorHome ?? path.join(os.homedir(), ".cursor"), "cli-config.json");
}
export async function readCursorAuthInfo(cursorHome?: string): Promise<CursorAuthInfo | null> {
let raw: string;
try {
raw = await fs.readFile(cursorConfigPath(cursorHome), "utf8");
} catch {
return null;
}
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
return null;
}
if (typeof parsed !== "object" || parsed === null) return null;
const obj = parsed as Record<string, unknown>;
const authInfo = obj.authInfo;
if (typeof authInfo !== "object" || authInfo === null) return null;
const info = authInfo as Record<string, unknown>;
const email = typeof info.email === "string" && info.email.trim().length > 0 ? info.email.trim() : null;
const displayName = typeof info.displayName === "string" && info.displayName.trim().length > 0 ? info.displayName.trim() : null;
const userId = typeof info.userId === "number" ? info.userId : null;
if (!email && !displayName && userId == null) return null;
return { email, displayName, userId };
}
const CURSOR_AUTH_REQUIRED_RE =
/(?:authentication\s+required|not\s+authenticated|not\s+logged\s+in|unauthorized|invalid(?:\s+or\s+missing)?\s+api(?:[_\s-]?key)?|cursor[_\s-]?api[_\s-]?key|run\s+'?agent\s+login'?\s+first|api(?:[_\s-]?key)?(?:\s+is)?\s+required)/i;
@ -109,12 +146,25 @@ export async function testEnvironment(
detail: `Detected in ${source}.`,
});
} else {
checks.push({
code: "cursor_api_key_missing",
level: "warn",
message: "CURSOR_API_KEY is not set. Cursor runs may fail until authentication is configured.",
hint: "Set CURSOR_API_KEY in adapter env or run `agent login`.",
});
const cursorHome = isNonEmpty(env.CURSOR_HOME) ? env.CURSOR_HOME : undefined;
const cursorAuth = await readCursorAuthInfo(cursorHome).catch(() => null);
if (cursorAuth) {
checks.push({
code: "cursor_native_auth_present",
level: "info",
message: "Cursor is authenticated via `agent login`.",
detail: cursorAuth.email
? `Logged in as ${cursorAuth.email}.`
: `Credentials found in ${cursorConfigPath(cursorHome)}.`,
});
} else {
checks.push({
code: "cursor_api_key_missing",
level: "warn",
message: "CURSOR_API_KEY is not set. Cursor runs may fail until authentication is configured.",
hint: "Set CURSOR_API_KEY in adapter env or run `agent login`.",
});
}
}
const canRunProbe =

View file

@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
@ -28,6 +28,13 @@ console.log(JSON.stringify({
}
describe("cursor environment diagnostics", () => {
beforeEach(() => {
vi.stubEnv("CURSOR_API_KEY", "");
});
afterEach(() => {
vi.unstubAllEnvs();
});
it("creates a missing working directory when cwd is absolute", async () => {
const cwd = path.join(
os.tmpdir(),
@ -116,4 +123,73 @@ describe("cursor environment diagnostics", () => {
expect(args).not.toContain("--trust");
await fs.rm(root, { recursive: true, force: true });
});
it("emits cursor_native_auth_present when cli-config.json has authInfo and CURSOR_API_KEY is unset", async () => {
const root = path.join(
os.tmpdir(),
`paperclip-cursor-auth-${Date.now()}-${Math.random().toString(16).slice(2)}`,
);
const cursorHome = path.join(root, ".cursor");
const cwd = path.join(root, "workspace");
try {
await fs.mkdir(cursorHome, { recursive: true });
await fs.writeFile(
path.join(cursorHome, "cli-config.json"),
JSON.stringify({
authInfo: {
email: "test@example.com",
displayName: "Test User",
userId: 12345,
},
}),
);
const result = await testEnvironment({
companyId: "company-1",
adapterType: "cursor",
config: {
command: process.execPath,
cwd,
env: { CURSOR_HOME: cursorHome },
},
});
expect(result.checks.some((check) => check.code === "cursor_native_auth_present")).toBe(true);
expect(result.checks.some((check) => check.code === "cursor_api_key_missing")).toBe(false);
const authCheck = result.checks.find((check) => check.code === "cursor_native_auth_present");
expect(authCheck?.detail).toContain("test@example.com");
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
it("emits cursor_api_key_missing when neither env var nor native auth exists", async () => {
const root = path.join(
os.tmpdir(),
`paperclip-cursor-noauth-${Date.now()}-${Math.random().toString(16).slice(2)}`,
);
const cursorHome = path.join(root, ".cursor");
const cwd = path.join(root, "workspace");
try {
await fs.mkdir(cursorHome, { recursive: true });
// No cli-config.json written
const result = await testEnvironment({
companyId: "company-1",
adapterType: "cursor",
config: {
command: process.execPath,
cwd,
env: { CURSOR_HOME: cursorHome },
},
});
expect(result.checks.some((check) => check.code === "cursor_api_key_missing")).toBe(true);
expect(result.checks.some((check) => check.code === "cursor_native_auth_present")).toBe(false);
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
});