Skip to content
Source
Sign inSign up
Open History to see the last commit

driver.ts

308 lines10.5 KB

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