/** * CursorDriver — `ProviderDriver` for the Cursor Agent (`cursor-agent`) runtime. * * Cursor exposes an ACP-based CLI. Model catalog and capability refreshes * happen during the managed provider status check via Cursor's * `list_available_models` extension method. * * Text generation is supported via the ACP runtime — `makeCursorTextGeneration` * drives `runtime.prompt` with a structured-output schema and collects the * agent's `agent_message_chunk` stream into a single JSON blob. * * @module provider/Drivers/CursorDriver */ import { CursorSettings, ProviderDriverKind } from "@t3tools/contracts"; import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import { HttpClient } from "effect/unstable/http"; import { ChildProcessSpawner } from "effect/unstable/process"; import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts"; import { ServerConfig } from "../../config.ts"; import { ServerSettingsService } from "../../serverSettings.ts"; import { makeCursorTextGeneration } from "../../textGeneration/CursorTextGeneration.ts"; import { ProviderDriverError } from "../Errors.ts"; import { makeCursorAdapter } from "../Layers/CursorAdapter.ts"; import { buildInitialCursorProviderSnapshot, checkCursorProviderStatus, makeCursorModelDiscovery, enrichCursorSnapshot, } from "../Layers/CursorProvider.ts"; import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts"; import { makeManagedServerProvider } from "../makeManagedServerProvider.ts"; import { defaultProviderContinuationIdentity, type ProviderDriver, type ProviderInstance, } from "../ProviderDriver.ts"; import { withInstanceIdentity } from "./instanceIdentity.ts"; import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts"; import { makeCachedProviderMaintenanceResolution, makeManualOnlyProviderMaintenanceCapabilities, makeProviderMaintenanceCapabilities, type ProviderMaintenanceCapabilitiesResolver, resolveProviderMaintenanceCapabilitiesEffect, } from "../providerMaintenance.ts"; import { haveProviderSnapshotSettingsChanged, makeProviderSnapshotSettingsSource, type ProviderSnapshotSettings, } from "../providerUpdateSettings.ts"; import { probeCursorSkills } from "./CursorSkills.ts"; const decodeCursorSettings = Schema.decodeSync(CursorSettings); const DRIVER_KIND = ProviderDriverKind.make("cursor"); // cursor-agent updates itself, so the resolved executable is its own updater. // No executable means nothing to update, not "whatever is on PATH". const UPDATE: ProviderMaintenanceCapabilitiesResolver = { resolve: (context) => Effect.succeed( context ? makeProviderMaintenanceCapabilities({ provider: DRIVER_KIND, packageName: null, updateExecutable: context.resolvedCommandPath, updateArgs: ["update"], updateLockKey: "cursor-agent", platform: context.platform, }) : makeManualOnlyProviderMaintenanceCapabilities({ provider: DRIVER_KIND, packageName: null, }), ), }; export type CursorDriverEnv = | BackgroundPolicy.BackgroundPolicy | ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | FileSystem.FileSystem | HttpClient.HttpClient | Path.Path | ProviderEventLoggers | ServerConfig | ServerSettingsService; export const CursorDriver: ProviderDriver = { driverKind: DRIVER_KIND, metadata: { displayName: "Cursor", supportsMultipleInstances: true, }, configSchema: CursorSettings, defaultConfig: (): CursorSettings => decodeCursorSettings({}), create: ({ instanceId, displayName, accentColor, environment, enabled, config }) => Effect.gen(function* () { const crypto = yield* Crypto.Crypto; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const httpClient = yield* HttpClient.HttpClient; const serverSettings = yield* ServerSettingsService; const eventLoggers = yield* ProviderEventLoggers; const processEnv = mergeProviderInstanceEnvironment(environment); const continuationIdentity = defaultProviderContinuationIdentity({ driverKind: DRIVER_KIND, instanceId, }); const stampIdentity = withInstanceIdentity({ instanceId, driverKind: DRIVER_KIND, displayName, accentColor, continuationGroupKey: continuationIdentity.continuationKey, }); const effectiveConfig = { ...config, enabled } satisfies CursorSettings; const resolveMaintenance = yield* makeCachedProviderMaintenanceResolution( resolveProviderMaintenanceCapabilitiesEffect(UPDATE, { binaryPath: effectiveConfig.binaryPath, env: processEnv, }).pipe( Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path.Path, path), ), ); const adapter = yield* makeCursorAdapter(effectiveConfig, { environment: processEnv, ...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}), instanceId, }); const textGeneration = yield* makeCursorTextGeneration(effectiveConfig, processEnv); const discoverModels = yield* makeCursorModelDiscovery(effectiveConfig, processEnv); const checkProvider = checkCursorProviderStatus( effectiveConfig, processEnv, discoverModels, ).pipe( Effect.map(stampIdentity), Effect.provideService(Crypto.Crypto, crypto), Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path.Path, path), ); const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings); const snapshot = yield* makeManagedServerProvider>({ resolveMaintenance, getSettings: snapshotSettings.getSettings, streamSettings: snapshotSettings.streamSettings, haveSettingsChanged: haveProviderSnapshotSettingsChanged, initialSnapshot: (settings) => buildInitialCursorProviderSnapshot(settings.provider).pipe(Effect.map(stampIdentity)), checkProvider, // Model catalog and capabilities come exclusively from Cursor's // list_available_models extension method during provider checks. enrichSnapshot: ({ settings, snapshot: currentSnapshot, publishSnapshot }) => resolveMaintenance().pipe( Effect.flatMap((maintenanceCapabilities) => enrichCursorSnapshot({ settings: settings.provider, snapshot: currentSnapshot, maintenanceCapabilities, enableProviderUpdateChecks: settings.enableProviderUpdateChecks, publishSnapshot, stampIdentity, httpClient, }), ), Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), ), }).pipe( Effect.mapError( (cause) => new ProviderDriverError({ driver: DRIVER_KIND, instanceId, detail: `Failed to build Cursor snapshot: ${cause.message ?? String(cause)}`, cause, }), ), ); return { instanceId, driverKind: DRIVER_KIND, continuationIdentity, displayName, accentColor, enabled, snapshot, snapshotForCwd: (cwd) => !effectiveConfig.enabled ? snapshot.getSnapshot : Effect.all([ snapshot.getSnapshot, probeCursorSkills(cwd, processEnv).pipe( Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path.Path, path), Effect.mapError( (cause) => new ProviderDriverError({ driver: DRIVER_KIND, instanceId, detail: `Failed to discover Cursor skills for '${cwd}'`, cause, }), ), ), ]).pipe(Effect.map(([machineSnapshot, skills]) => ({ ...machineSnapshot, skills }))), adapter, textGeneration, } satisfies ProviderInstance; }), };