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 */
9import { ProviderDriverKind, ProviderSetupError } from "@t3tools/contracts";
10import { CursorSettings } from "../settings.ts";
11import * as Effect from "effect/Effect";
12import * as Crypto from "effect/Crypto";
13import * as FileSystem from "effect/FileSystem";
14import * as Path from "effect/Path";
15import * as Schema from "effect/Schema";
16import * as HttpClient from "effect/http/HttpClient";
17import { readCursorUsageLimits } from "./usageLimits.ts";
18
19import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
20import { makeCursorTextGeneration } from "./textGeneration.ts";
21import { CursorAdapterV2Driver, type CursorAdapterV2DriverEnv } from "./adapter.ts";
22import { ProviderDriverError } from "@t3tools/provider-core/server/errors";
23import { buildInitialCursorProviderSnapshot, checkCursorProviderStatus } from "./status.ts";
24import * as CursorSdkCatalog from "./CursorSdkCatalog.ts";
25import { makeManagedServerProvider } from "@t3tools/provider-core/server/managedProvider";
26import {
27 defaultProviderContinuationIdentity,
28 type ProviderDriver,
29 type ProviderInstance,
30} from "@t3tools/provider-core/server/driver";
31import { withInstanceIdentity } from "@t3tools/provider-core/server/instanceIdentity";
32import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment";
33import { makeManualOnlyProviderMaintenanceCapabilities } from "@t3tools/provider-core/server/maintenanceResolver";
34import {
35 haveProviderSnapshotSettingsChanged,
36 makeProviderSnapshotSettingsSource,
37 type ProviderSnapshotSettings,
38} from "@t3tools/provider-core/server/snapshotSettings";
39import { probeCursorSkills } from "./skills.ts";
40import { makeCursorAuth } from "./auth.ts";
41import * as CursorCredentialStore from "./credentialStore.ts";
42import * as CursorAgentSdk from "./CursorAgentSdk.ts";
43import * as CursorSdk from "./CursorSdk.ts";
44import * as CursorKeychain from "./CursorKeychain.ts";
45import * as CursorUsageAccounts from "./CursorUsageAccounts.ts";
46
47const decodeCursorSettings = Schema.decodeSync(CursorSettings);
48const isSdkRunnerError = Schema.is(CursorAgentSdk.CursorAgentSdkRunnerError);
49
50const DRIVER_KIND = ProviderDriverKind.make("cursor");
51const MAINTENANCE_CAPABILITIES = makeManualOnlyProviderMaintenanceCapabilities({
52 provider: DRIVER_KIND,
53 packageName: null,
54});
55
56export 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
66export 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};