feat(FN-3396): bundle Cursor CLI as a plugin provider with dashboard auth w

Merges FN-3396's full Cursor CLI provider integration (Steps 1–4): defines a CLI-backed provider contract, adds the `fusion-plugin-cursor-runtime` plugin package with process management and runtime probes, wires dashboard auth flows and UI (ProviderCard, onboarding modal, settings), and bundles the

Fusion-Task-Id: FN-3396
This commit is contained in:
Fusion
2026-05-07 04:10:15 -07:00
committed by gsxdsm
parent 2eba5a007c
commit dca07892ff
50 changed files with 1292 additions and 3 deletions

View File

@@ -2,6 +2,7 @@ import type { Request } from "express";
import { isGhAvailable, isGhAuthenticated } from "@fusion/core";
import { probeClaudeCli } from "../claude-cli-probe.js";
import { probeDroidCli } from "../droid-cli-probe.js";
import { probeCursorCliProvider } from "../runtime-provider-probes.js";
import { probeLlamaCpp } from "../llama-cpp-probe.js";
import { ApiError, badRequest, conflict } from "../api-error.js";
import { clearUsageCache } from "../usage.js";
@@ -310,6 +311,23 @@ export const registerAuthRoutes: ApiRouteRegistrar = (ctx) => {
});
}
if (store) {
let cursorEnabled = false;
try {
const globalSettings = await store.getGlobalSettingsStore().getSettings();
cursorEnabled = (globalSettings as Record<string, unknown>).useCursorCli === true;
} catch {
// best effort
}
const cursorBinary = await probeCursorCliProvider();
providers.push({
id: "cursor-cli",
name: "Cursor — via Cursor CLI",
authenticated: cursorEnabled && cursorBinary.available,
type: "cli" as const,
});
}
// Inject synthetic llama.cpp provider.
if (store) {
let llamaEnabled = false;
@@ -564,6 +582,51 @@ export const registerAuthRoutes: ApiRouteRegistrar = (ctx) => {
}
});
router.post("/auth/cursor-cli", async (req, res) => {
try {
if (!store) {
throw new ApiError(500, "Settings store unavailable");
}
const enabled = req.body?.enabled;
if (typeof enabled !== "boolean") {
throw badRequest("enabled must be a boolean");
}
if (enabled) {
const binary = await probeCursorCliProvider();
if (!binary.available) {
throw new ApiError(400, `Cannot enable Cursor CLI routing: ${binary.reason ?? "cursor binary not available"}`);
}
}
const settings = await store.updateGlobalSettings({ useCursorCli: enabled } as Record<string, unknown>);
invalidateAllGlobalSettingsCaches();
res.json({ enabled: (settings as Record<string, unknown>).useCursorCli === true, restartRequired: false });
} catch (err: unknown) {
if (err instanceof ApiError) throw err;
rethrowAsApiError(err);
}
});
router.get("/providers/cursor-cli/status", async (_req, res) => {
try {
const binary = await probeCursorCliProvider();
let enabled = false;
if (store) {
try {
const globalSettings = await store.getGlobalSettingsStore().getSettings();
enabled = (globalSettings as Record<string, unknown>).useCursorCli === true;
} catch {
// best effort
}
}
res.json({ binary, enabled, extension: null, ready: enabled && binary.available });
} catch (err: unknown) {
if (err instanceof ApiError) throw err;
rethrowAsApiError(err);
}
});
router.post("/auth/llama-cpp", async (req, res) => {
try {
if (!store) {

View File

@@ -14,6 +14,7 @@ export const registerModelRoutes: ApiRouteRegistrar = (ctx) => {
let useClaudeCli = false;
let useDroidCli = false;
let useLlamaCpp = false;
let useCursorCli = false;
let resolvedPlanningProvider: string | undefined;
let resolvedPlanningModelId: string | undefined;
if (store) {
@@ -27,6 +28,7 @@ export const registerModelRoutes: ApiRouteRegistrar = (ctx) => {
useClaudeCli = globalSettings.useClaudeCli === true;
useDroidCli = globalSettings.useDroidCli === true;
useLlamaCpp = globalSettings.useLlamaCpp === true;
useCursorCli = (globalSettings as Record<string, unknown>).useCursorCli === true;
const mergedSettings = await store.getSettingsFast();
const resolvedPlanningModel = resolvePlanningSettingsModel(mergedSettings);
@@ -87,6 +89,9 @@ export const registerModelRoutes: ApiRouteRegistrar = (ctx) => {
if (!useLlamaCpp) {
models = models.filter((m) => m.provider !== "llama-server");
}
if (!useCursorCli) {
models = models.filter((m) => m.provider !== "cursor-cli");
}
res.json({
models,