222 lines · 8.7 KB · TypeScript
RawDownload
/** * 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<CursorSettings, CursorDriverEnv> = {  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<ProviderSnapshotSettings<CursorSettings>>({        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;    }),};