| 1 | /** |
| 2 | * CursorDriver — `ProviderDriver` for the Cursor Agent SDK runtime. |
| 3 | * |
| 4 | * Provider status, model discovery, orchestration, and text generation use the |
| 5 | * official Cursor SDK with an instance browser login or CURSOR_API_KEY. |
| 6 | * |
| 7 | * @module provider/Drivers/CursorDriver |
| 8 | */ |
| 9 | import { ProviderDriverKind, ProviderSetupError } from "@t3tools/contracts"; |
| 10 | import { CursorSettings } from "../settings.ts"; |
| 11 | import * as Effect from "effect/Effect"; |
| 12 | import * as Crypto from "effect/Crypto"; |
| 13 | import * as FileSystem from "effect/FileSystem"; |
| 14 | import * as Path from "effect/Path"; |
| 15 | import * as Schema from "effect/Schema"; |
| 16 | import * as HttpClient from "effect/http/HttpClient"; |
| 17 | import { readCursorUsageLimits } from "./usageLimits.ts"; |
| 18 | |
| 19 | import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost"; |
| 20 | import { makeCursorTextGeneration } from "./textGeneration.ts"; |
| 21 | import { CursorAdapterV2Driver, type CursorAdapterV2DriverEnv } from "./adapter.ts"; |
| 22 | import { ProviderDriverError } from "@t3tools/provider-core/server/errors"; |
| 23 | import { buildInitialCursorProviderSnapshot, checkCursorProviderStatus } from "./status.ts"; |
| 24 | import * as CursorSdkCatalog from "./CursorSdkCatalog.ts"; |
| 25 | import { makeManagedServerProvider } from "@t3tools/provider-core/server/managedProvider"; |
| 26 | import { |
| 27 | defaultProviderContinuationIdentity, |
| 28 | type ProviderDriver, |
| 29 | type ProviderInstance, |
| 30 | } from "@t3tools/provider-core/server/driver"; |
| 31 | import { withInstanceIdentity } from "@t3tools/provider-core/server/instanceIdentity"; |
| 32 | import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment"; |
| 33 | import { makeManualOnlyProviderMaintenanceCapabilities } from "@t3tools/provider-core/server/maintenanceResolver"; |
| 34 | import { |
| 35 | haveProviderSnapshotSettingsChanged, |
| 36 | makeProviderSnapshotSettingsSource, |
| 37 | type ProviderSnapshotSettings, |
| 38 | } from "@t3tools/provider-core/server/snapshotSettings"; |
| 39 | import { probeCursorSkills } from "./skills.ts"; |
| 40 | import { makeCursorAuth } from "./auth.ts"; |
| 41 | import * as CursorCredentialStore from "./credentialStore.ts"; |
| 42 | import * as CursorAgentSdk from "./CursorAgentSdk.ts"; |
| 43 | import * as CursorSdk from "./CursorSdk.ts"; |
| 44 | import * as CursorKeychain from "./CursorKeychain.ts"; |
| 45 | import * as CursorUsageAccounts from "./CursorUsageAccounts.ts"; |
| 46 | |
| 47 | const decodeCursorSettings = Schema.decodeSync(CursorSettings); |
| 48 | const isSdkRunnerError = Schema.is(CursorAgentSdk.CursorAgentSdkRunnerError); |
| 49 | |
| 50 | const DRIVER_KIND = ProviderDriverKind.make("cursor"); |
| 51 | const MAINTENANCE_CAPABILITIES = makeManualOnlyProviderMaintenanceCapabilities({ |
| 52 | provider: DRIVER_KIND, |
| 53 | packageName: null, |
| 54 | }); |
| 55 | |
| 56 | export type CursorDriverEnv = |
| 57 | | CursorAdapterV2DriverEnv |
| 58 | | Crypto.Crypto |
| 59 | | FileSystem.FileSystem |
| 60 | | Path.Path |
| 61 | | HttpClient.HttpClient |
| 62 | | CursorSdk.CursorSdk |
| 63 | | ProviderHost.ProviderHost |
| 64 | | CursorKeychain.CursorKeychain; |
| 65 | |
| 66 | export const CursorDriver: ProviderDriver< |
| 67 | CursorSettings, |
| 68 | CursorDriverEnv, |
| 69 | CursorUsageAccounts.CursorUsageAccounts |
| 70 | > = { |
| 71 | driverKind: DRIVER_KIND, |
| 72 | metadata: { |
| 73 | displayName: "Cursor", |
| 74 | supportsMultipleInstances: true, |
| 75 | }, |
| 76 | configSchema: CursorSettings, |
| 77 | defaultConfig: (): CursorSettings => decodeCursorSettings({}), |
| 78 | // One account source per environment: the host's Cursor CLI login. |
| 79 | usage: { |
| 80 | kind: "scan", |
| 81 | provider: "cursor", |
| 82 | scan: ({ settings, windowStartMs, retentionCutoffMs, awaitRefresh }) => |
| 83 | CursorUsageAccounts.CursorUsageAccounts.pipe( |
| 84 | Effect.flatMap((accounts) => |
| 85 | accounts.scan({ |
| 86 | keychainUsageEnabled: settings.cursorKeychainUsageEnabled, |
| 87 | windowStartMs, |
| 88 | retentionCutoffMs, |
| 89 | awaitRefresh, |
| 90 | }), |
| 91 | ), |
| 92 | ), |
| 93 | }, |
| 94 | create: ({ instanceId, displayName, accentColor, environment, enabled, config }) => |
| 95 | Effect.gen(function* () { |
| 96 | const host = yield* ProviderHost.ProviderHost; |
| 97 | const fileSystem = yield* FileSystem.FileSystem; |
| 98 | const path = yield* Path.Path; |
| 99 | const httpClient = yield* HttpClient.HttpClient; |
| 100 | const keychain = yield* CursorKeychain.CursorKeychain; |
| 101 | const sdkRunner = yield* CursorAgentSdk.CursorAgentSdkRunner; |
| 102 | const processEnv = yield* mergeProviderInstanceEnvironment(environment); |
| 103 | const continuationIdentity = defaultProviderContinuationIdentity({ |
| 104 | driverKind: DRIVER_KIND, |
| 105 | instanceId, |
| 106 | }); |
| 107 | const stampIdentity = withInstanceIdentity({ |
| 108 | instanceId, |
| 109 | driverKind: DRIVER_KIND, |
| 110 | displayName: displayName ?? "Cursor", |
| 111 | accentColor, |
| 112 | continuationGroupKey: continuationIdentity.continuationKey, |
| 113 | }); |
| 114 | const effectiveConfig = { ...config, enabled } satisfies CursorSettings; |
| 115 | const credentials = yield* CursorCredentialStore.makeCursorCredentialStore( |
| 116 | instanceId, |
| 117 | path.join( |
| 118 | host.paths.stateDir, |
| 119 | "provider-auth", |
| 120 | encodeURIComponent(instanceId), |
| 121 | "cursor.json", |
| 122 | ), |
| 123 | ).pipe( |
| 124 | Effect.mapError( |
| 125 | (cause) => |
| 126 | new ProviderDriverError({ |
| 127 | driver: DRIVER_KIND, |
| 128 | instanceId, |
| 129 | detail: "Could not open the Cursor credential store.", |
| 130 | cause, |
| 131 | }), |
| 132 | ), |
| 133 | ); |
| 134 | const auth = yield* makeCursorAuth({ |
| 135 | instanceId, |
| 136 | displayName: displayName ?? "Cursor", |
| 137 | enabled, |
| 138 | ...(processEnv.CURSOR_API_KEY ? { apiKey: processEnv.CURSOR_API_KEY } : {}), |
| 139 | store: credentials.store, |
| 140 | credentialBinding: credentials.binding, |
| 141 | onChanged: (signedIn): Effect.Effect<void, ProviderSetupError> => |
| 142 | snapshot.refresh.pipe( |
| 143 | Effect.flatMap((provider) => |
| 144 | !signedIn || provider.auth.status === "authenticated" |
| 145 | ? Effect.void |
| 146 | : Effect.fail( |
| 147 | new ProviderSetupError({ |
| 148 | instanceId, |
| 149 | operation: "start", |
| 150 | detail: provider.message ?? "Could not verify the Cursor sign-in. Try again.", |
| 151 | }), |
| 152 | ), |
| 153 | ), |
| 154 | ), |
| 155 | }); |
| 156 | const stampSnapshot: typeof stampIdentity = (draft) => |
| 157 | stampIdentity({ |
| 158 | ...draft, |
| 159 | setup: { canAuthenticate: !auth.usesApiKey, canInstall: false }, |
| 160 | auth: { |
| 161 | ...draft.auth, |
| 162 | canLogout: !auth.usesApiKey, |
| 163 | }, |
| 164 | }); |
| 165 | |
| 166 | const orchestrationAdapter = yield* CursorAdapterV2Driver.create({ |
| 167 | instanceId, |
| 168 | displayName, |
| 169 | accentColor, |
| 170 | environment, |
| 171 | enabled, |
| 172 | config, |
| 173 | }).pipe( |
| 174 | Effect.provideService(CursorAgentSdk.CursorAgentSdkRunner, { |
| 175 | ...sdkRunner, |
| 176 | open: (input) => |
| 177 | auth.requireApiKey.pipe( |
| 178 | Effect.flatMap((apiKey) => |
| 179 | Effect.acquireRelease( |
| 180 | sdkRunner |
| 181 | .open({ ...input, options: { ...input.options, apiKey } }) |
| 182 | .pipe( |
| 183 | Effect.flatMap((session) => |
| 184 | Effect.cached(session.close).pipe( |
| 185 | Effect.map((close) => ({ ...session, close })), |
| 186 | ), |
| 187 | ), |
| 188 | ), |
| 189 | (session) => session.close.pipe(Effect.ignore), |
| 190 | ), |
| 191 | ), |
| 192 | auth.withAccess, |
| 193 | Effect.mapError((cause) => |
| 194 | isSdkRunnerError(cause) |
| 195 | ? cause |
| 196 | : new CursorAgentSdk.CursorAgentSdkRunnerError({ method: "open", cause }), |
| 197 | ), |
| 198 | ), |
| 199 | }), |
| 200 | Effect.mapError( |
| 201 | (cause) => |
| 202 | new ProviderDriverError({ |
| 203 | driver: DRIVER_KIND, |
| 204 | instanceId, |
| 205 | detail: "Failed to build Cursor orchestration adapter.", |
| 206 | cause, |
| 207 | }), |
| 208 | ), |
| 209 | ); |
| 210 | const textGeneration = yield* makeCursorTextGeneration( |
| 211 | effectiveConfig, |
| 212 | processEnv, |
| 213 | auth.requireApiKey, |
| 214 | auth.withAccess, |
| 215 | ); |
| 216 | |
| 217 | const checkProvider = auth.readApiKey.pipe( |
| 218 | Effect.orElseSucceed(() => undefined), |
| 219 | Effect.flatMap((apiKey) => |
| 220 | checkCursorProviderStatus( |
| 221 | effectiveConfig, |
| 222 | { |
| 223 | ...processEnv, |
| 224 | CURSOR_API_KEY: apiKey, |
| 225 | }, |
| 226 | auth.usesApiKey ? "api-key" : "browser", |
| 227 | ).pipe( |
| 228 | Effect.flatMap((snapshot) => |
| 229 | effectiveConfig.enabled && |
| 230 | snapshot.installed && |
| 231 | snapshot.auth.status === "authenticated" |
| 232 | ? host.settings.get.pipe( |
| 233 | Effect.flatMap((settings) => |
| 234 | readCursorUsageLimits( |
| 235 | effectiveConfig, |
| 236 | { ...processEnv, CURSOR_API_KEY: apiKey }, |
| 237 | settings.cursorKeychainUsageEnabled, |
| 238 | ), |
| 239 | ), |
| 240 | Effect.map((usageLimits) => ({ ...snapshot, usageLimits })), |
| 241 | ) |
| 242 | : Effect.succeed(snapshot), |
| 243 | ), |
| 244 | ), |
| 245 | ), |
| 246 | Effect.provideService(HttpClient.HttpClient, httpClient), |
| 247 | Effect.provideService(FileSystem.FileSystem, fileSystem), |
| 248 | Effect.provideService(Path.Path, path), |
| 249 | Effect.provideService(CursorKeychain.CursorKeychain, keychain), |
| 250 | Effect.map(stampSnapshot), |
| 251 | Effect.provide(CursorSdkCatalog.layer), |
| 252 | ); |
| 253 | |
| 254 | const snapshotSettings = yield* makeProviderSnapshotSettingsSource(effectiveConfig); |
| 255 | const snapshot = yield* makeManagedServerProvider<ProviderSnapshotSettings<CursorSettings>>({ |
| 256 | resolveMaintenance: () => Effect.succeed(MAINTENANCE_CAPABILITIES), |
| 257 | getSettings: snapshotSettings.getSettings, |
| 258 | streamSettings: snapshotSettings.streamSettings, |
| 259 | haveSettingsChanged: haveProviderSnapshotSettingsChanged, |
| 260 | initialSnapshot: (settings) => |
| 261 | buildInitialCursorProviderSnapshot(settings.provider).pipe(Effect.map(stampSnapshot)), |
| 262 | checkProvider, |
| 263 | }).pipe( |
| 264 | Effect.mapError( |
| 265 | (cause) => |
| 266 | new ProviderDriverError({ |
| 267 | driver: DRIVER_KIND, |
| 268 | instanceId, |
| 269 | detail: "Failed to build Cursor snapshot.", |
| 270 | cause, |
| 271 | }), |
| 272 | ), |
| 273 | ); |
| 274 | |
| 275 | return { |
| 276 | instanceId, |
| 277 | driverKind: DRIVER_KIND, |
| 278 | continuationIdentity, |
| 279 | displayName, |
| 280 | accentColor, |
| 281 | enabled, |
| 282 | auth: auth.controller, |
| 283 | snapshot, |
| 284 | snapshotForCwd: (cwd) => |
| 285 | !effectiveConfig.enabled |
| 286 | ? snapshot.getSnapshot |
| 287 | : Effect.all([ |
| 288 | snapshot.getSnapshot, |
| 289 | probeCursorSkills(cwd, processEnv).pipe( |
| 290 | Effect.provideService(FileSystem.FileSystem, fileSystem), |
| 291 | Effect.provideService(Path.Path, path), |
| 292 | Effect.mapError( |
| 293 | (cause) => |
| 294 | new ProviderDriverError({ |
| 295 | driver: DRIVER_KIND, |
| 296 | instanceId, |
| 297 | detail: `Failed to discover Cursor skills for '${cwd}'`, |
| 298 | cause, |
| 299 | }), |
| 300 | ), |
| 301 | ), |
| 302 | ]).pipe(Effect.map(([machineSnapshot, skills]) => ({ ...machineSnapshot, skills }))), |
| 303 | orchestrationAdapter, |
| 304 | textGeneration, |
| 305 | } satisfies ProviderInstance; |
| 306 | }), |
| 307 | }; |