| import { spawn } from "node:child_process";
|
| import { existsSync } from "node:fs";
|
| import { homedir } from "node:os";
|
| import { delimiter, join } from "node:path";
|
|
|
|
|
|
|
| function runCursorAgent(
|
| binary: string,
|
| args: string[],
|
| timeoutMs: number
|
| ): Promise<{ stdout: string; stderr: string; code: number | null; signal: NodeJS.Signals | null }> {
|
| return new Promise((resolve, reject) => {
|
| let child;
|
| try {
|
| child = spawn(binary, args, { stdio: ["ignore", "pipe", "pipe"] });
|
| } catch (err) {
|
| reject(err);
|
| return;
|
| }
|
| let stdout = "";
|
| let stderr = "";
|
| child.stdout.setEncoding("utf8");
|
| child.stderr.setEncoding("utf8");
|
| child.stdout.on("data", (chunk) => {
|
| stdout += chunk;
|
| });
|
| child.stderr.on("data", (chunk) => {
|
| stderr += chunk;
|
| });
|
| const killTimer = setTimeout(() => child.kill("SIGTERM"), timeoutMs);
|
| child.on("error", (err) => {
|
| clearTimeout(killTimer);
|
| reject(err);
|
| });
|
| child.on("close", (code, signal) => {
|
| clearTimeout(killTimer);
|
| resolve({ stdout, stderr, code, signal });
|
| });
|
| });
|
| }
|
|
|
|
|
|
|
| function resolveCursorAgentBinary(): string | null {
|
| const home = homedir();
|
| const candidates = [
|
| join(home, ".local", "bin", "cursor-agent"),
|
| "/root/.local/bin/cursor-agent",
|
| "/usr/local/bin/cursor-agent",
|
| "/usr/bin/cursor-agent",
|
| ];
|
| for (const candidate of candidates) {
|
| if (existsSync(candidate)) return candidate;
|
| }
|
|
|
| const pathDirs = (process.env.PATH || "").split(delimiter).filter(Boolean);
|
| for (const dir of pathDirs) {
|
| const candidate = join(dir, "cursor-agent");
|
| if (existsSync(candidate)) return candidate;
|
| }
|
| return null;
|
| }
|
|
|
| const SEGMENT_OVERRIDES: Record<string, string> = {
|
| gpt: "GPT",
|
| claude: "Claude",
|
| gemini: "Gemini",
|
| grok: "Grok",
|
| kimi: "Kimi",
|
| composer: "Composer",
|
| opus: "Opus",
|
| sonnet: "Sonnet",
|
| haiku: "Haiku",
|
| codex: "Codex",
|
| mini: "Mini",
|
| nano: "Nano",
|
| max: "Max",
|
| high: "High",
|
| low: "Low",
|
| medium: "Medium",
|
| xhigh: "XHigh",
|
| none: "None",
|
| fast: "Fast",
|
| thinking: "Thinking",
|
| extra: "Extra",
|
| spark: "Spark",
|
| preview: "Preview",
|
| flash: "Flash",
|
| pro: "Pro",
|
| };
|
|
|
| export function humanizeCursorModelId(id: string): string {
|
| if (id === "auto") return "Auto (Server Picks)";
|
|
|
|
|
|
|
| const collapsed = id.replace(/(\d+)-(\d+)(?=-|$)/g, "$1.$2");
|
| return collapsed
|
| .split("-")
|
| .map((part) => {
|
| if (SEGMENT_OVERRIDES[part]) return SEGMENT_OVERRIDES[part];
|
| if (/^\d/.test(part)) return part;
|
| return part.charAt(0).toUpperCase() + part.slice(1);
|
| })
|
| .join(" ");
|
| }
|
|
|
| export function parseCursorAgentModels(text: string): string[] {
|
| const match = text.match(/Available models:\s*([^\n]+)/);
|
| if (!match) return [];
|
| const seen = new Set<string>();
|
| const out: string[] = [];
|
| for (const raw of match[1].split(",")) {
|
| const id = raw.trim();
|
| if (!id || seen.has(id)) continue;
|
| seen.add(id);
|
| out.push(id);
|
| }
|
| return out;
|
| }
|
|
|
| export type CursorAgentModelEntry = {
|
| id: string;
|
| name: string;
|
| owned_by: "cursor";
|
| };
|
|
|
| export async function fetchCursorAgentModels(
|
| options: { binary?: string; timeoutMs?: number } = {}
|
| ): Promise<CursorAgentModelEntry[]> {
|
| const binary = options.binary || resolveCursorAgentBinary();
|
| const timeoutMs = options.timeoutMs ?? 5000;
|
|
|
| if (!binary) {
|
| throw new Error(
|
| "cursor-agent binary not found. Install it (curl https://cursor.com/install -fsS | bash) so ~/.local/bin/cursor-agent exists, or pass a binary path explicitly."
|
| );
|
| }
|
|
|
|
|
|
|
|
|
| let result: { stdout: string; stderr: string };
|
| try {
|
| result = await runCursorAgent(binary, ["--model", "--help"], timeoutMs);
|
| } catch (err: unknown) {
|
| const e = err as NodeJS.ErrnoException;
|
| if (e?.code === "ENOENT") {
|
| throw new Error(`cursor-agent binary not executable at ${binary}`);
|
| }
|
| throw err;
|
| }
|
| const combined = `${result.stdout}\n${result.stderr}`;
|
|
|
| const ids = parseCursorAgentModels(combined);
|
| if (ids.length === 0) {
|
| throw new Error("cursor-agent did not return an 'Available models:' line");
|
| }
|
|
|
| return ids.map((id) => ({
|
| id,
|
| name: humanizeCursorModelId(id),
|
| owned_by: "cursor" as const,
|
| }));
|
| }
|
|
|