| @@ -19,6 +19,7 @@ import * as Layer from "effect/Layer"; · 18 unmodified lines |
| 19 | import * as Option from "effect/Option"; | 19 | import * as Option from "effect/Option"; |
| 20 | import * as Path from "effect/Path"; | 20 | import * as Path from "effect/Path"; |
| 21 | import * as Result from "effect/Result"; | 21 | import * as Result from "effect/Result"; |
| | 22 | import * as Schema from "effect/Schema"; |
| 22 | import { HttpClient } from "effect/unstable/http"; | 23 | import { HttpClient } from "effect/unstable/http"; |
| 23 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; | 24 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; |
| 24 | import { | 25 | import { |
| @@ -42,6 +43,7 @@ import { · 17 unmodified lines |
| 42 | type ProviderMaintenanceCapabilities, | 43 | type ProviderMaintenanceCapabilities, |
| 43 | } from "../providerMaintenance.ts"; | 44 | } from "../providerMaintenance.ts"; |
| 44 | import { AcpSessionRuntime } from "../acp/AcpSessionRuntime.ts"; | 45 | import { AcpSessionRuntime } from "../acp/AcpSessionRuntime.ts"; |
| | 46 | import { CursorListAvailableModelsResponse } from "../acp/CursorAcpExtension.ts"; |
| 45 | | 47 | |
| 46 | const PROVIDER = ProviderDriverKind.make("cursor"); | 48 | const PROVIDER = ProviderDriverKind.make("cursor"); |
| 47 | const CURSOR_PRESENTATION = { | 49 | const CURSOR_PRESENTATION = { |
| @@ -54,8 +56,6 @@ const EMPTY_CAPABILITIES: ModelCapabilities = createModelCapabilities({ · 6 unmodified lines |
| 54 | }); | 56 | }); |
| 55 | | 57 | |
| 56 | const CURSOR_ACP_MODEL_DISCOVERY_TIMEOUT_MS = 15_000; | 58 | const CURSOR_ACP_MODEL_DISCOVERY_TIMEOUT_MS = 15_000; |
| 57 | const CURSOR_ACP_MODEL_CAPABILITY_TIMEOUT = "4 seconds"; | | |
| 58 | const CURSOR_ACP_MODEL_DISCOVERY_CONCURRENCY = 4; | | |
| 59 | const CURSOR_PARAMETERIZED_MODEL_PICKER_MIN_VERSION_DATE = 2026_04_08; | 59 | const CURSOR_PARAMETERIZED_MODEL_PICKER_MIN_VERSION_DATE = 2026_04_08; |
| 60 | export const CURSOR_PARAMETERIZED_MODEL_PICKER_CAPABILITIES = { | 60 | export const CURSOR_PARAMETERIZED_MODEL_PICKER_CAPABILITIES = { |
| 61 | _meta: { | 61 | _meta: { |
| @@ -154,12 +154,6 @@ function normalizeCursorReasoningValue(value: string | null | undefined): string · 92 unmodified lines |
| 154 | } | 154 | } |
| 155 | } | 155 | } |
| 156 | | 156 | |
| 157 | function findCursorModelConfigOption( | | |
| 158 | configOptions: ReadonlyArray<EffectAcpSchema.SessionConfigOption>, | | |
| 159 | ): EffectAcpSchema.SessionConfigOption | undefined { | | |
| 160 | return configOptions.find((option) => option.category === "model"); | | |
| 161 | } | | |
| 162 | | | |
| 163 | function getCursorConfigOptionCategory(option: EffectAcpSchema.SessionConfigOption): string { | 157 | function getCursorConfigOptionCategory(option: EffectAcpSchema.SessionConfigOption): string { |
| 164 | return option.category?.trim().toLowerCase() ?? ""; | 158 | return option.category?.trim().toLowerCase() ?? ""; |
| 165 | } | 159 | } |
| @@ -373,36 +367,25 @@ function buildCursorDiscoveredModels( · 207 unmodified lines |
| 373 | }); | 367 | }); |
| 374 | } | 368 | } |
| 375 | | 369 | |
| 376 | function hasCursorModelCapabilities(model: Pick<ServerProviderModel, "capabilities">): boolean { | 370 | function buildCursorDiscoveredModelsFromAvailableModelsResponse( |
| 377 | return (model.capabilities?.optionDescriptors?.length ?? 0) > 0; | 371 | response: typeof CursorListAvailableModelsResponse.Type, |
| 378 | } | | |
| 379 | | | |
| 380 | export function buildCursorDiscoveredModelsFromConfigOptions( | | |
| 381 | configOptions: ReadonlyArray<EffectAcpSchema.SessionConfigOption> | null | undefined, | | |
| 382 | ): ReadonlyArray<ServerProviderModel> { | 372 | ): ReadonlyArray<ServerProviderModel> { |
| 383 | if (!configOptions || configOptions.length === 0) { | | |
| 384 | return []; | | |
| 385 | } | | |
| 386 | | | |
| 387 | const modelOption = findCursorModelConfigOption(configOptions); | | |
| 388 | const modelChoices = flattenSessionConfigSelectOptions(modelOption); | | |
| 389 | if (!modelOption || modelChoices.length === 0) { | | |
| 390 | return []; | | |
| 391 | } | | |
| 392 | | | |
| 393 | const currentModelValue = | | |
| 394 | modelOption.type === "select" ? modelOption.currentValue?.trim() || undefined : undefined; | | |
| 395 | const currentModelCapabilities = buildCursorCapabilitiesFromConfigOptions(configOptions); | | |
| 396 | | | |
| 397 | return buildCursorDiscoveredModels( | 373 | return buildCursorDiscoveredModels( |
| 398 | modelChoices.map((modelChoice) => ({ | 374 | response.models.flatMap((model) => { |
| 399 | slug: modelChoice.value.trim(), | 375 | const slug = model.value.trim(); |
| 400 | name: modelChoice.name.trim(), | 376 | const name = model.name.trim(); |
| 401 | capabilities: | 377 | if (!slug || !name) { |
| 402 | currentModelValue === modelChoice.value.trim() | 378 | return []; |
| 403 | ? currentModelCapabilities | 379 | } |
| 404 | : EMPTY_CAPABILITIES, | 380 | |
| 405 | })), | 381 | return [ |
| | 382 | { |
| | 383 | slug, |
| | 384 | name, |
| | 385 | capabilities: buildCursorCapabilitiesFromConfigOptions(model.configOptions), |
| | 386 | }, |
| | 387 | ]; |
| | 388 | }), |
| 406 | ); | 389 | ); |
| 407 | } | 390 | } |
| 408 | | 391 | |
| @@ -554,140 +537,28 @@ export function resolveCursorAcpConfigUpdates( · 145 unmodified lines |
| 554 | return updates; | 537 | return updates; |
| 555 | } | 538 | } |
| 556 | | 539 | |
| 557 | export const discoverCursorModelsViaAcp = ( | 540 | const discoverCursorModelsViaListAvailableModels = ( |
| 558 | cursorSettings: CursorSettings, | 541 | cursorSettings: CursorSettings, |
| 559 | environment: NodeJS.ProcessEnv = process.env, | 542 | environment: NodeJS.ProcessEnv = process.env, |
| 560 | ) => | 543 | ) => |
| 561 | withCursorAcpProbeRuntime( | 544 | withCursorAcpProbeRuntime( |
| 562 | cursorSettings, | 545 | cursorSettings, |
| 563 | (acp) => | 546 | (acp) => |
| 564 | Effect.map(acp.start(), (started) => | 547 | Effect.gen(function* () { |
| 565 | buildCursorDiscoveredModelsFromConfigOptions( | 548 | yield* acp.start(); |
| 566 | started.sessionSetupResult.configOptions ?? [], | 549 | const response = yield* acp.request("cursor/list_available_models", {}); |
| 567 | ), | 550 | const decoded = yield* Schema.decodeUnknownEffect(CursorListAvailableModelsResponse)( |
| 568 | ), | 551 | response, |
| | 552 | ); |
| | 553 | return buildCursorDiscoveredModelsFromAvailableModelsResponse(decoded); |
| | 554 | }), |
| 569 | environment, | 555 | environment, |
| 570 | ); | 556 | ); |
| 571 | | 557 | |
| 572 | export const discoverCursorModelCapabilitiesViaAcp = ( | 558 | export const discoverCursorModelsViaAcp = ( |
| 573 | cursorSettings: CursorSettings, | 559 | cursorSettings: CursorSettings, |
| 574 | existingModels: ReadonlyArray<ServerProviderModel>, | | |
| 575 | environment: NodeJS.ProcessEnv = process.env, | 560 | environment: NodeJS.ProcessEnv = process.env, |
| 576 | ) => | 561 | ) => discoverCursorModelsViaListAvailableModels(cursorSettings, environment); |
| 577 | withCursorAcpProbeRuntime( | | |
| 578 | cursorSettings, | | |
| 579 | (acp) => | | |
| 580 | Effect.gen(function* () { | | |
| 581 | const started = yield* acp.start(); | | |
| 582 | const initialConfigOptions = started.sessionSetupResult.configOptions ?? []; | | |
| 583 | const modelOption = findCursorModelConfigOption(initialConfigOptions); | | |
| 584 | const modelChoices = flattenSessionConfigSelectOptions(modelOption); | | |
| 585 | if (!modelOption || modelChoices.length === 0) { | | |
| 586 | return []; | | |
| 587 | } | | |
| 588 | | | |
| 589 | const currentModelValue = | | |
| 590 | modelOption.type === "select" ? modelOption.currentValue?.trim() || undefined : undefined; | | |
| 591 | const capabilitiesBySlug = new Map<string, ModelCapabilities>(); | | |
| 592 | if (currentModelValue) { | | |
| 593 | capabilitiesBySlug.set( | | |
| 594 | currentModelValue, | | |
| 595 | buildCursorCapabilitiesFromConfigOptions(initialConfigOptions), | | |
| 596 | ); | | |
| 597 | } | | |
| 598 | | | |
| 599 | const targetModelSlugs = new Set<string>(); | | |
| 600 | for (const model of existingModels) { | | |
| 601 | if (!model.isCustom && !hasCursorModelCapabilities(model)) { | | |
| 602 | targetModelSlugs.add(model.slug); | | |
| 603 | } | | |
| 604 | } | | |
| 605 | if (targetModelSlugs.size === 0) { | | |
| 606 | return buildCursorDiscoveredModels( | | |
| 607 | modelChoices.map((modelChoice) => ({ | | |
| 608 | slug: modelChoice.value.trim(), | | |
| 609 | name: modelChoice.name.trim(), | | |
| 610 | capabilities: capabilitiesBySlug.get(modelChoice.value.trim()) ?? EMPTY_CAPABILITIES, | | |
| 611 | })), | | |
| 612 | ); | | |
| 613 | } | | |
| 614 | | | |
| 615 | const probedCapabilities = yield* Effect.forEach( | | |
| 616 | modelChoices, | | |
| 617 | (modelChoice) => { | | |
| 618 | const modelSlug = modelChoice.value.trim(); | | |
| 619 | if ( | | |
| 620 | !modelSlug || | | |
| 621 | !targetModelSlugs.has(modelSlug) || | | |
| 622 | capabilitiesBySlug.has(modelSlug) | | |
| 623 | ) { | | |
| 624 | return Effect.void.pipe( | | |
| 625 | Effect.as<readonly [string, ModelCapabilities] | undefined>(undefined), | | |
| 626 | ); | | |
| 627 | } | | |
| 628 | | | |
| 629 | return withCursorAcpProbeRuntime( | | |
| 630 | cursorSettings, | | |
| 631 | (probeAcp) => | | |
| 632 | Effect.gen(function* () { | | |
| 633 | const probeStarted = yield* probeAcp.start(); | | |
| 634 | const probeConfigOptions = probeStarted.sessionSetupResult.configOptions ?? []; | | |
| 635 | const probeModelOption = findCursorModelConfigOption(probeConfigOptions); | | |
| 636 | const probeCurrentModelValue = | | |
| 637 | probeModelOption?.type === "select" | | |
| 638 | ? probeModelOption.currentValue?.trim() || undefined | | |
| 639 | : undefined; | | |
| 640 | yield* Effect.annotateCurrentSpan({ | | |
| 641 | "cursor.acp.model.value": modelSlug, | | |
| 642 | "cursor.acp.model.currentValue": probeCurrentModelValue, | | |
| 643 | "cursor.acp.config_option_id": probeModelOption?.id ?? modelOption.id, | | |
| 644 | }); | | |
| 645 | const nextConfigOptions = | | |
| 646 | probeCurrentModelValue === modelSlug | | |
| 647 | ? probeConfigOptions | | |
| 648 | : yield* probeAcp | | |
| 649 | .setConfigOption(probeModelOption?.id ?? modelOption.id, modelSlug) | | |
| 650 | .pipe( | | |
| 651 | Effect.map((response) => response.configOptions ?? probeConfigOptions), | | |
| 652 | ); | | |
| 653 | return [ | | |
| 654 | modelSlug, | | |
| 655 | buildCursorCapabilitiesFromConfigOptions(nextConfigOptions), | | |
| 656 | ] as const; | | |
| 657 | }), | | |
| 658 | environment, | | |
| 659 | ).pipe( | | |
| 660 | Effect.timeout(CURSOR_ACP_MODEL_CAPABILITY_TIMEOUT), | | |
| 661 | Effect.retry({ times: 3 }), | | |
| 662 | Effect.withSpan("cursor-acp-model-capability-probe"), | | |
| 663 | Effect.catchCause((cause) => | | |
| 664 | Effect.logWarning("Cursor ACP capability probe failed", { | | |
| 665 | modelSlug, | | |
| 666 | cause: Cause.pretty(cause), | | |
| 667 | }), | | |
| 668 | ), | | |
| 669 | ); | | |
| 670 | }, | | |
| 671 | { concurrency: CURSOR_ACP_MODEL_DISCOVERY_CONCURRENCY }, | | |
| 672 | ); | | |
| 673 | | | |
| 674 | for (const entry of probedCapabilities) { | | |
| 675 | if (!entry) { | | |
| 676 | continue; | | |
| 677 | } | | |
| 678 | capabilitiesBySlug.set(entry[0], entry[1]); | | |
| 679 | } | | |
| 680 | | | |
| 681 | return buildCursorDiscoveredModels( | | |
| 682 | modelChoices.map((modelChoice) => ({ | | |
| 683 | slug: modelChoice.value.trim(), | | |
| 684 | name: modelChoice.name.trim(), | | |
| 685 | capabilities: capabilitiesBySlug.get(modelChoice.value.trim()) ?? EMPTY_CAPABILITIES, | | |
| 686 | })), | | |
| 687 | ); | | |
| 688 | }).pipe(Effect.withSpan("cursor-acp-model-capability-discovery", {})), | | |
| 689 | environment, | | |
| 690 | ); | | |
| 691 | | 562 | |
| 692 | export function getCursorFallbackModels( | 563 | export function getCursorFallbackModels( |
| 693 | cursorSettings: Pick<CursorSettings, "customModels">, | 564 | cursorSettings: Pick<CursorSettings, "customModels">, |
| @@ -1219,36 +1090,30 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")( · 525 unmodified lines |
| 1219 | }); | 1090 | }); |
| 1220 | }); | 1091 | }); |
| 1221 | | 1092 | |
| 1222 | export function hasUncapturedCursorModels(snapshot: Pick<ServerProvider, "models">): boolean { | | |
| 1223 | return snapshot.models.some((model) => !model.isCustom && !hasCursorModelCapabilities(model)); | | |
| 1224 | } | | |
| 1225 | | | |
| 1226 | /** | 1093 | /** |
| 1227 | * Background capability enrichment for a Cursor snapshot. | 1094 | * Background maintenance enrichment for a Cursor snapshot. |
| 1228 | * | 1095 | * |
| 1229 | * Used by `CursorDriver` as the `makeManagedServerProvider.enrichSnapshot` | 1096 | * Used by `CursorDriver` as the `makeManagedServerProvider.enrichSnapshot` |
| 1230 | * hook: runs the slow ACP per-model capability probe, and republishes the | 1097 | * hook: republishes update/version advisory metadata without performing any |
| 1231 | * snapshot through `publishSnapshot` when new capabilities arrive. Skips | 1098 | * model or capability discovery. Cursor model data comes exclusively from |
| 1232 | * the probe when the provider is disabled, unauthenticated, or has no | 1099 | * `cursor/list_available_models` during provider status checks. |
| 1233 | * uncaptured models. Keeps `EMPTY_CAPABILITIES` and the `PROVIDER` literal | | |
| 1234 | * private to this module. | | |
| 1235 | */ | 1100 | */ |
| 1236 | export const enrichCursorSnapshot = (input: { | 1101 | export const enrichCursorSnapshot = (input: { |
| 1237 | readonly settings: CursorSettings; | 1102 | readonly settings: CursorSettings; |
| 1238 | readonly environment?: NodeJS.ProcessEnv; | | |
| 1239 | readonly snapshot: ServerProvider; | 1103 | readonly snapshot: ServerProvider; |
| 1240 | readonly maintenanceCapabilities: ProviderMaintenanceCapabilities; | 1104 | readonly maintenanceCapabilities: ProviderMaintenanceCapabilities; |
| 1241 | readonly publishSnapshot: (snapshot: ServerProvider) => Effect.Effect<void>; | 1105 | readonly publishSnapshot: (snapshot: ServerProvider) => Effect.Effect<void>; |
| 1242 | readonly stampIdentity?: (snapshot: ServerProvider) => ServerProvider; | 1106 | readonly stampIdentity?: (snapshot: ServerProvider) => ServerProvider; |
| 1243 | readonly httpClient: HttpClient.HttpClient; | 1107 | readonly httpClient: HttpClient.HttpClient; |
| 1244 | }): Effect.Effect<void, never, ChildProcessSpawner.ChildProcessSpawner> => { | 1108 | }): Effect.Effect<void> => { |
| 1245 | const { settings, snapshot, publishSnapshot } = input; | 1109 | const { settings, snapshot, publishSnapshot } = input; |
| 1246 | const stampIdentity = input.stampIdentity ?? ((value) => value); | 1110 | const stampIdentity = input.stampIdentity ?? ((value) => value); |
| 1247 | | 1111 | |
| 1248 | const enrichVersionAdvisory = enrichProviderSnapshotWithVersionAdvisory( | 1112 | if (!settings.enabled || snapshot.auth.status === "unauthenticated") { |
| 1249 | snapshot, | 1113 | return Effect.void; |
| 1250 | input.maintenanceCapabilities, | 1114 | } |
| 1251 | ).pipe( | 1115 | |
| | 1116 | return enrichProviderSnapshotWithVersionAdvisory(snapshot, input.maintenanceCapabilities).pipe( |
| 1252 | Effect.provideService(HttpClient.HttpClient, input.httpClient), | 1117 | Effect.provideService(HttpClient.HttpClient, input.httpClient), |
| 1253 | Effect.flatMap((enrichedSnapshot) => | 1118 | Effect.flatMap((enrichedSnapshot) => |
| 1254 | publishSnapshot(stampIdentity(enrichedSnapshot)).pipe(Effect.as(enrichedSnapshot)), | 1119 | publishSnapshot(stampIdentity(enrichedSnapshot)).pipe(Effect.as(enrichedSnapshot)), |
| @@ -1256,48 +1121,7 @@ export const enrichCursorSnapshot = (input: { · 1 unmodified line |
| 1256 | Effect.catchCause((cause) => | 1121 | Effect.catchCause((cause) => |
| 1257 | Effect.logWarning("Cursor version advisory enrichment failed", { | 1122 | Effect.logWarning("Cursor version advisory enrichment failed", { |
| 1258 | cause: Cause.pretty(cause), | 1123 | cause: Cause.pretty(cause), |
| 1259 | }).pipe(Effect.as(snapshot)), | 1124 | }).pipe(Effect.asVoid), |
| 1260 | ), | 1125 | ), |
| 1261 | ); | 1126 | ); |
| 1262 | | | |
| 1263 | return enrichVersionAdvisory.pipe( | | |
| 1264 | Effect.flatMap((baseSnapshot) => { | | |
| 1265 | if ( | | |
| 1266 | !settings.enabled || | | |
| 1267 | baseSnapshot.auth.status === "unauthenticated" || | | |
| 1268 | !hasUncapturedCursorModels(baseSnapshot) | | |
| 1269 | ) { | | |
| 1270 | return Effect.void; | | |
| 1271 | } | | |
| 1272 | | | |
| 1273 | return discoverCursorModelCapabilitiesViaAcp( | | |
| 1274 | settings, | | |
| 1275 | baseSnapshot.models, | | |
| 1276 | input.environment, | | |
| 1277 | ).pipe( | | |
| 1278 | Effect.flatMap((discoveredModels) => { | | |
| 1279 | if (discoveredModels.length === 0) { | | |
| 1280 | return Effect.void; | | |
| 1281 | } | | |
| 1282 | return publishSnapshot( | | |
| 1283 | stampIdentity({ | | |
| 1284 | ...baseSnapshot, | | |
| 1285 | models: providerModelsFromSettings( | | |
| 1286 | discoveredModels, | | |
| 1287 | PROVIDER, | | |
| 1288 | settings.customModels, | | |
| 1289 | EMPTY_CAPABILITIES, | | |
| 1290 | ), | | |
| 1291 | }), | | |
| 1292 | ); | | |
| 1293 | }), | | |
| 1294 | Effect.catchCause((cause) => | | |
| 1295 | Effect.logWarning("Cursor ACP background capability enrichment failed", { | | |
| 1296 | models: baseSnapshot.models.map((model) => model.slug), | | |
| 1297 | cause: Cause.pretty(cause), | | |
| 1298 | }).pipe(Effect.asVoid), | | |
| 1299 | ), | | |
| 1300 | ); | | |
| 1301 | }), | | |
| 1302 | ); | | |
| 1303 | }; | 1127 | }; |