/** * 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, type ServerProvider } from "@t3tools/contracts";import * as Duration from "effect/Duration";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 { 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, 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 type { ServerProviderDraft } from "../providerSnapshot.ts";import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";import { makeProviderMaintenanceCapabilities, type ProviderMaintenanceCapabilitiesResolver, resolveProviderMaintenanceCapabilitiesEffect,} from "../providerMaintenance.ts";import { haveProviderSnapshotSettingsChanged, makeProviderSnapshotSettingsSource, type ProviderSnapshotSettings,} from "../providerUpdateSettings.ts";const decodeCursorSettings = Schema.decodeSync(CursorSettings); const DRIVER_KIND = ProviderDriverKind.make("cursor");const SNAPSHOT_REFRESH_INTERVAL = Duration.minutes(5);const UPDATE: ProviderMaintenanceCapabilitiesResolver = { resolve: (options) => makeProviderMaintenanceCapabilities({ provider: DRIVER_KIND, packageName: null, updateExecutable: options?.binaryPath?.trim() || "cursor-agent", updateArgs: ["update"], updateLockKey: "cursor-agent", }),}; export type CursorDriverEnv = | ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | FileSystem.FileSystem | HttpClient.HttpClient | Path.Path | ProviderEventLoggers | ServerConfig | ServerSettingsService; const withInstanceIdentity = (input: { readonly instanceId: ProviderInstance["instanceId"]; readonly displayName: string | undefined; readonly accentColor: string | undefined; readonly continuationGroupKey: string; }) => (snapshot: ServerProviderDraft): ServerProvider => ({ ...snapshot, instanceId: input.instanceId, driver: DRIVER_KIND, ...(input.displayName ? { displayName: input.displayName } : {}), ...(input.accentColor ? { accentColor: input.accentColor } : {}), continuation: { groupKey: input.continuationGroupKey }, }); 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, displayName, accentColor, continuationGroupKey: continuationIdentity.continuationKey, }); const effectiveConfig = { ...config, enabled } satisfies CursorSettings; const maintenanceCapabilities = yield* resolveProviderMaintenanceCapabilitiesEffect(UPDATE, { binaryPath: effectiveConfig.binaryPath, env: processEnv, }); const adapter = yield* makeCursorAdapter(effectiveConfig, { environment: processEnv, ...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}), instanceId, }); const textGeneration = yield* makeCursorTextGeneration(effectiveConfig, processEnv); const checkProvider = checkCursorProviderStatus(effectiveConfig, processEnv).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>>({ maintenanceCapabilities, 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 }) => enrichCursorSnapshot({ settings: settings.provider, snapshot: currentSnapshot, maintenanceCapabilities, enableProviderUpdateChecks: settings.enableProviderUpdateChecks, publishSnapshot, stampIdentity, httpClient, }), refreshInterval: SNAPSHOT_REFRESH_INTERVAL, }).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, adapter, textGeneration, } satisfies ProviderInstance; }),};
/**
* 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, type ServerProvider } from "@t3tools/contracts";
import * as Duration from "effect/Duration";
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 { 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,
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 type { ServerProviderDraft } from "../providerSnapshot.ts";
import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
import {
makeProviderMaintenanceCapabilities,
type ProviderMaintenanceCapabilitiesResolver,
resolveProviderMaintenanceCapabilitiesEffect,
} from "../providerMaintenance.ts";
import {
haveProviderSnapshotSettingsChanged,
makeProviderSnapshotSettingsSource,
type ProviderSnapshotSettings,
} from "../providerUpdateSettings.ts";
const decodeCursorSettings = Schema.decodeSync(CursorSettings);
const DRIVER_KIND = ProviderDriverKind.make("cursor");
const SNAPSHOT_REFRESH_INTERVAL = Duration.minutes(5);
const UPDATE: ProviderMaintenanceCapabilitiesResolver = {
resolve: (options) =>
makeProviderMaintenanceCapabilities({
provider: DRIVER_KIND,
packageName: null,
updateExecutable: options?.binaryPath?.trim() || "cursor-agent",
updateArgs: ["update"],
updateLockKey: "cursor-agent",
}),
};
export type CursorDriverEnv =
| ChildProcessSpawner.ChildProcessSpawner
| Crypto.Crypto
| FileSystem.FileSystem
| HttpClient.HttpClient
| Path.Path
| ProviderEventLoggers
| ServerConfig
| ServerSettingsService;
const withInstanceIdentity =
(input: {
readonly instanceId: ProviderInstance["instanceId"];
readonly displayName: string | undefined;
readonly accentColor: string | undefined;
readonly continuationGroupKey: string;
}) =>
(snapshot: ServerProviderDraft): ServerProvider => ({
...snapshot,
instanceId: input.instanceId,
driver: DRIVER_KIND,
...(input.displayName ? { displayName: input.displayName } : {}),
...(input.accentColor ? { accentColor: input.accentColor } : {}),
continuation: { groupKey: input.continuationGroupKey },
});
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,
displayName,
accentColor,
continuationGroupKey: continuationIdentity.continuationKey,
});
const effectiveConfig = { ...config, enabled } satisfies CursorSettings;
const maintenanceCapabilities = yield* resolveProviderMaintenanceCapabilitiesEffect(UPDATE, {
binaryPath: effectiveConfig.binaryPath,
env: processEnv,
});
const adapter = yield* makeCursorAdapter(effectiveConfig, {
environment: processEnv,
...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}),
instanceId,
});
const textGeneration = yield* makeCursorTextGeneration(effectiveConfig, processEnv);
const checkProvider = checkCursorProviderStatus(effectiveConfig, processEnv).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>>({
maintenanceCapabilities,
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 }) =>
enrichCursorSnapshot({
settings: settings.provider,
snapshot: currentSnapshot,
maintenanceCapabilities,
enableProviderUpdateChecks: settings.enableProviderUpdateChecks,
publishSnapshot,
stampIdentity,
httpClient,
}),
refreshInterval: SNAPSHOT_REFRESH_INTERVAL,
}).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,
adapter,
textGeneration,
} satisfies ProviderInstance;
}),
};