From bccb552211a95ef6b6667d558a5511e4e1ce7c00 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 8 Jul 2026 18:07:48 -0700 Subject: [PATCH] FN-7697: fix Cursor CLI auth-status and model-list discovery Corrects the Cursor plugin's CLI integration to match cursor-agent's real command contract instead of best-effort heuristics. - Derive authentication from `cursor-agent status --format json` (`isAuthenticated` field), failing closed with an actionable reason on non-zero exit or malformed JSON, instead of treating `--version` success as auth-ready. - Switch model discovery to `cursor-agent models` plain-text output (`id - Label` lines), filtering header/tip/empty-state lines, since `--json`/`model list` are not supported. - Update README to document the corrected CLI usage. - Add regression tests covering probe.ts auth-status parsing and process-manager.ts model discovery. - Add changeset (patch) documenting the fix. Files changed: .changeset/fn-7697-cursor-cli.md | 7 ++ plugins/fusion-plugin-cursor-runtime/README.md | 3 +- .../src/__tests__/probe.test.ts | 94 +++++++++++++++++++--- .../src/__tests__/process-manager.test.ts | 89 +++++++++++++------- plugins/fusion-plugin-cursor-runtime/src/probe.ts | 39 +++++++-- .../src/process-manager.ts | 84 ++++++++++++------- 6 files changed, 239 insertions(+), 77 deletions(-) Fusion-Task-Id: FN-7697 Fusion-Task-Lineage: 6dd56a7f-da8f-4a73-82ff-5e9baab697c5 Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-7697-cursor-cli.md | 7 ++ .../fusion-plugin-cursor-runtime/README.md | 3 +- .../src/__tests__/probe.test.ts | 94 +++++++++++++++--- .../src/__tests__/process-manager.test.ts | 99 ++++++++++++------- .../fusion-plugin-cursor-runtime/src/probe.ts | 39 +++++++- .../src/process-manager.ts | 78 ++++++++++----- 6 files changed, 241 insertions(+), 79 deletions(-) create mode 100644 .changeset/fn-7697-cursor-cli.md diff --git a/.changeset/fn-7697-cursor-cli.md b/.changeset/fn-7697-cursor-cli.md new file mode 100644 index 0000000000..169f5c53d1 --- /dev/null +++ b/.changeset/fn-7697-cursor-cli.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix Cursor CLI model discovery and auth to use the real cursor-agent commands. +category: fix +dev: Switches model discovery to `cursor-agent models` (plain text `id - Label`, no `--json`/`model list` support) with header/tip/empty-state filtering, and derives auth from `cursor-agent status --format json` (`isAuthenticated`) instead of a `--version`-success heuristic. diff --git a/plugins/fusion-plugin-cursor-runtime/README.md b/plugins/fusion-plugin-cursor-runtime/README.md index b531d8a477..5d05e7c08f 100644 --- a/plugins/fusion-plugin-cursor-runtime/README.md +++ b/plugins/fusion-plugin-cursor-runtime/README.md @@ -7,7 +7,8 @@ Cursor CLI-backed provider/runtime plugin for Fusion. - Provider ID: `cursor-cli` - Binary probes: `cursor-agent`, then `cursor` - Expected failure states: missing binary, missing Cursor IDE install, locked macOS keychain, unauthenticated runtime -- Model discovery: dynamic command probing (`models --json`, fallbacks) with dedupe + fallback metadata +- Model discovery: `cursor-agent models` (plain text `id - Label` output; no `--json` support) with header/tip/empty-state filtering, dedupe, and fallback metadata +- Auth status: `cursor-agent status --format json` (`isAuthenticated`), probed against the same candidate binary that succeeded `--version` ## Notes diff --git a/plugins/fusion-plugin-cursor-runtime/src/__tests__/probe.test.ts b/plugins/fusion-plugin-cursor-runtime/src/__tests__/probe.test.ts index d0d2aa6d2a..68adbf9f42 100644 --- a/plugins/fusion-plugin-cursor-runtime/src/__tests__/probe.test.ts +++ b/plugins/fusion-plugin-cursor-runtime/src/__tests__/probe.test.ts @@ -5,22 +5,82 @@ vi.mock("../cli-spawn.js", () => ({ runCursorCommand: vi.fn() })); import { runCursorCommand } from "../cli-spawn.js"; import { probeCursorBinary } from "../probe.js"; +const AUTHENTICATED_STATUS = JSON.stringify({ isAuthenticated: true, status: "logged_in", hasAccessToken: true, userInfo: { email: "dev@example.com" } }); +const UNAUTHENTICATED_STATUS = JSON.stringify({ isAuthenticated: false, status: "logged_out", hasAccessToken: false }); + describe("probeCursorBinary", () => { beforeEach(() => { vi.clearAllMocks(); }); - it("reports available when probe succeeds", async () => { - vi.mocked(runCursorCommand).mockResolvedValue({ code: 0, stdout: "1.2.3", stderr: "" }); + it("reports authenticated:true from status --format json isAuthenticated", async () => { + vi.mocked(runCursorCommand) + .mockResolvedValueOnce({ code: 0, stdout: "1.2.3", stderr: "" }) + .mockResolvedValueOnce({ code: 0, stdout: AUTHENTICATED_STATUS, stderr: "" }); + const result = await probeCursorBinary({ binaryPath: "/usr/local/bin/cursor-agent" }); - expect(runCursorCommand).toHaveBeenCalledWith("/usr/local/bin/cursor-agent", ["--version"], 3000); + + expect(runCursorCommand).toHaveBeenNthCalledWith(1, "/usr/local/bin/cursor-agent", ["--version"], 3000); + expect(runCursorCommand).toHaveBeenNthCalledWith(2, "/usr/local/bin/cursor-agent", ["status", "--format", "json"], 3000); expect(result.available).toBe(true); + expect(result.authenticated).toBe(true); expect(result.version).toBe("1.2.3"); expect(result.binaryPath).toBe("/usr/local/bin/cursor-agent"); expect(result.configuredBinaryPath).toBe("/usr/local/bin/cursor-agent"); expect(result.usingConfiguredBinaryPath).toBe(true); }); + it("reports authenticated:false from status --format json isAuthenticated:false", async () => { + vi.mocked(runCursorCommand) + .mockResolvedValueOnce({ code: 0, stdout: "1.2.3", stderr: "" }) + .mockResolvedValueOnce({ code: 0, stdout: UNAUTHENTICATED_STATUS, stderr: "" }); + + const result = await probeCursorBinary({ binaryPath: "cursor-agent" }); + + expect(result.available).toBe(true); + expect(result.authenticated).toBe(false); + expect(result.reason).toBe("cursor-agent reports not authenticated"); + }); + + it("fails closed to authenticated:false with an actionable reason on malformed/non-JSON status output", async () => { + vi.mocked(runCursorCommand) + .mockResolvedValueOnce({ code: 0, stdout: "1.2.3", stderr: "" }) + .mockResolvedValueOnce({ code: 0, stdout: "not json at all", stderr: "" }); + + const result = await probeCursorBinary({ binaryPath: "cursor-agent" }); + + expect(result.available).toBe(true); + expect(result.authenticated).toBe(false); + expect(result.reason).toBe("cursor-agent status --format json returned malformed JSON"); + }); + + it("fails closed to authenticated:false with an actionable reason when status exits non-zero", async () => { + vi.mocked(runCursorCommand) + .mockResolvedValueOnce({ code: 0, stdout: "1.2.3", stderr: "" }) + .mockResolvedValueOnce({ code: 1, stdout: "", stderr: "unexpected error" }); + + const result = await probeCursorBinary({ binaryPath: "cursor-agent" }); + + expect(result.available).toBe(true); + expect(result.authenticated).toBe(false); + expect(result.reason).toBe("cursor-agent status --format json did not return output"); + }); + + it("probes status against the SAME candidate binary that succeeded --version, never re-probing a different candidate", async () => { + vi.mocked(runCursorCommand) + .mockResolvedValueOnce({ code: 127, stdout: "", stderr: "spawn error: ENOENT: cursor-agent" }) + .mockResolvedValueOnce({ code: 0, stdout: "cursor 0.50.0\n", stderr: "" }) + .mockResolvedValueOnce({ code: 0, stdout: AUTHENTICATED_STATUS, stderr: "" }); + + const result = await probeCursorBinary(); + + expect(runCursorCommand).toHaveBeenNthCalledWith(1, "cursor-agent", ["--version"], 3000); + expect(runCursorCommand).toHaveBeenNthCalledWith(2, "cursor", ["--version"], 3000); + expect(runCursorCommand).toHaveBeenNthCalledWith(3, "cursor", ["status", "--format", "json"], 3000); + expect(result.binaryName).toBe("cursor"); + expect(result.authenticated).toBe(true); + }); + it("reports keychain lock as auth failure", async () => { vi.mocked(runCursorCommand).mockResolvedValue({ code: 1, stdout: "", stderr: "Error: Your macOS login keychain is locked." }); const result = await probeCursorBinary({ binaryPath: "cursor-agent" }); @@ -38,12 +98,14 @@ describe("probeCursorBinary", () => { }); it("probes cursor-agent before cursor and reports the first Windows shim success", async () => { - vi.mocked(runCursorCommand).mockResolvedValueOnce({ code: 0, stdout: "cursor-agent 0.50.0\n", stderr: "" }); + vi.mocked(runCursorCommand) + .mockResolvedValueOnce({ code: 0, stdout: "cursor-agent 0.50.0\n", stderr: "" }) + .mockResolvedValueOnce({ code: 0, stdout: AUTHENTICATED_STATUS, stderr: "" }); const result = await probeCursorBinary(); - expect(runCursorCommand).toHaveBeenCalledWith("cursor-agent", ["--version"], 3000); - expect(runCursorCommand).toHaveBeenCalledTimes(1); + expect(runCursorCommand).toHaveBeenNthCalledWith(1, "cursor-agent", ["--version"], 3000); + expect(runCursorCommand).toHaveBeenCalledTimes(2); expect(result).toMatchObject({ available: true, authenticated: true, @@ -56,7 +118,8 @@ describe("probeCursorBinary", () => { it("falls back to cursor when cursor-agent fails but cursor succeeds", async () => { vi.mocked(runCursorCommand) .mockResolvedValueOnce({ code: 127, stdout: "", stderr: "spawn error: ENOENT: cursor-agent" }) - .mockResolvedValueOnce({ code: 0, stdout: "cursor 0.50.0\n", stderr: "" }); + .mockResolvedValueOnce({ code: 0, stdout: "cursor 0.50.0\n", stderr: "" }) + .mockResolvedValueOnce({ code: 0, stdout: AUTHENTICATED_STATUS, stderr: "" }); const result = await probeCursorBinary(); @@ -80,13 +143,15 @@ describe("probeCursorBinary", () => { }); it("tries a Windows path with spaces and .cmd shim before PATH fallback", async () => { - vi.mocked(runCursorCommand).mockResolvedValueOnce({ code: 0, stdout: "cursor-agent.cmd 0.50.0", stderr: "" }); + vi.mocked(runCursorCommand) + .mockResolvedValueOnce({ code: 0, stdout: "cursor-agent.cmd 0.50.0", stderr: "" }) + .mockResolvedValueOnce({ code: 0, stdout: AUTHENTICATED_STATUS, stderr: "" }); const binaryPath = "C:\\Users\\A User\\AppData\\Roaming\\npm\\cursor-agent.cmd"; const result = await probeCursorBinary({ binaryPath }); - expect(runCursorCommand).toHaveBeenCalledWith(binaryPath, ["--version"], 3000); - expect(runCursorCommand).toHaveBeenCalledTimes(1); + expect(runCursorCommand).toHaveBeenNthCalledWith(1, binaryPath, ["--version"], 3000); + expect(runCursorCommand).toHaveBeenNthCalledWith(2, binaryPath, ["status", "--format", "json"], 3000); expect(result.binaryPath).toBe(binaryPath); expect(result.usingConfiguredBinaryPath).toBe(true); }); @@ -94,12 +159,14 @@ describe("probeCursorBinary", () => { it("falls back to PATH candidates when a configured binary fails", async () => { vi.mocked(runCursorCommand) .mockResolvedValueOnce({ code: 127, stdout: "", stderr: "spawn error: ENOENT: /missing/cursor-agent" }) - .mockResolvedValueOnce({ code: 0, stdout: "cursor-agent 0.50.0\n", stderr: "" }); + .mockResolvedValueOnce({ code: 0, stdout: "cursor-agent 0.50.0\n", stderr: "" }) + .mockResolvedValueOnce({ code: 0, stdout: AUTHENTICATED_STATUS, stderr: "" }); const result = await probeCursorBinary({ binaryPath: "/missing/cursor-agent" }); expect(runCursorCommand).toHaveBeenNthCalledWith(1, "/missing/cursor-agent", ["--version"], 3000); expect(runCursorCommand).toHaveBeenNthCalledWith(2, "cursor-agent", ["--version"], 3000); + expect(runCursorCommand).toHaveBeenNthCalledWith(3, "cursor-agent", ["status", "--format", "json"], 3000); expect(result.available).toBe(true); expect(result.binaryPath).toBe("cursor-agent"); expect(result.usingConfiguredBinaryPath).toBe(false); @@ -124,11 +191,12 @@ describe("probeCursorBinary", () => { it("dedupes overrides equal to default PATH candidate names", async () => { vi.mocked(runCursorCommand) .mockResolvedValueOnce({ code: 127, stdout: "", stderr: "spawn error: ENOENT: cursor-agent" }) - .mockResolvedValueOnce({ code: 0, stdout: "cursor 0.50.0\n", stderr: "" }); + .mockResolvedValueOnce({ code: 0, stdout: "cursor 0.50.0\n", stderr: "" }) + .mockResolvedValueOnce({ code: 0, stdout: AUTHENTICATED_STATUS, stderr: "" }); const result = await probeCursorBinary({ binaryPath: " cursor-agent " }); - expect(runCursorCommand).toHaveBeenCalledTimes(2); + expect(runCursorCommand).toHaveBeenCalledTimes(3); expect(runCursorCommand).toHaveBeenNthCalledWith(1, "cursor-agent", ["--version"], 3000); expect(runCursorCommand).toHaveBeenNthCalledWith(2, "cursor", ["--version"], 3000); expect(result.binaryPath).toBe("cursor"); diff --git a/plugins/fusion-plugin-cursor-runtime/src/__tests__/process-manager.test.ts b/plugins/fusion-plugin-cursor-runtime/src/__tests__/process-manager.test.ts index e577dfeed2..1e77e839b7 100644 --- a/plugins/fusion-plugin-cursor-runtime/src/__tests__/process-manager.test.ts +++ b/plugins/fusion-plugin-cursor-runtime/src/__tests__/process-manager.test.ts @@ -5,53 +5,86 @@ vi.mock("../cli-spawn.js", () => ({ runCursorCommand: vi.fn() })); import { runCursorCommand } from "../cli-spawn.js"; import { discoverCursorModels } from "../process-manager.js"; +const REAL_MODELS_OUTPUT = [ + "Available models", + "", + "auto - Auto (default)", + "claude-4.5-sonnet - Sonnet 4.5", + "gpt-5 - GPT-5", + "", + "Tip: use --model (or /model in interactive mode) to switch.", +].join("\n"); + describe("discoverCursorModels", () => { beforeEach(() => { vi.clearAllMocks(); }); - it("uses json list when available", async () => { - vi.mocked(runCursorCommand).mockResolvedValueOnce({ code: 0, stdout: '[{"id":"cursor/a"},{"id":"cursor/b"}]', stderr: "" }); + it("invokes only `models` (never --json or model list) and never falls back", async () => { + vi.mocked(runCursorCommand).mockResolvedValueOnce({ code: 0, stdout: REAL_MODELS_OUTPUT, stderr: "" }); + await discoverCursorModels("cursor-agent"); + + expect(runCursorCommand).toHaveBeenCalledTimes(1); + expect(runCursorCommand).toHaveBeenCalledWith("cursor-agent", ["models"], 5000); + expect(runCursorCommand).not.toHaveBeenCalledWith("cursor-agent", ["models", "--json"], expect.anything()); + expect(runCursorCommand).not.toHaveBeenCalledWith("cursor-agent", ["model", "list", "--json"], expect.anything()); + }); + + it("extracts bare ids from real `id - Label` output, dropping header and tip lines", async () => { + vi.mocked(runCursorCommand).mockResolvedValueOnce({ code: 0, stdout: REAL_MODELS_OUTPUT, stderr: "" }); const result = await discoverCursorModels("cursor-agent"); - expect(runCursorCommand).toHaveBeenCalledWith("cursor-agent", ["models", "--json"], 5000); - expect(result.models).toEqual(["cursor/a", "cursor/b"]); + + expect(result.models).toEqual(["auto", "claude-4.5-sonnet", "gpt-5"]); + expect(result.source).toBe("models-text"); expect(result.fallbackUsed).toBe(false); }); + it("parses the `auto - Auto (default)` first entry to id `auto`", async () => { + vi.mocked(runCursorCommand).mockResolvedValueOnce({ code: 0, stdout: "Available models\n\nauto - Auto (default)\n\nTip: use --model to switch.", stderr: "" }); + const result = await discoverCursorModels("cursor-agent"); + + expect(result.models[0]).toBe("auto"); + }); + + it("dedupes repeated ids", async () => { + vi.mocked(runCursorCommand).mockResolvedValueOnce({ code: 0, stdout: "auto - Auto (default)\nauto - Auto (default)\ngpt-5 - GPT-5", stderr: "" }); + const result = await discoverCursorModels("cursor-agent"); + + expect(result.models).toEqual(["auto", "gpt-5"]); + }); + + it("returns an empty list with a clear reason for the empty-account state", async () => { + vi.mocked(runCursorCommand).mockResolvedValueOnce({ code: 0, stdout: "No models available for this account.", stderr: "" }); + const result = await discoverCursorModels("cursor-agent"); + + expect(result).toEqual({ models: [], source: "models-text", fallbackUsed: false, reason: "no models available for this account" }); + }); + + it("tolerates JSON output defensively even though the real CLI never sends it", async () => { + vi.mocked(runCursorCommand).mockResolvedValueOnce({ code: 0, stdout: '[{"id":"cursor/a"},{"id":"cursor/b"}]', stderr: "" }); + const result = await discoverCursorModels("cursor-agent"); + + expect(runCursorCommand).toHaveBeenCalledWith("cursor-agent", ["models"], 5000); + expect(result.models).toEqual(["cursor/a", "cursor/b"]); + expect(result.source).toBe("models-json"); + }); + + it("returns empty discovery when the command fails outright", async () => { + vi.mocked(runCursorCommand).mockResolvedValueOnce({ code: 127, stdout: "", stderr: "spawn error: ENOENT" }); + + const result = await discoverCursorModels("cursor-agent", 2500); + + expect(runCursorCommand).toHaveBeenCalledWith("cursor-agent", ["models"], 2500); + expect(result).toEqual({ models: [], source: "none", fallbackUsed: true, reason: "model discovery command unavailable" }); + }); + it("passes Windows .bat paths with spaces as one binary string", async () => { - vi.mocked(runCursorCommand).mockResolvedValueOnce({ code: 0, stdout: '["cursor/a"]', stderr: "" }); + vi.mocked(runCursorCommand).mockResolvedValueOnce({ code: 0, stdout: "cursor/a - Cursor A", stderr: "" }); const binary = "C:\\Program Files\\Cursor\\cursor-agent.bat"; const result = await discoverCursorModels(binary); - expect(runCursorCommand).toHaveBeenCalledWith(binary, ["models", "--json"], 5000); + expect(runCursorCommand).toHaveBeenCalledWith(binary, ["models"], 5000); expect(result.models).toEqual(["cursor/a"]); }); - - it("falls back to text parsing", async () => { - vi.mocked(runCursorCommand) - .mockResolvedValueOnce({ code: 1, stdout: "", stderr: "" }) - .mockResolvedValueOnce({ code: 1, stdout: "", stderr: "" }) - .mockResolvedValueOnce({ code: 0, stdout: "cursor/x\ncursor/y", stderr: "" }); - const result = await discoverCursorModels("cursor-agent"); - expect(runCursorCommand).toHaveBeenNthCalledWith(1, "cursor-agent", ["models", "--json"], 5000); - expect(runCursorCommand).toHaveBeenNthCalledWith(2, "cursor-agent", ["model", "list", "--json"], 5000); - expect(runCursorCommand).toHaveBeenNthCalledWith(3, "cursor-agent", ["models"], 5000); - expect(result.models).toEqual(["cursor/x", "cursor/y"]); - expect(result.fallbackUsed).toBe(true); - }); - - it("returns empty discovery when every command fails", async () => { - vi.mocked(runCursorCommand) - .mockResolvedValueOnce({ code: 127, stdout: "", stderr: "spawn error: ENOENT" }) - .mockResolvedValueOnce({ code: 1, stdout: "", stderr: "unknown command" }) - .mockResolvedValueOnce({ code: 1, stdout: "", stderr: "unknown command" }); - - const result = await discoverCursorModels("cursor-agent", 2500); - - expect(runCursorCommand).toHaveBeenNthCalledWith(1, "cursor-agent", ["models", "--json"], 2500); - expect(runCursorCommand).toHaveBeenNthCalledWith(2, "cursor-agent", ["model", "list", "--json"], 2500); - expect(runCursorCommand).toHaveBeenNthCalledWith(3, "cursor-agent", ["models"], 2500); - expect(result).toEqual({ models: [], source: "none", fallbackUsed: true, reason: "model discovery command unavailable" }); - }); }); diff --git a/plugins/fusion-plugin-cursor-runtime/src/probe.ts b/plugins/fusion-plugin-cursor-runtime/src/probe.ts index c0ead7aa44..0e557075b1 100644 --- a/plugins/fusion-plugin-cursor-runtime/src/probe.ts +++ b/plugins/fusion-plugin-cursor-runtime/src/probe.ts @@ -21,6 +21,28 @@ function summarizeFailure(binary: string, stdout: string, stderr: string): strin return `${binary}: ${truncated}`; } +async function probeCursorAuthStatus(binary: string, timeoutMs: number): Promise<{ authenticated: boolean; reason?: string }> { + const status = await runCursorCommand(binary, ["status", "--format", "json"], timeoutMs); + const output = (status.stdout || "").trim(); + + if (status.code !== 0 || !output) { + return { authenticated: false, reason: "cursor-agent status --format json did not return output" }; + } + + try { + const parsed = JSON.parse(output) as { isAuthenticated?: unknown }; + if (typeof parsed?.isAuthenticated === "boolean") { + return { + authenticated: parsed.isAuthenticated, + reason: parsed.isAuthenticated ? undefined : "cursor-agent reports not authenticated", + }; + } + return { authenticated: false, reason: "cursor-agent status --format json missing isAuthenticated field" }; + } catch { + return { authenticated: false, reason: "cursor-agent status --format json returned malformed JSON" }; + } +} + export async function probeCursorBinary(options?: { timeoutMs?: number; binaryPath?: string }): Promise { const startedAt = Date.now(); const timeoutMs = options?.timeoutMs ?? 3000; @@ -40,15 +62,22 @@ export async function probeCursorBinary(options?: { timeoutMs?: number; binaryPa probeDurationMs: Date.now() - startedAt, }; if (version.code === 0) { - // NOTE: Cursor CLI currently lacks a stable auth-status contract we can - // invoke without side effects. Treating successful --version as ready is - // a best-effort heuristic; keychain/auth errors are handled by fallback - // probes below when surfaced in stderr/stdout. + /* + FNXC:CursorCli 2026-07-08-00:00: + `cursor-agent status --format json` (alias `whoami`) is the real auth + contract, returning `{ isAuthenticated, status, hasAccessToken, userInfo }`. + We probe it with the SAME candidate binary that just succeeded --version + (never re-probing a different candidate), and fail closed to + `authenticated: false` with an actionable reason on any non-zero exit or + malformed/non-JSON output rather than throwing. + */ + const auth = await probeCursorAuthStatus(binary, timeoutMs); return { available: true, - authenticated: true, + authenticated: auth.authenticated, ...common, version: version.stdout.trim() || undefined, + reason: auth.authenticated ? undefined : auth.reason, }; } diff --git a/plugins/fusion-plugin-cursor-runtime/src/process-manager.ts b/plugins/fusion-plugin-cursor-runtime/src/process-manager.ts index aa22a5c818..bcfa603170 100644 --- a/plugins/fusion-plugin-cursor-runtime/src/process-manager.ts +++ b/plugins/fusion-plugin-cursor-runtime/src/process-manager.ts @@ -1,45 +1,69 @@ import { runCursorCommand } from "./cli-spawn.js"; +const EMPTY_ACCOUNT_MESSAGE = "no models available for this account."; + +/* +FNXC:CursorCli 2026-07-08-00:00: +`cursor-agent models` is plain text, NOT JSON — the real CLI rejects `--json` +(`error: unknown option '--json'`) and has no `model list` subcommand. Output +shape is: an `Available models` header line, a blank line, then one model per +line as ` -