diff --git a/packages/adapters/cursor-local/src/server/test.ts b/packages/adapters/cursor-local/src/server/test.ts index 15263812..c8e53b98 100644 --- a/packages/adapters/cursor-local/src/server/test.ts +++ b/packages/adapters/cursor-local/src/server/test.ts @@ -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 { + 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; + const authInfo = obj.authInfo; + if (typeof authInfo !== "object" || authInfo === null) return null; + const info = authInfo as Record; + 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 = diff --git a/server/src/__tests__/cursor-local-adapter-environment.test.ts b/server/src/__tests__/cursor-local-adapter-environment.test.ts index e6892259..c873d34e 100644 --- a/server/src/__tests__/cursor-local-adapter-environment.test.ts +++ b/server/src/__tests__/cursor-local-adapter-environment.test.ts @@ -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 }); + } + }); });