diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 02ba1b20ae85..2b73304bebd1 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -70,6 +70,7 @@ "@t3tools/mobile-review-diff-native": "file:./modules/t3-review-diff", "@t3tools/mobile-terminal-native": "file:./modules/t3-terminal", "@t3tools/provider-core": "workspace:*", + "@t3tools/provider-cursor": "workspace:*", "@t3tools/provider-muse": "workspace:*", "@t3tools/provider-opencode": "workspace:*", "@t3tools/provider-pi": "workspace:*", diff --git a/apps/mobile/src/components/ProviderIcon.tsx b/apps/mobile/src/components/ProviderIcon.tsx index dda6e6f3b544..f2eb4e3c7f70 100644 --- a/apps/mobile/src/components/ProviderIcon.tsx +++ b/apps/mobile/src/components/ProviderIcon.tsx @@ -109,17 +109,6 @@ export function ProviderIcon(props: ProviderIconProps) { ); } - if (props.provider === "cursor") { - return ( - - - - ); - } - const packageIcon = getProviderClient(props.provider)?.icon; if (packageIcon) { const themeFill = (fill: { readonly light: string; readonly dark: string }) => diff --git a/apps/mobile/src/lib/providerClients.ts b/apps/mobile/src/lib/providerClients.ts index 11f7e13a3a78..cb469a28706c 100644 --- a/apps/mobile/src/lib/providerClients.ts +++ b/apps/mobile/src/lib/providerClients.ts @@ -1,11 +1,17 @@ import { isProviderDriverKind } from "@t3tools/contracts"; import { makeProviderClientRegistry } from "@t3tools/provider-core/client"; +import { cursorClient } from "@t3tools/provider-cursor/client"; import { museClient } from "@t3tools/provider-muse/client"; import { openCodeClient } from "@t3tools/provider-opencode/client"; import { piClient } from "@t3tools/provider-pi/client"; /** The provider client definitions this mobile build ships. */ -const providerClients = makeProviderClientRegistry([museClient, openCodeClient, piClient]); +const providerClients = makeProviderClientRegistry([ + cursorClient, + museClient, + openCodeClient, + piClient, +]); /** The client definition for a driver kind, or `undefined` for drivers drawn by hand. */ export function getProviderClient(driver: string | null | undefined) { diff --git a/apps/server/package.json b/apps/server/package.json index 11f202120f85..de20a01de3b9 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -39,6 +39,7 @@ "@noble/hashes": "catalog:", "@opencode-ai/sdk": "^1.3.15", "@t3tools/provider-core": "workspace:*", + "@t3tools/provider-cursor": "workspace:*", "@t3tools/provider-muse": "workspace:*", "@t3tools/provider-opencode": "workspace:*", "@t3tools/provider-pi": "workspace:*", diff --git a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.testkit.ts b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.testkit.ts index a3a72f741138..3f2f6581f90d 100644 --- a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.testkit.ts +++ b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.testkit.ts @@ -1,6 +1,4 @@ import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; -import * as OtelEnvironment from "@t3tools/shared/otelEnvironment"; -import { DEFAULT_SIGNAL_EXPORT } from "@t3tools/shared/observability"; import type { InteractionUpdate, RunResult } from "@cursor/sdk"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { @@ -15,23 +13,21 @@ import * as Effect from "effect/Effect"; import * as Fiber from "effect/Fiber"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; -import * as Path from "effect/Path"; -import * as PlatformError from "effect/PlatformError"; import * as Schema from "effect/Schema"; -import * as ServerConfig from "../../config.ts"; +import { layerTestProviderHost } from "@t3tools/provider-testing/host"; import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator"; import { ProviderAdapterDriverCreateError } from "@t3tools/provider-core/server/adapterDriver"; import * as ProviderAdapterRegistry from "../ProviderAdapterRegistry.ts"; import type { OrchestratorV2ProviderReplayHarness } from "../testkit/ProviderReplayHarness.ts"; -import * as CursorAgentSdk from "./CursorAgentSdk.ts"; +import { CursorAdapterV2Driver } from "@t3tools/provider-cursor/server"; +import * as CursorAgentSdk from "@t3tools/provider-cursor/server/CursorAgentSdk"; import { CURSOR_DEFAULT_INSTANCE_ID, CURSOR_DRIVER_KIND, - CursorAdapterV2Driver, cursorSdkModelSelection, makeCursorAgentOptions, -} from "./CursorAdapterV2.ts"; +} from "@t3tools/provider-cursor/testing"; import type { ProviderAdapterV2RuntimePolicy } from "@t3tools/provider-core/server/ProviderAdapter"; import type { RuntimePolicyV2Override } from "../RuntimePolicy.ts"; @@ -513,91 +509,6 @@ function layerCursorAgentSdkReplay( ); } -function makeReplayServerConfig( - scenario: string, -): Effect.Effect< - ServerConfig.ServerConfig["Service"], - PlatformError.PlatformError, - FileSystem.FileSystem | Path.Path -> { - return Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const baseDir = yield* fs.makeTempDirectory({ - prefix: `t3-orchestration-v2-cursor-${scenario}-`, - }); - const stateDir = path.join(baseDir, "userdata"); - const logsDir = path.join(stateDir, "logs"); - const providerLogsDir = path.join(logsDir, "provider"); - const terminalLogsDir = path.join(logsDir, "terminals"); - const attachmentsDir = path.join(stateDir, "attachments"); - const environmentThemesDir = path.join(stateDir, "themes"); - const worktreesDir = path.join(baseDir, "worktrees"); - const providerStatusCacheDir = path.join(baseDir, "caches"); - for (const directory of [ - stateDir, - logsDir, - providerLogsDir, - terminalLogsDir, - attachmentsDir, - environmentThemesDir, - worktreesDir, - providerStatusCacheDir, - ]) { - yield* fs.makeDirectory(directory, { recursive: true }); - } - return { - logLevel: "Error", - traceMinLevel: "Info", - traceTimingEnabled: true, - traceBatchWindowMs: 200, - traceMaxBytes: 10 * 1024 * 1024, - traceMaxFiles: 10, - otelEnvironment: OtelEnvironment.none, - otlpTracesUrl: undefined, - otlpMetricsUrl: undefined, - otlpLogsUrl: undefined, - otlpTracesExport: DEFAULT_SIGNAL_EXPORT, - otlpMetricsExport: DEFAULT_SIGNAL_EXPORT, - otlpLogsExport: DEFAULT_SIGNAL_EXPORT, - mode: "web", - port: 0, - host: undefined, - cwd: process.cwd(), - baseDir, - staticDir: undefined, - devUrl: undefined, - devAllowedOrigins: [], - noBrowser: false, - startupPresentation: "browser", - tailscaleServeEnabled: false, - tailscaleServePort: 443, - desktopBootstrapToken: undefined, - autoBootstrapProjectFromCwd: false, - logWebSocketEvents: false, - stateDir, - dbPath: path.join(stateDir, "state.sqlite"), - keybindingsConfigPath: path.join(stateDir, "keybindings.json"), - settingsPath: path.join(stateDir, "settings.json"), - providerStatusCacheDir, - worktreesDir, - attachmentsDir, - browserArtifactsDir: path.join(stateDir, "browser-artifacts"), - environmentThemesDir, - logsDir, - serverLogPath: path.join(logsDir, "server.log"), - serverTracePath: path.join(logsDir, "server.trace.ndjson"), - providerLogsDir, - providerEventLogPath: path.join(providerLogsDir, "events.log"), - terminalLogsDir, - anonymousIdPath: path.join(stateDir, "anonymous-id"), - environmentIdPath: path.join(stateDir, "environment-id"), - serverRuntimeStatePath: path.join(stateDir, "server-runtime.json"), - secretsDir: path.join(stateDir, "secrets"), - }; - }); -} - export function layer( transcript: CursorAgentSdkReplayTranscript, options?: { @@ -605,10 +516,6 @@ export function layer( readonly assertCompleteOnFinalize?: boolean; }, ) { - const layerServerConfig = Layer.effect( - ServerConfig.ServerConfig, - makeReplayServerConfig(transcript.scenario).pipe(Effect.orDie), - ).pipe(Layer.provide(NodeServices.layer)); // Skill discovery also scans user roots under HOME; an empty HOME keeps // replays from picking up the host's own skills. const layerHostEnvironment = Layer.effect( @@ -630,7 +537,7 @@ export function layer( Layer.provide( Layer.mergeAll( layerCursorAgentSdkReplay(transcript, options), - layerServerConfig, + layerTestProviderHost().pipe(Layer.provide(NodeServices.layer)), layerHostEnvironment, NodeServices.layer, IdAllocator.layer, diff --git a/apps/server/src/orchestration-v2/CommandPolicy.test.ts b/apps/server/src/orchestration-v2/CommandPolicy.test.ts index 256ecd45e75a..517229c222ea 100644 --- a/apps/server/src/orchestration-v2/CommandPolicy.test.ts +++ b/apps/server/src/orchestration-v2/CommandPolicy.test.ts @@ -12,7 +12,7 @@ import { import * as Effect from "effect/Effect"; import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts"; -import { CursorProviderCapabilitiesV2 } from "./Adapters/CursorAdapterV2.ts"; +import { CursorProviderCapabilitiesV2 } from "@t3tools/provider-cursor/testing"; import { GrokProviderCapabilitiesV2 } from "./Adapters/GrokAdapterV2.ts"; import * as CommandPolicy from "./CommandPolicy.ts"; diff --git a/apps/server/src/orchestration-v2/builtInProviderAdapterDrivers.ts b/apps/server/src/orchestration-v2/builtInProviderAdapterDrivers.ts index 3e33d5b2de41..e48ceb60177c 100644 --- a/apps/server/src/orchestration-v2/builtInProviderAdapterDrivers.ts +++ b/apps/server/src/orchestration-v2/builtInProviderAdapterDrivers.ts @@ -12,7 +12,7 @@ import { CodexAdapterV2Driver, type CodexAdapterV2DriverEnv } from "./Adapters/C import { CursorAdapterV2Driver, type CursorAdapterV2DriverEnv, -} from "./Adapters/CursorAdapterV2.ts"; +} from "@t3tools/provider-cursor/server"; import { GrokAdapterV2Driver, type GrokAdapterV2DriverEnv } from "./Adapters/GrokAdapterV2.ts"; import { OpenCodeAdapterV2Driver, diff --git a/apps/server/src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts b/apps/server/src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts index a0de3392631c..b71043aabc4e 100644 --- a/apps/server/src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts +++ b/apps/server/src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts @@ -43,7 +43,7 @@ import { canReuseCodexContextUsage, } from "../Adapters/CodexAdapterV2.ts"; import { AcpProviderCapabilitiesV2 } from "../Adapters/AcpAdapterV2.ts"; -import { CursorProviderCapabilitiesV2 } from "../Adapters/CursorAdapterV2.ts"; +import { CursorProviderCapabilitiesV2 } from "@t3tools/provider-cursor/testing"; import * as EventSink from "../EventSink.ts"; import * as EventStore from "../EventStore.ts"; import * as LegacyV1ThreadImporter from "../legacy/LegacyV1ThreadImporter.ts"; diff --git a/apps/server/src/provider/AntigravityProvider.test.ts b/apps/server/src/provider/AntigravityProvider.test.ts index 77a436aaf7be..69502047f73d 100644 --- a/apps/server/src/provider/AntigravityProvider.test.ts +++ b/apps/server/src/provider/AntigravityProvider.test.ts @@ -22,6 +22,7 @@ import * as BackgroundPolicy from "../background/BackgroundPolicy.ts"; import * as ServerConfig from "../config.ts"; import * as ServerSettings from "../serverSettings.ts"; import * as ProviderHostLive from "./ProviderHostLive.ts"; +import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; import type { AcpSessionRuntimeStartResult } from "./acp/AcpSessionRuntime.ts"; import { buildAntigravityModelsFromSession, @@ -116,9 +117,10 @@ const layerTest = ProviderHostLive.layer.pipe( }), ), Layer.provideMerge(ServerSettings.layerTest()), + Layer.provideMerge(ServerSecretStore.layer), Layer.provide( ServerConfig.layerTest(process.cwd(), { prefix: "t3-antigravity-provider-test-" }).pipe( - Layer.provide(NodeServices.layer), + Layer.provideMerge(NodeServices.layer), ), ), ); diff --git a/apps/server/src/provider/Drivers/AntigravityDriver.test.ts b/apps/server/src/provider/Drivers/AntigravityDriver.test.ts index d08cacb60dcd..33aa39f5598b 100644 --- a/apps/server/src/provider/Drivers/AntigravityDriver.test.ts +++ b/apps/server/src/provider/Drivers/AntigravityDriver.test.ts @@ -34,6 +34,7 @@ import * as ModelManifest from "../ModelManifest.ts"; import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator"; import { AntigravityDriver } from "./AntigravityDriver.ts"; import * as ProviderHostLive from "../ProviderHostLive.ts"; +import * as ServerSecretStore from "../../auth/ServerSecretStore.ts"; const hostPlatform = HostProcessPlatform.defaultValue(); const windowsHost = hostPlatform === "win32"; @@ -264,7 +265,10 @@ const layerDeps = ServerConfig.layerTest(process.cwd(), { Layer.provideMerge(ModelManifest.layerTest), Layer.provideMerge(IdAllocator.layer), ); -const layerTest = ProviderHostLive.layer.pipe(Layer.provideMerge(layerDeps)); +const layerTest = ProviderHostLive.layer.pipe( + Layer.provideMerge(ServerSecretStore.layer), + Layer.provideMerge(layerDeps), +); it.layer(layerTest)("AntigravityDriver", (it) => { it.effect.skipIf(windowsHost)( diff --git a/apps/server/src/provider/Drivers/GrokDriver.test.ts b/apps/server/src/provider/Drivers/GrokDriver.test.ts index f1737f71f391..e0da3203902c 100644 --- a/apps/server/src/provider/Drivers/GrokDriver.test.ts +++ b/apps/server/src/provider/Drivers/GrokDriver.test.ts @@ -17,6 +17,7 @@ import { GrokDriver } from "./GrokDriver.ts"; import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator"; import * as ProviderHostLive from "../ProviderHostLive.ts"; +import * as ServerSecretStore from "../../auth/ServerSecretStore.ts"; const layerDeps = ServerConfig.layerTest(process.cwd(), { prefix: "t3-grok-driver-update-", @@ -42,7 +43,10 @@ const layerDeps = ServerConfig.layerTest(process.cwd(), { ), ), ); -const layerTest = ProviderHostLive.layer.pipe(Layer.provideMerge(layerDeps)); +const layerTest = ProviderHostLive.layer.pipe( + Layer.provideMerge(ServerSecretStore.layer), + Layer.provideMerge(layerDeps), +); const noSpawner = ChildProcessSpawner.make(() => Effect.die("Disabled Grok must not spawn a process"), diff --git a/apps/server/src/provider/ProviderHostLive.ts b/apps/server/src/provider/ProviderHostLive.ts index 965cb47e365f..372ac75765c6 100644 --- a/apps/server/src/provider/ProviderHostLive.ts +++ b/apps/server/src/provider/ProviderHostLive.ts @@ -5,13 +5,17 @@ * @module provider/ProviderHostLive */ import { ProviderHost } from "@t3tools/provider-core/server/ProviderHost"; +import { ProviderCredentialError } from "@t3tools/provider-core/server/errors"; +import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import { resolveAttachmentPath } from "../attachmentStore.ts"; +import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; import * as BackgroundPolicy from "../background/BackgroundPolicy.ts"; import * as ServerConfig from "../config.ts"; import * as ServerSettings from "../serverSettings.ts"; +import * as ProviderCredentialStore from "./ProviderCredentialStore.ts"; export const layer = Layer.effect( ProviderHost, @@ -19,6 +23,8 @@ export const layer = Layer.effect( const config = yield* ServerConfig.ServerConfig; const serverSettings = yield* ServerSettings.ServerSettingsService; const backgroundPolicy = yield* BackgroundPolicy.BackgroundPolicy; + const secrets = yield* ServerSecretStore.ServerSecretStore; + const crypto = yield* Crypto.Crypto; return ProviderHost.of({ paths: { cwd: config.cwd, @@ -34,6 +40,30 @@ export const layer = Layer.effect( shouldRunBackgroundWork: backgroundPolicy.shouldRunScopeWork, resolveAttachmentPath: (attachment) => resolveAttachmentPath({ attachmentsDir: config.attachmentsDir, attachment }), + credentials: (namespace, bindingId) => + ProviderCredentialStore.make(namespace, bindingId).pipe( + Effect.map((store) => ({ + binding: store.binding, + get: store.get.pipe( + Effect.mapError((cause) => new ProviderCredentialError({ operation: "get", cause })), + ), + set: (credentials: Uint8Array) => + store + .set(credentials) + .pipe( + Effect.mapError( + (cause) => new ProviderCredentialError({ operation: "set", cause }), + ), + ), + remove: store.remove.pipe( + Effect.mapError( + (cause) => new ProviderCredentialError({ operation: "remove", cause }), + ), + ), + })), + Effect.provideService(ServerSecretStore.ServerSecretStore, secrets), + Effect.provideService(Crypto.Crypto, crypto), + ), }); }), ); diff --git a/apps/server/src/provider/ProviderInstanceRegistry.test.ts b/apps/server/src/provider/ProviderInstanceRegistry.test.ts index 5c1bbb165822..353c29fb90da 100644 --- a/apps/server/src/provider/ProviderInstanceRegistry.test.ts +++ b/apps/server/src/provider/ProviderInstanceRegistry.test.ts @@ -31,12 +31,12 @@ import { EnvironmentId, type ClaudeSettings, type CodexSettings, - type CursorSettings, type GrokSettings, ProviderDriverKind, type ProviderInstanceConfigMap, ProviderInstanceId, } from "@t3tools/contracts"; +import type { CursorSettings } from "@t3tools/provider-cursor/settings"; import { HostProcessPlatform, isHostWindows } from "@t3tools/shared/hostProcess"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; @@ -53,7 +53,7 @@ import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; import * as ServerSettings from "../serverSettings.ts"; import { ClaudeDriver, type ClaudeDriverEnv } from "./Drivers/ClaudeDriver.ts"; import { CodexDriver, type CodexDriverEnv } from "./Drivers/CodexDriver.ts"; -import { CursorDriver, type CursorDriverEnv } from "./Drivers/CursorDriver.ts"; +import { CursorDriver, type CursorDriverEnv } from "@t3tools/provider-cursor/server"; import { GrokDriver, type GrokDriverEnv } from "./Drivers/GrokDriver.ts"; import { OpenCodeDriver, type OpenCodeDriverEnv } from "@t3tools/provider-opencode/server"; import type { OpenCodeSettings } from "@t3tools/provider-opencode/settings"; diff --git a/apps/server/src/provider/ProviderOrchestrationAdapterInfrastructure.ts b/apps/server/src/provider/ProviderOrchestrationAdapterInfrastructure.ts index 6640288c7ff4..2cbb70fbbdec 100644 --- a/apps/server/src/provider/ProviderOrchestrationAdapterInfrastructure.ts +++ b/apps/server/src/provider/ProviderOrchestrationAdapterInfrastructure.ts @@ -2,7 +2,7 @@ import * as Layer from "effect/Layer"; import * as ClaudeAdapterV2 from "../orchestration-v2/Adapters/ClaudeAdapterV2.ts"; import * as CodexAdapterV2 from "../orchestration-v2/Adapters/CodexAdapterV2.ts"; -import * as CursorAgentSdk from "../orchestration-v2/Adapters/CursorAgentSdk.ts"; +import * as CursorAgentSdk from "@t3tools/provider-cursor/server/CursorAgentSdk"; import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator"; import * as ProviderContinuationRequests from "@t3tools/provider-core/server/continuationRequests"; diff --git a/apps/server/src/provider/builtInDrivers.ts b/apps/server/src/provider/builtInDrivers.ts index 8cfcc007e9c1..516e52d0e49b 100644 --- a/apps/server/src/provider/builtInDrivers.ts +++ b/apps/server/src/provider/builtInDrivers.ts @@ -24,7 +24,7 @@ import { AcpRegistryDriver, type AcpRegistryDriverEnv } from "./Drivers/AcpRegis import { AntigravityDriver, type AntigravityDriverEnv } from "./Drivers/AntigravityDriver.ts"; import { ClaudeDriver, type ClaudeDriverEnv } from "./Drivers/ClaudeDriver.ts"; import { CodexDriver, type CodexDriverEnv } from "./Drivers/CodexDriver.ts"; -import { CursorDriver, type CursorDriverEnv } from "./Drivers/CursorDriver.ts"; +import { CursorDriver, type CursorDriverEnv } from "@t3tools/provider-cursor/server"; import { GrokDriver, type GrokDriverEnv } from "./Drivers/GrokDriver.ts"; import { OpenCodeDriver, type OpenCodeDriverEnv } from "@t3tools/provider-opencode/server"; import { MuseDriver, type MuseDriverEnv } from "@t3tools/provider-muse/server"; diff --git a/apps/server/src/usage/cursorUsageReader.test.ts b/apps/server/src/usage/cursorUsageReader.test.ts index 0c6783757748..4897f4604a9e 100644 --- a/apps/server/src/usage/cursorUsageReader.test.ts +++ b/apps/server/src/usage/cursorUsageReader.test.ts @@ -1,6 +1,6 @@ import { assert, describe, it } from "@effect/vitest"; -import { CursorKeychainTimeoutError } from "../provider/cursorKeychainToken.ts"; +import { CursorKeychainTimeoutError } from "@t3tools/provider-cursor/server"; import { readCursorAccountUsage } from "./cursorUsageReader.ts"; describe("readCursorAccountUsage", () => { diff --git a/apps/server/src/usage/cursorUsageReader.ts b/apps/server/src/usage/cursorUsageReader.ts index 86ba226a0529..95b87f1984bb 100644 --- a/apps/server/src/usage/cursorUsageReader.ts +++ b/apps/server/src/usage/cursorUsageReader.ts @@ -12,7 +12,7 @@ import type { UsageRecord } from "./usageTranscripts.ts"; import { CursorKeychainTimeoutError, readMacCursorAccessToken, -} from "../provider/cursorKeychainToken.ts"; +} from "@t3tools/provider-cursor/server"; function object(value: unknown): Record { return typeof value === "object" && value !== null && !Array.isArray(value) diff --git a/apps/web/package.json b/apps/web/package.json index 372c956ec04e..64012d9bdab3 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -29,6 +29,7 @@ "@t3tools/client-runtime": "workspace:*", "@t3tools/contracts": "workspace:*", "@t3tools/provider-core": "workspace:*", + "@t3tools/provider-cursor": "workspace:*", "@t3tools/provider-muse": "workspace:*", "@t3tools/provider-opencode": "workspace:*", "@t3tools/provider-pi": "workspace:*", diff --git a/apps/web/src/components/chat/ProviderInstanceIcon.tsx b/apps/web/src/components/chat/ProviderInstanceIcon.tsx index 5337fbc9cf3f..927f78ceb4f2 100644 --- a/apps/web/src/components/chat/ProviderInstanceIcon.tsx +++ b/apps/web/src/components/chat/ProviderInstanceIcon.tsx @@ -3,7 +3,7 @@ import { type CSSProperties, memo } from "react"; import { providerInstanceInitials } from "@t3tools/client-runtime/state/provider-instance-display"; import { ProviderDriverKind } from "@t3tools/contracts"; -import { AntigravityIcon, ClaudeAI, CursorIcon, GrokIcon, Icon, OpenAI } from "../Icons"; +import { AntigravityIcon, ClaudeAI, GrokIcon, Icon, OpenAI } from "../Icons"; import { cn } from "~/lib/utils"; import { providerClients } from "../settings/providerDriverMeta"; @@ -17,7 +17,6 @@ import { const PROVIDER_ICON_BY_PROVIDER: Partial> = { [ProviderDriverKind.make("codex")]: OpenAI, [ProviderDriverKind.make("claudeAgent")]: ClaudeAI, - [ProviderDriverKind.make("cursor")]: CursorIcon, [ProviderDriverKind.make("grok")]: GrokIcon, [ProviderDriverKind.make("antigravity")]: AntigravityIcon, }; @@ -25,7 +24,6 @@ const PROVIDER_ICON_BY_PROVIDER: Partial> = { const PROVIDER_TEXT_COLOR_BY_PROVIDER: Partial> = { [ProviderDriverKind.make("codex")]: "text-black dark:text-white", [ProviderDriverKind.make("claudeAgent")]: "text-[#d97757]", - [ProviderDriverKind.make("cursor")]: "text-[#26251E] dark:text-[#EDECEC]", [ProviderDriverKind.make("grok")]: "text-[#0F0F0F] dark:text-[#F5F5F5]", [ProviderDriverKind.make("antigravity")]: "text-[#5b87bf]", }; diff --git a/apps/web/src/components/settings/providerDriverMeta.ts b/apps/web/src/components/settings/providerDriverMeta.ts index ae70d22ec9d7..aca6ffbc724f 100644 --- a/apps/web/src/components/settings/providerDriverMeta.ts +++ b/apps/web/src/components/settings/providerDriverMeta.ts @@ -3,11 +3,11 @@ import { AntigravitySettings, ClaudeSettings, CodexSettings, - CursorSettings, GrokSettings, ProviderDriverKind, } from "@t3tools/contracts"; import { makeProviderClientRegistry } from "@t3tools/provider-core/client"; +import { cursorClient } from "@t3tools/provider-cursor/client"; import { museClient } from "@t3tools/provider-muse/client"; import { openCodeClient } from "@t3tools/provider-opencode/client"; import { piClient } from "@t3tools/provider-pi/client"; @@ -24,20 +24,7 @@ export const providerClients = makeProviderClientRegistry([ label: "Claude", settingsSchema: ClaudeSettings, }, - { - driverKind: ProviderDriverKind.make("cursor"), - label: "Cursor", - settingsSchema: CursorSettings, - environmentFields: [ - { - name: "CURSOR_API_KEY", - label: "Cursor API key", - description: "Optional. Overrides browser sign-in for this provider.", - placeholder: "Paste API key", - sensitive: true, - }, - ], - }, + cursorClient, { driverKind: ProviderDriverKind.make("grok"), label: "Grok", diff --git a/docs/internals/adding-a-provider.md b/docs/internals/adding-a-provider.md index 3c6fa7f00649..b442053c8001 100644 --- a/docs/internals/adding-a-provider.md +++ b/docs/internals/adding-a-provider.md @@ -115,7 +115,7 @@ A provider lives in its own `packages/provider-` package, with Settings live in `providerInstances`; new providers are off by default. - **Package boundaries.** Server code imports only `@t3tools/provider-core/server/*`, contracts, and shared. It reaches the server through `ProviderHost` (paths, settings, background demand, - attachments), never through `apps/server`. Tests use `@t3tools/provider-testing`. + attachments, stored credentials), never through `apps/server`. Tests use `@t3tools/provider-testing`. - **Server registration:** the driver's entry in [`builtInDrivers.ts`](../../apps/server/src/provider/builtInDrivers.ts) and the adapter driver's in [`builtInProviderAdapterDrivers.ts`](../../apps/server/src/orchestration-v2/builtInProviderAdapterDrivers.ts). diff --git a/knip.jsonc b/knip.jsonc index 24d0f85db881..c3fed7f19200 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -36,7 +36,9 @@ ], // Keep the transitive Effect runtime pinned for standalone npm installs. // The Vite+ web build prerequisite. - "ignoreDependencies": ["@effect/platform-node-shared", "@t3tools/web"], + // Native and disk-loaded provider packages stay external to the bundle; the + // CLI archive and desktop sidecar stage them from this manifest. + "ignoreDependencies": ["@effect/platform-node-shared", "@t3tools/web", "@napi-rs/keyring"], }, "apps/desktop": { // Electron loads these bundles by filename rather than importing them. diff --git a/package.json b/package.json index e261ffedfb35..545a4247c098 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "tc": "vp run -r --concurrency-limit 2 typecheck", "lint": "vp lint --report-unused-disable-directives", "knip": "knip --preprocessor ./scripts/knip-schemas.ts", - "knip:check": "knip --include files,dependencies --no-config-hints && knip --workspace apps/server --workspace apps/desktop --workspace apps/web --workspace packages/client-runtime --workspace packages/contracts --workspace packages/effect-acp --workspace packages/effect-codex-app-server --workspace packages/shared --workspace packages/ssh --workspace packages/tailscale --workspace packages/provider-core --workspace packages/provider-testing --workspace packages/provider-pi --workspace packages/provider-muse --workspace packages/provider-opencode --exports --preprocessor ./scripts/knip-schemas.ts --no-config-hints", + "knip:check": "knip --include files,dependencies --no-config-hints && knip --workspace apps/server --workspace apps/desktop --workspace apps/web --workspace packages/client-runtime --workspace packages/contracts --workspace packages/effect-acp --workspace packages/effect-codex-app-server --workspace packages/shared --workspace packages/ssh --workspace packages/tailscale --workspace packages/provider-core --workspace packages/provider-testing --workspace packages/provider-pi --workspace packages/provider-muse --workspace packages/provider-opencode --workspace packages/provider-cursor --exports --preprocessor ./scripts/knip-schemas.ts --no-config-hints", "knip:production": "knip --production --preprocessor ./scripts/knip-schemas.ts", "lint:mobile": "node scripts/mobile-native-static-check.ts", "test": "vp run -r test", diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index a302412f179e..c43df190af3f 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -707,32 +707,6 @@ export const ClaudeSettings = makeProviderSettingsSchema( ); export type ClaudeSettings = typeof ClaudeSettings.Type; -export const CursorSettings = makeProviderSettingsSchema( - { - // Off by default like Grok and OpenCode. Users opt in from Settings. - enabled: Schema.Boolean.pipe( - Schema.withDecodingDefault(Effect.succeed(false)), - Schema.annotateKey({ providerSettingsForm: { hidden: true } }), - ), - // Keep V1's CLI configuration when V2 rewrites the shared settings file. - // V2's Cursor SDK does not use these fields. - binaryPath: Schema.optionalKey(TrimmedString).pipe( - Schema.annotateKey({ providerSettingsForm: { hidden: true } }), - ), - apiEndpoint: Schema.optionalKey(TrimmedString).pipe( - Schema.annotateKey({ providerSettingsForm: { hidden: true } }), - ), - customModels: Schema.Array(CustomModelSetting).pipe( - Schema.withDecodingDefault(Effect.succeed([])), - Schema.annotateKey({ providerSettingsForm: { hidden: true } }), - ), - }, - { - order: [], - }, -); -export type CursorSettings = typeof CursorSettings.Type; - export const GrokSettings = makeProviderSettingsSchema( { // Off by default (like Cursor and OpenCode): the binding is not yet diff --git a/packages/provider-core/src/server/ProviderHost.ts b/packages/provider-core/src/server/ProviderHost.ts index f0a695da43d4..318037cdcb3b 100644 --- a/packages/provider-core/src/server/ProviderHost.ts +++ b/packages/provider-core/src/server/ProviderHost.ts @@ -17,9 +17,12 @@ import type { } from "@t3tools/contracts"; import * as Context from "effect/Context"; import type * as Effect from "effect/Effect"; +import type * as Option from "effect/Option"; import type * as Scope from "effect/Scope"; import type * as Stream from "effect/Stream"; +import type { ProviderCredentialError } from "./errors.ts"; + export interface ProviderHostPaths { /** Directory the server was started in; the fallback cwd for sessions without a project. */ readonly cwd: string; @@ -31,6 +34,15 @@ export interface ProviderHostPaths { readonly providerStatusCacheDir: string; } +/** One opaque credential blob in the server's secret store. Only the provider decodes it. */ +export interface ProviderCredentials { + /** Instances whose bindings are equal share these credentials. */ + readonly binding: { readonly owner: "t3"; readonly key: string }; + readonly get: Effect.Effect, ProviderCredentialError>; + readonly set: (credentials: Uint8Array) => Effect.Effect; + readonly remove: Effect.Effect; +} + export interface ProviderHostShape { readonly paths: ProviderHostPaths; readonly settings: { @@ -47,6 +59,11 @@ export interface ProviderHostShape { * resolve inside the attachments directory. */ readonly resolveAttachmentPath: (attachment: ChatAttachment) => string | null; + /** The credentials stored for `bindingId`, usually an instance id, under a provider-chosen namespace. */ + readonly credentials: ( + namespace: string, + bindingId: string, + ) => Effect.Effect; } export class ProviderHost extends Context.Service()( diff --git a/packages/provider-core/src/server/errors.ts b/packages/provider-core/src/server/errors.ts index eda2eaa686ec..46ca6e6541fe 100644 --- a/packages/provider-core/src/server/errors.ts +++ b/packages/provider-core/src/server/errors.ts @@ -19,3 +19,16 @@ export class ProviderDriverError extends Schema.TaggedError return `Provider driver '${this.driver}' failed to create instance '${this.instanceId}': ${this.detail}`; } } + +/** Reading or writing a provider's stored credentials failed. */ +export class ProviderCredentialError extends Schema.TaggedError()( + "ProviderCredentialError", + { + operation: Schema.Literals(["get", "set", "remove"]), + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Could not ${this.operation} stored provider credentials.`; + } +} diff --git a/packages/provider-core/src/server/managedProvider.test.ts b/packages/provider-core/src/server/managedProvider.test.ts index a4d7dc752f1c..6d9e3827531b 100644 --- a/packages/provider-core/src/server/managedProvider.test.ts +++ b/packages/provider-core/src/server/managedProvider.test.ts @@ -117,6 +117,7 @@ function layerProviderHost(input: { }, shouldRunBackgroundWork: () => Effect.succeed(input.runBackgroundWork), resolveAttachmentPath: () => null, + credentials: () => Effect.die("unused"), }), ); } diff --git a/packages/provider-cursor/package.json b/packages/provider-cursor/package.json new file mode 100644 index 000000000000..4a50b9647332 --- /dev/null +++ b/packages/provider-cursor/package.json @@ -0,0 +1,47 @@ +{ + "name": "@t3tools/provider-cursor", + "private": true, + "type": "module", + "exports": { + "./client": { + "types": "./src/client.ts", + "import": "./src/client.ts" + }, + "./settings": { + "types": "./src/settings.ts", + "import": "./src/settings.ts" + }, + "./server": { + "types": "./src/server.ts", + "import": "./src/server.ts" + }, + "./testing": { + "types": "./src/testing.ts", + "import": "./src/testing.ts" + }, + "./server/CursorAgentSdk": { + "types": "./src/server/CursorAgentSdk.ts", + "import": "./src/server/CursorAgentSdk.ts" + } + }, + "scripts": { + "typecheck": "tsc --noEmit", + "test": "vp test run --config ../../vite.config.ts --dir ." + }, + "dependencies": { + "@cursor/sdk": "1.0.35", + "@napi-rs/keyring": "^1.3.0", + "@t3tools/contracts": "workspace:*", + "@t3tools/provider-core": "workspace:*", + "@t3tools/shared": "workspace:*", + "effect": "catalog:", + "yaml": "catalog:" + }, + "devDependencies": { + "@effect/platform-node": "catalog:", + "@effect/vitest": "catalog:", + "@t3tools/provider-testing": "workspace:*", + "@types/node": "catalog:", + "vite-plus": "catalog:" + } +} diff --git a/packages/provider-cursor/src/client.ts b/packages/provider-cursor/src/client.ts new file mode 100644 index 000000000000..c76378ab1739 --- /dev/null +++ b/packages/provider-cursor/src/client.ts @@ -0,0 +1,34 @@ +/** + * Cursor's client definition: settings form, label, sign-in environment + * field, and glyph. Browser- and React Native-safe. + * + * @module provider-cursor/client + */ +import { ProviderDriverKind } from "@t3tools/contracts"; +import { defineProviderClient } from "@t3tools/provider-core/client"; + +import { CursorSettings } from "./settings.ts"; + +export const cursorClient = defineProviderClient({ + driverKind: ProviderDriverKind.make("cursor"), + label: "Cursor", + settingsSchema: CursorSettings, + environmentFields: [ + { + name: "CURSOR_API_KEY", + label: "Cursor API key", + description: "Optional. Overrides browser sign-in for this provider.", + placeholder: "Paste API key", + sensitive: true, + }, + ], + icon: { + viewBox: "0 0 466.73 532.09", + fill: { light: "#26251E", dark: "#EDECEC" }, + paths: [ + { + d: "M457.43,125.94L244.42,2.96c-6.84-3.95-15.28-3.95-22.12,0L9.3,125.94c-5.75,3.32-9.3,9.46-9.3,16.11v247.99c0,6.65,3.55,12.79,9.3,16.11l213.01,122.98c6.84,3.95,15.28,3.95,22.12,0l213.01-122.98c5.75-3.32,9.3-9.46,9.3-16.11v-247.99c0-6.65-3.55-12.79-9.3-16.11h-.01ZM444.05,151.99l-205.63,356.16c-1.39,2.4-5.06,1.42-5.06-1.36v-233.21c0-4.66-2.49-8.97-6.53-11.31L24.87,145.67c-2.4-1.39-1.42-5.06,1.36-5.06h411.26c5.84,0,9.49,6.33,6.57,11.39h-.01Z", + }, + ], + }, +}); diff --git a/packages/provider-cursor/src/server.ts b/packages/provider-cursor/src/server.ts new file mode 100644 index 000000000000..471737aa58fd --- /dev/null +++ b/packages/provider-cursor/src/server.ts @@ -0,0 +1,10 @@ +/** + * Cursor's server entry: the driver the server registers, its adapter + * driver, the SDK runner layer the server provides once, and the keychain + * token reader the usage scanner shares. + * + * @module provider-cursor/server + */ +export { CursorDriver, type CursorDriverEnv } from "./server/driver.ts"; +export { CursorAdapterV2Driver, type CursorAdapterV2DriverEnv } from "./server/adapter.ts"; +export { CursorKeychainTimeoutError, readMacCursorAccessToken } from "./server/keychainToken.ts"; diff --git a/apps/server/src/orchestration-v2/Adapters/CursorAgentSdk.test.ts b/packages/provider-cursor/src/server/CursorAgentSdk.test.ts similarity index 98% rename from apps/server/src/orchestration-v2/Adapters/CursorAgentSdk.test.ts rename to packages/provider-cursor/src/server/CursorAgentSdk.test.ts index 5754ec28706c..6a4e209bf192 100644 --- a/apps/server/src/orchestration-v2/Adapters/CursorAgentSdk.test.ts +++ b/packages/provider-cursor/src/server/CursorAgentSdk.test.ts @@ -5,7 +5,7 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import { vi } from "vite-plus/test"; -import * as ProviderEventLoggers from "../../provider/ProviderEventLoggers.ts"; +import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers"; import * as CursorAgentSdk from "./CursorAgentSdk.ts"; const cursorSdkMock = vi.hoisted(() => { @@ -92,7 +92,7 @@ const cursorSdkMock = vi.hoisted(() => { }; }); -vi.mock("../../provider/cursorSdk.ts", () => ({ +vi.mock("./sdk.ts", () => ({ Agent: { create: cursorSdkMock.create, resume: cursorSdkMock.resume, diff --git a/apps/server/src/orchestration-v2/Adapters/CursorAgentSdk.ts b/packages/provider-cursor/src/server/CursorAgentSdk.ts similarity index 98% rename from apps/server/src/orchestration-v2/Adapters/CursorAgentSdk.ts rename to packages/provider-cursor/src/server/CursorAgentSdk.ts index 036a93a0e557..30d6090fab4e 100644 --- a/apps/server/src/orchestration-v2/Adapters/CursorAgentSdk.ts +++ b/packages/provider-cursor/src/server/CursorAgentSdk.ts @@ -16,9 +16,8 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Schema from "effect/Schema"; -import { Agent, createAgentPlatform } from "../../provider/cursorSdk.ts"; -import type { EventNdjsonLogger } from "../../provider/EventNdjsonLogger.ts"; -import * as ProviderEventLoggers from "../../provider/ProviderEventLoggers.ts"; +import { Agent, createAgentPlatform } from "./sdk.ts"; +import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers"; export const CURSOR_AGENT_SDK_PROTOCOL = "cursor-agent-sdk.local" as const; export const CURSOR_PROVIDER = ProviderDriverKind.make("cursor"); @@ -77,7 +76,7 @@ export interface CursorAgentSdkRunnerShape { export class CursorAgentSdkRunner extends Context.Service< CursorAgentSdkRunner, CursorAgentSdkRunnerShape ->()("t3/orchestration-v2/Adapters/CursorAgentSdk/CursorAgentSdkRunner") {} +>()("@t3tools/provider-cursor/server/CursorAgentSdk/CursorAgentSdkRunner") {} export interface CursorAgentSdkLoggedAgentOptions { readonly model?: AgentOptions["model"]; @@ -276,7 +275,7 @@ export function loggedCursorSendOptions( } function makeCursorAgentSdkProtocolLogger(input: { - readonly nativeEventLogger: EventNdjsonLogger | undefined; + readonly nativeEventLogger: ProviderEventLoggers.EventNdjsonLogger | undefined; readonly threadId: ThreadId; readonly providerSessionId: OrchestrationV2ProviderSession["id"]; }): CursorAgentSdkProtocolLogger | undefined { diff --git a/apps/server/src/provider/CursorSdkCatalog.test.ts b/packages/provider-cursor/src/server/CursorSdkCatalog.test.ts similarity index 100% rename from apps/server/src/provider/CursorSdkCatalog.test.ts rename to packages/provider-cursor/src/server/CursorSdkCatalog.test.ts diff --git a/apps/server/src/provider/CursorSdkCatalog.ts b/packages/provider-cursor/src/server/CursorSdkCatalog.ts similarity index 95% rename from apps/server/src/provider/CursorSdkCatalog.ts rename to packages/provider-cursor/src/server/CursorSdkCatalog.ts index e866955dd55f..1a0f6b649496 100644 --- a/apps/server/src/provider/CursorSdkCatalog.ts +++ b/packages/provider-cursor/src/server/CursorSdkCatalog.ts @@ -7,7 +7,7 @@ import * as Exit from "effect/Exit"; import * as Layer from "effect/Layer"; import * as Schema from "effect/Schema"; -import { AuthenticationError, Cursor, CursorSdkError } from "./cursorSdk.ts"; +import { AuthenticationError, Cursor, CursorSdkError } from "./sdk.ts"; export interface CursorSdkCatalogSnapshot { readonly user: SDKUser; @@ -33,7 +33,7 @@ export interface CursorSdkCatalogShape { } export class CursorSdkCatalog extends Context.Service()( - "t3/provider/CursorSdkCatalog", + "@t3tools/provider-cursor/server/CursorSdkCatalog", ) {} function isAuthenticationFailure(cause: unknown): boolean { diff --git a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.test.ts b/packages/provider-cursor/src/server/adapter.test.ts similarity index 95% rename from apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.test.ts rename to packages/provider-cursor/src/server/adapter.test.ts index cf37340d0e18..730cbb7649f8 100644 --- a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.test.ts +++ b/packages/provider-cursor/src/server/adapter.test.ts @@ -2,7 +2,6 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, describe, it } from "@effect/vitest"; import type { InteractionUpdate } from "@cursor/sdk"; import { - CursorSettings, EnvironmentId, MessageId, NodeId, @@ -13,6 +12,7 @@ import { RunId, ThreadId, } from "@t3tools/contracts"; +import { CursorSettings } from "../settings.ts"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -22,10 +22,11 @@ import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import * as Stream from "effect/Stream"; -import * as ServerConfig from "../../config.ts"; +import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost"; +import { layerTestProviderHost } from "@t3tools/provider-testing/host"; import * as McpProviderSession from "@t3tools/provider-core/server/mcpSession"; import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator"; -import { ProviderAdapterV2RuntimePolicy } from "@t3tools/provider-core/server/ProviderAdapter"; +import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter"; import { cursorMcpServers, cursorRuntimeAgentPolicy, @@ -33,8 +34,8 @@ import { makeCursorAgentOptions, makeCursorAdapterV2, nestedToolCallFromEnvelope, -} from "./CursorAdapterV2.ts"; -import { isCursorCancellationError, loggedCursorAgentOptions } from "./CursorAgentSdk.ts"; +} from "./adapter.ts"; +import * as CursorAgentSdk from "./CursorAgentSdk.ts"; const decodeCursorSettings = Schema.decodeEffect(CursorSettings); @@ -57,7 +58,7 @@ describe("CursorAdapterV2", () => { const instanceId = ProviderInstanceId.make("cursor"); const threadId = ThreadId.make("cursor-lifecycle-thread"); const modelSelection = { instanceId, model: "composer-2.5" }; - const runtimePolicy = ProviderAdapterV2RuntimePolicy.make({ + const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({ runtimeMode: "full-access", interactionMode: "default", cwd: workspace, @@ -69,10 +70,8 @@ describe("CursorAdapterV2", () => { fileSystem, path, idAllocator: yield* IdAllocator.IdAllocatorV2, - serverConfig: yield* ServerConfig.ServerConfig.pipe( - Effect.provide( - ServerConfig.layerTest(workspace, { prefix: "cursor-v2-lifecycle-config-" }), - ), + host: yield* ProviderHost.ProviderHost.pipe( + Effect.provide(layerTestProviderHost({ cwd: workspace })), ), runner: { assertComplete: Effect.void, @@ -219,7 +218,7 @@ describe("CursorAdapterV2", () => { const instanceId = ProviderInstanceId.make("cursor"); const threadId = ThreadId.make("cursor-transport-error-thread"); const modelSelection = { instanceId, model: "composer-2.5" }; - const runtimePolicy = ProviderAdapterV2RuntimePolicy.make({ + const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({ runtimeMode: "full-access", interactionMode: "default", cwd: workspace, @@ -231,8 +230,8 @@ describe("CursorAdapterV2", () => { fileSystem, path, idAllocator: yield* IdAllocator.IdAllocatorV2, - serverConfig: yield* ServerConfig.ServerConfig.pipe( - Effect.provide(ServerConfig.layerTest(workspace, { prefix: "cursor-v2-error-config-" })), + host: yield* ProviderHost.ProviderHost.pipe( + Effect.provide(layerTestProviderHost({ cwd: workspace })), ), runner: { assertComplete: Effect.void, @@ -367,7 +366,7 @@ describe("CursorAdapterV2", () => { const instanceId = ProviderInstanceId.make("cursor"); const threadId = ThreadId.make("cursor-search-thread"); const modelSelection = { instanceId, model: "composer-2.5" }; - const runtimePolicy = ProviderAdapterV2RuntimePolicy.make({ + const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({ runtimeMode: "full-access", interactionMode: "default", cwd: workspace, @@ -572,8 +571,8 @@ describe("CursorAdapterV2", () => { fileSystem, path, idAllocator: yield* IdAllocator.IdAllocatorV2, - serverConfig: yield* ServerConfig.ServerConfig.pipe( - Effect.provide(ServerConfig.layerTest(workspace, { prefix: "cursor-v2-search-config-" })), + host: yield* ProviderHost.ProviderHost.pipe( + Effect.provide(layerTestProviderHost({ cwd: workspace })), ), runner: { assertComplete: Effect.void, @@ -908,7 +907,7 @@ describe("CursorAdapterV2", () => { }); assert.deepEqual(options.mcpServers, cursorMcpServers(threadId)); - const logged = JSON.stringify(loggedCursorAgentOptions(options)); + const logged = JSON.stringify(CursorAgentSdk.loggedCursorAgentOptions(options)); assert.notInclude(logged, "secret-cursor-api-key"); assert.notInclude(logged, "secret-cursor-mcp-token"); } finally { @@ -917,9 +916,9 @@ describe("CursorAdapterV2", () => { }); it("recognizes direct and SDK-wrapped abort failures as cancellation", () => { - assert.isTrue(isCursorCancellationError({ name: "AbortError" })); + assert.isTrue(CursorAgentSdk.isCursorCancellationError({ name: "AbortError" })); assert.isTrue( - isCursorCancellationError({ + CursorAgentSdk.isCursorCancellationError({ name: "ConnectError", cause: { name: "ConnectError", @@ -927,8 +926,8 @@ describe("CursorAdapterV2", () => { }, }), ); - assert.isFalse(isCursorCancellationError(new Error("request failed"))); - assert.isFalse(isCursorCancellationError(null)); + assert.isFalse(CursorAgentSdk.isCursorCancellationError(new Error("request failed"))); + assert.isFalse(CursorAgentSdk.isCursorCancellationError(null)); }); it("preserves failed nested read calls when Cursor omits their path", () => { diff --git a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.ts b/packages/provider-cursor/src/server/adapter.ts similarity index 98% rename from apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.ts rename to packages/provider-cursor/src/server/adapter.ts index 273564e786c4..33796270205a 100644 --- a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.ts +++ b/packages/provider-cursor/src/server/adapter.ts @@ -11,7 +11,6 @@ import type { import { formatReadToolLabel, formatSearchToolLabel } from "@t3tools/shared/toolActivity"; import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; import { - CursorSettings, isOrchestrationV2WorkActive, defaultInstanceIdForDriver, type ChatAttachment, @@ -30,6 +29,7 @@ import { type ProviderInstanceId, type ThreadId, } from "@t3tools/contracts"; +import { CursorSettings } from "../settings.ts"; import * as DateTime from "effect/DateTime"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; @@ -42,17 +42,16 @@ import * as Ref from "effect/Ref"; import * as Schema from "effect/Schema"; import * as Stream from "effect/Stream"; -import { resolveAttachmentPath } from "../../attachmentStore.ts"; -import * as ServerConfig from "../../config.ts"; +import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost"; import { mcpToolPresentation } from "@t3tools/provider-core/server/mcpToolPresentation"; import * as McpProviderSession from "@t3tools/provider-core/server/mcpSession"; -import { CursorTransportFailure } from "../../provider/acp/CursorTransportFailure.ts"; -import { cursorSdkModelSelection } from "../../provider/cursorSdkModel.ts"; +import { CursorTransportFailure } from "./transportFailure.ts"; +import { cursorSdkModelSelection } from "./sdkModel.ts"; import { discoverCursorSkills, hasCursorSkillMention, rewriteCursorSkillMentions, -} from "../../provider/Drivers/CursorSkills.ts"; +} from "./skills.ts"; import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment"; import { t3OrchestrationPromptForFirstRun } from "@t3tools/provider-core/server/orchestrationInstructions"; import { buildRuntimeInstructions } from "@t3tools/provider-core/server/runtimeInstructions"; @@ -75,7 +74,7 @@ import { subagentThreadTitle, } from "@t3tools/provider-core/server/subagentProjection"; import * as CursorAgentSdk from "./CursorAgentSdk.ts"; -export { cursorSdkModelSelection } from "../../provider/cursorSdkModel.ts"; +export { cursorSdkModelSelection } from "./sdkModel.ts"; export const CURSOR_DRIVER_KIND = CursorAgentSdk.CURSOR_PROVIDER; export const CURSOR_DEFAULT_INSTANCE_ID = defaultInstanceIdForDriver(CURSOR_DRIVER_KIND); @@ -849,13 +848,13 @@ export interface CursorAdapterV2Options { readonly path: Path.Path; readonly idAllocator: IdAllocator.IdAllocatorV2Shape; readonly runner: CursorAgentSdk.CursorAgentSdkRunnerShape; - readonly serverConfig: ServerConfig.ServerConfig["Service"]; + readonly host: ProviderHost.ProviderHostShape; } export function makeCursorAdapterV2( adapterOptions: CursorAdapterV2Options, ): ProviderAdapter.ProviderAdapterV2Shape { - const { fileSystem, path, idAllocator, runner, serverConfig } = adapterOptions; + const { fileSystem, path, idAllocator, runner, host } = adapterOptions; const apiKey = adapterOptions.environment.CURSOR_API_KEY?.trim() || undefined; return ProviderAdapter.ProviderAdapterV2.of({ @@ -2133,8 +2132,7 @@ export function makeCursorAdapterV2( ? rawText : rewriteCursorSkillMentions(rawText, cursorSkillNames), attachments: turnInput.message.attachments, - resolveAttachmentPath: (attachment) => - resolveAttachmentPath({ attachmentsDir: serverConfig.attachmentsDir, attachment }), + resolveAttachmentPath: host.resolveAttachmentPath, }), runOrdinal: turnInput.runOrdinal, hasT3Mcp: cursorMcpServers(turnInput.threadId) !== undefined, @@ -2143,10 +2141,7 @@ export function makeCursorAdapterV2( turnInput.message.attachments.filter(isProviderNativeImageAttachment), (attachment: ChatAttachment) => Effect.gen(function* () { - const path = resolveAttachmentPath({ - attachmentsDir: serverConfig.attachmentsDir, - attachment, - }); + const path = host.resolveAttachmentPath(attachment); if (path === null) { return yield* new ProviderAdapter.ProviderAdapterProtocolError({ driver: CursorAgentSdk.CURSOR_PROVIDER, @@ -2608,7 +2603,7 @@ export type CursorAdapterV2DriverEnv = | FileSystem.FileSystem | Path.Path | IdAllocator.IdAllocatorV2 - | ServerConfig.ServerConfig; + | ProviderHost.ProviderHost; export const CursorAdapterV2Driver: ProviderAdapterDriver< CursorSettings, @@ -2624,7 +2619,7 @@ export const CursorAdapterV2Driver: ProviderAdapterDriver< const path = yield* Path.Path; const idAllocator = yield* IdAllocator.IdAllocatorV2; const runner = yield* CursorAgentSdk.CursorAgentSdkRunner; - const serverConfig = yield* ServerConfig.ServerConfig; + const host = yield* ProviderHost.ProviderHost; return makeCursorAdapterV2({ instanceId: input.instanceId, settings: { @@ -2636,7 +2631,7 @@ export const CursorAdapterV2Driver: ProviderAdapterDriver< path, idAllocator, runner, - serverConfig, + host, }); }, (effect, input) => @@ -2661,7 +2656,7 @@ const layer: Layer.Layer< | FileSystem.FileSystem | Path.Path | IdAllocator.IdAllocatorV2 - | ServerConfig.ServerConfig + | ProviderHost.ProviderHost > = Layer.effect( ProviderAdapter.ProviderAdapterV2, Effect.gen(function* () { @@ -2670,7 +2665,7 @@ const layer: Layer.Layer< const path = yield* Path.Path; const idAllocator = yield* IdAllocator.IdAllocatorV2; const runner = yield* CursorAgentSdk.CursorAgentSdkRunner; - const serverConfig = yield* ServerConfig.ServerConfig; + const host = yield* ProviderHost.ProviderHost; return makeCursorAdapterV2({ instanceId: CURSOR_DEFAULT_INSTANCE_ID, settings: DEFAULT_CURSOR_SETTINGS, @@ -2679,7 +2674,7 @@ const layer: Layer.Layer< path, idAllocator, runner, - serverConfig, + host, }); }), ); diff --git a/apps/server/src/provider/CursorAuth.test.ts b/packages/provider-cursor/src/server/auth.test.ts similarity index 99% rename from apps/server/src/provider/CursorAuth.test.ts rename to packages/provider-cursor/src/server/auth.test.ts index 6fa7133093c7..3d31211ac5ae 100644 --- a/apps/server/src/provider/CursorAuth.test.ts +++ b/packages/provider-cursor/src/server/auth.test.ts @@ -12,8 +12,8 @@ import * as Scope from "effect/Scope"; import * as Stream from "effect/Stream"; import * as TestClock from "effect/testing/TestClock"; -import { makeCursorAuth, type CursorAuthOptions } from "./CursorAuth.ts"; -import { InMemoryCredentialStore } from "./cursorSdk.ts"; +import { makeCursorAuth, type CursorAuthOptions } from "./auth.ts"; +import { InMemoryCredentialStore } from "./sdk.ts"; const instanceId = ProviderInstanceId.make("cursor-personal"); const owner = "owner-client"; diff --git a/apps/server/src/provider/CursorAuth.ts b/packages/provider-cursor/src/server/auth.ts similarity index 99% rename from apps/server/src/provider/CursorAuth.ts rename to packages/provider-cursor/src/server/auth.ts index 14988170b1eb..ca1c64d9b2c0 100644 --- a/apps/server/src/provider/CursorAuth.ts +++ b/packages/provider-cursor/src/server/auth.ts @@ -16,8 +16,8 @@ import * as Semaphore from "effect/Semaphore"; import * as Stream from "effect/Stream"; import * as SubscriptionRef from "effect/SubscriptionRef"; -import { Cursor, InMemoryCredentialStore } from "./cursorSdk.ts"; -import type { ProviderAuthController } from "./ProviderAuthService.ts"; +import { Cursor, InMemoryCredentialStore } from "./sdk.ts"; +import type { ProviderAuthController } from "@t3tools/provider-core/server/auth"; const AUTH_TIMEOUT_MS = 300_000; diff --git a/apps/server/src/provider/CursorCredentialStore.test.ts b/packages/provider-cursor/src/server/credentialStore.test.ts similarity index 62% rename from apps/server/src/provider/CursorCredentialStore.test.ts rename to packages/provider-cursor/src/server/credentialStore.test.ts index e3c0995ba072..5e176b661c46 100644 --- a/apps/server/src/provider/CursorCredentialStore.test.ts +++ b/packages/provider-cursor/src/server/credentialStore.test.ts @@ -6,28 +6,12 @@ import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import * as Option from "effect/Option"; -import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; -import { makeCursorCredentialStore } from "./CursorCredentialStore.ts"; +import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost"; +import { layerTestProviderHost } from "@t3tools/provider-testing/host"; +import * as Layer from "effect/Layer"; +import { makeCursorCredentialStore } from "./credentialStore.ts"; -const makeSecrets = () => { - const data = new Map(); - return { - data, - secrets: ServerSecretStore.ServerSecretStore.of({ - get: (key) => Effect.sync(() => Option.fromUndefinedOr(data.get(key))), - set: (key, bytes) => - Effect.sync(() => { - data.set(key, bytes); - }), - remove: (key) => - Effect.sync(() => { - data.delete(key); - }), - create: () => Effect.die("unused"), - getOrCreateRandom: () => Effect.die("unused"), - }), - }; -}; +const layerHost = Layer.provideMerge(layerTestProviderHost(), NodeServices.layer); const legacyCredentials = { version: 1 as const, @@ -74,44 +58,23 @@ it.effect.each([ const path = yield* Path.Path; const legacyFile = path.join(yield* fileSystem.makeTempDirectoryScoped(), "cursor.json"); yield* fileSystem.writeFileString(legacyFile, legacy); - const { secrets } = makeSecrets(); + const host = yield* ProviderHost.ProviderHost; const instanceId = ProviderInstanceId.make("personal"); if (stored !== undefined) { - const current = yield* makeCursorCredentialStore(instanceId).pipe( - Effect.provideService(ServerSecretStore.ServerSecretStore, secrets), - ); - yield* secrets.set(current.binding.key, new TextEncoder().encode(stored)); + yield* (yield* host.credentials("cursor", instanceId)).set(new TextEncoder().encode(stored)); } - const migrated = yield* makeCursorCredentialStore(instanceId, legacyFile).pipe( - Effect.provideService(ServerSecretStore.ServerSecretStore, secrets), - ); + const migrated = yield* makeCursorCredentialStore(instanceId, legacyFile); assert.strictEqual((yield* Effect.tryPromise(() => migrated.store.load()))?.apiKey, expected); assert.isFalse(yield* fileSystem.exists(legacyFile)); - }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), + }).pipe(Effect.scoped, Effect.provide(layerHost)), ); it.effect( "restores SDK credentials in a new controller and keeps another account when signing out", () => Effect.gen(function* () { - const data = new Map(); - const secrets = ServerSecretStore.ServerSecretStore.of({ - get: (key) => Effect.sync(() => Option.fromUndefinedOr(data.get(key))), - set: (key, bytes) => - Effect.sync(() => { - data.set(key, bytes); - }), - remove: (key) => - Effect.sync(() => { - data.delete(key); - }), - create: () => Effect.die("unused"), - getOrCreateRandom: () => Effect.die("unused"), - }); - const makeStore = (id: string) => - makeCursorCredentialStore(ProviderInstanceId.make(id)).pipe( - Effect.provideService(ServerSecretStore.ServerSecretStore, secrets), - ); + const host = yield* ProviderHost.ProviderHost; + const makeStore = (id: string) => makeCursorCredentialStore(ProviderInstanceId.make(id)); const personal = yield* makeStore("personal"); const work = yield* makeStore("work"); const credentials = { @@ -128,7 +91,9 @@ it.effect( ); const restored = yield* makeStore("personal"); assert.deepEqual(yield* Effect.tryPromise(() => restored.store.load()), credentials); - data.set(personal.binding.key, new TextEncoder().encode("damaged credential")); + yield* (yield* host.credentials("cursor", "personal")).set( + new TextEncoder().encode("damaged credential"), + ); assert.isUndefined(yield* Effect.tryPromise(() => restored.store.load())); yield* Effect.tryPromise(() => restored.store.save(credentials)); yield* Effect.tryPromise(() => restored.store.clear()); @@ -137,5 +102,5 @@ it.effect( (yield* Effect.tryPromise(() => work.store.load()))?.apiKey, "test-only-work-key", ); - }).pipe(Effect.provide(NodeServices.layer)), + }).pipe(Effect.provide(layerHost)), ); diff --git a/apps/server/src/provider/CursorCredentialStore.ts b/packages/provider-cursor/src/server/credentialStore.ts similarity index 94% rename from apps/server/src/provider/CursorCredentialStore.ts rename to packages/provider-cursor/src/server/credentialStore.ts index 56c3014deedb..70277a54e216 100644 --- a/apps/server/src/provider/CursorCredentialStore.ts +++ b/packages/provider-cursor/src/server/credentialStore.ts @@ -5,7 +5,7 @@ import * as FileSystem from "effect/FileSystem"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; -import * as ProviderCredentialStore from "./ProviderCredentialStore.ts"; +import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost"; const Credentials = Schema.fromJsonString( Schema.Struct({ @@ -30,7 +30,7 @@ export const makeCursorCredentialStore = Effect.fn("makeCursorCredentialStore")( instanceId: ProviderInstanceId, legacyFile?: string, ) { - const credentials = yield* ProviderCredentialStore.make("cursor", instanceId); + const credentials = yield* (yield* ProviderHost.ProviderHost).credentials("cursor", instanceId); if (legacyFile !== undefined) { const fileSystem = yield* FileSystem.FileSystem; yield* Effect.gen(function* () { diff --git a/apps/server/src/provider/Drivers/CursorDriver.test.ts b/packages/provider-cursor/src/server/driver.test.ts similarity index 84% rename from apps/server/src/provider/Drivers/CursorDriver.test.ts rename to packages/provider-cursor/src/server/driver.test.ts index 8aae15d1c575..b7320ce8fa98 100644 --- a/apps/server/src/provider/Drivers/CursorDriver.test.ts +++ b/packages/provider-cursor/src/server/driver.test.ts @@ -1,5 +1,4 @@ // @effect-diagnostics nodeBuiltinImport:off -import * as ServerSecretStore from "../../auth/ServerSecretStore.ts"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { expect, it } from "@effect/vitest"; import { ProviderInstanceId, ProviderSessionId, ThreadId } from "@t3tools/contracts"; @@ -11,23 +10,15 @@ import { vi } from "vite-plus/test"; import { HttpClient } from "effect/http"; import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; -import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts"; -import * as ServerConfig from "../../config.ts"; -import * as ServerSettings from "../../serverSettings.ts"; -import * as ProviderEventLoggers from "../ProviderEventLoggers.ts"; -import { CursorDriver } from "./CursorDriver.ts"; -import * as CursorAgentSdk from "../../orchestration-v2/Adapters/CursorAgentSdk.ts"; +import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers"; +import { layerTestProviderHost } from "@t3tools/provider-testing/host"; +import { CursorDriver } from "./driver.ts"; +import * as CursorAgentSdk from "./CursorAgentSdk.ts"; import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator"; -import { ProviderAdapterV2RuntimePolicy } from "@t3tools/provider-core/server/ProviderAdapter"; -import { Cursor } from "../cursorSdk.ts"; -import * as ProviderHostLive from "../ProviderHostLive.ts"; +import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter"; +import { Cursor } from "./sdk.ts"; -const layerDeps = ServerSecretStore.layer.pipe( - Layer.provideMerge( - ServerConfig.layerTest(process.cwd(), { - prefix: "t3-cursor-driver-copy-command-", - }), - ), +const layerTest = layerTestProviderHost({ runBackgroundWork: false }).pipe( Layer.provideMerge(NodeServices.layer), Layer.provideMerge(IdAllocator.layer), Layer.provideMerge( @@ -35,12 +26,6 @@ const layerDeps = ServerSecretStore.layer.pipe( open: () => Effect.die("Maintenance resolution must not open a Cursor session"), }), ), - Layer.provideMerge(ServerSettings.layerTest()), - Layer.provideMerge( - Layer.mock(BackgroundPolicy.BackgroundPolicy)({ - shouldRunScopeWork: () => Effect.succeed(false), - }), - ), Layer.provideMerge( Layer.succeed( ProviderEventLoggers.ProviderEventLoggers, @@ -54,7 +39,6 @@ const layerDeps = ServerSecretStore.layer.pipe( ), ), ); -const layerTest = ProviderHostLive.layer.pipe(Layer.provideMerge(layerDeps)); it.layer(layerTest)("CursorDriver", (it) => { it.effect( @@ -134,7 +118,7 @@ it.layer(layerTest)("CursorDriver", (it) => { }); const threadId = ThreadId.make("cursor-browser-thread"); const modelSelection = { instanceId: input.instanceId, model: "auto" }; - const runtimePolicy = ProviderAdapterV2RuntimePolicy.make({ + const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({ runtimeMode: "full-access", interactionMode: "default", cwd: process.cwd(), diff --git a/apps/server/src/provider/Drivers/CursorDriver.ts b/packages/provider-cursor/src/server/driver.ts similarity index 86% rename from apps/server/src/provider/Drivers/CursorDriver.ts rename to packages/provider-cursor/src/server/driver.ts index cf4d82bcfdb1..157b7dca5c1e 100644 --- a/apps/server/src/provider/Drivers/CursorDriver.ts +++ b/packages/provider-cursor/src/server/driver.ts @@ -6,29 +6,22 @@ * * @module provider/Drivers/CursorDriver */ -import { CursorSettings, ProviderDriverKind, ProviderSetupError } from "@t3tools/contracts"; +import { ProviderDriverKind, ProviderSetupError } from "@t3tools/contracts"; +import { CursorSettings } from "../settings.ts"; import * as Effect from "effect/Effect"; import * as Crypto from "effect/Crypto"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import { HttpClient } from "effect/http"; -import { readCursorUsageLimits } from "../cursorUsageLimits.ts"; +import { readCursorUsageLimits } from "./usageLimits.ts"; -import { ProviderHost } from "@t3tools/provider-core/server/ProviderHost"; -import * as ServerConfig from "../../config.ts"; -import * as ServerSettings from "../../serverSettings.ts"; -import { makeCursorTextGeneration } from "../../textGeneration/CursorTextGeneration.ts"; -import { - CursorAdapterV2Driver, - type CursorAdapterV2DriverEnv, -} from "../../orchestration-v2/Adapters/CursorAdapterV2.ts"; -import { ProviderDriverError } from "../Errors.ts"; -import { - buildInitialCursorProviderSnapshot, - checkCursorProviderStatus, -} from "../CursorProvider.ts"; -import * as CursorSdkCatalog from "../CursorSdkCatalog.ts"; +import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost"; +import { makeCursorTextGeneration } from "./textGeneration.ts"; +import { CursorAdapterV2Driver, type CursorAdapterV2DriverEnv } from "./adapter.ts"; +import { ProviderDriverError } from "@t3tools/provider-core/server/errors"; +import { buildInitialCursorProviderSnapshot, checkCursorProviderStatus } from "./status.ts"; +import * as CursorSdkCatalog from "./CursorSdkCatalog.ts"; import { makeManagedServerProvider } from "@t3tools/provider-core/server/managedProvider"; import { defaultProviderContinuationIdentity, @@ -43,11 +36,10 @@ import { makeProviderSnapshotSettingsSource, type ProviderSnapshotSettings, } from "@t3tools/provider-core/server/snapshotSettings"; -import { probeCursorSkills } from "./CursorSkills.ts"; -import { makeCursorAuth } from "../CursorAuth.ts"; -import * as CursorCredentialStore from "../CursorCredentialStore.ts"; -import * as ServerSecretStore from "../../auth/ServerSecretStore.ts"; -import * as CursorAgentSdk from "../../orchestration-v2/Adapters/CursorAgentSdk.ts"; +import { probeCursorSkills } from "./skills.ts"; +import { makeCursorAuth } from "./auth.ts"; +import * as CursorCredentialStore from "./credentialStore.ts"; +import * as CursorAgentSdk from "./CursorAgentSdk.ts"; const decodeCursorSettings = Schema.decodeSync(CursorSettings); const isSdkRunnerError = Schema.is(CursorAgentSdk.CursorAgentSdkRunnerError); @@ -63,10 +55,7 @@ export type CursorDriverEnv = | FileSystem.FileSystem | Path.Path | HttpClient.HttpClient - | ProviderHost - | ServerConfig.ServerConfig - | ServerSecretStore.ServerSecretStore - | ServerSettings.ServerSettingsService; + | ProviderHost.ProviderHost; export const CursorDriver: ProviderDriver = { driverKind: DRIVER_KIND, @@ -78,8 +67,7 @@ export const CursorDriver: ProviderDriver = { defaultConfig: (): CursorSettings => decodeCursorSettings({}), create: ({ instanceId, displayName, accentColor, environment, enabled, config }) => Effect.gen(function* () { - const serverSettings = yield* ServerSettings.ServerSettingsService; - const host = yield* ProviderHost; + const host = yield* ProviderHost.ProviderHost; const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const httpClient = yield* HttpClient.HttpClient; @@ -100,7 +88,7 @@ export const CursorDriver: ProviderDriver = { const credentials = yield* CursorCredentialStore.makeCursorCredentialStore( instanceId, path.join( - (yield* ServerConfig.ServerConfig).stateDir, + host.paths.stateDir, "provider-auth", encodeURIComponent(instanceId), "cursor.json", @@ -214,7 +202,7 @@ export const CursorDriver: ProviderDriver = { effectiveConfig.enabled && snapshot.installed && snapshot.auth.status === "authenticated" - ? serverSettings.getSettings.pipe( + ? host.settings.get.pipe( Effect.flatMap((settings) => readCursorUsageLimits( effectiveConfig, diff --git a/apps/server/src/provider/cursorKeychainToken.test.ts b/packages/provider-cursor/src/server/keychainToken.test.ts similarity index 91% rename from apps/server/src/provider/cursorKeychainToken.test.ts rename to packages/provider-cursor/src/server/keychainToken.test.ts index 6c2e68d7b427..7f279bb60892 100644 --- a/apps/server/src/provider/cursorKeychainToken.test.ts +++ b/packages/provider-cursor/src/server/keychainToken.test.ts @@ -1,9 +1,6 @@ import { assert, describe, it } from "@effect/vitest"; -import { - CursorKeychainTimeoutError, - makeCachedCursorAccessTokenReader, -} from "./cursorKeychainToken.ts"; +import { CursorKeychainTimeoutError, makeCachedCursorAccessTokenReader } from "./keychainToken.ts"; describe("Cursor Keychain reader", () => { it("shares concurrent reads and rechecks after the cache expires", async () => { diff --git a/apps/server/src/provider/cursorKeychainToken.ts b/packages/provider-cursor/src/server/keychainToken.ts similarity index 100% rename from apps/server/src/provider/cursorKeychainToken.ts rename to packages/provider-cursor/src/server/keychainToken.ts diff --git a/apps/server/src/provider/cursorSdk.test.ts b/packages/provider-cursor/src/server/sdk.test.ts similarity index 97% rename from apps/server/src/provider/cursorSdk.test.ts rename to packages/provider-cursor/src/server/sdk.test.ts index 6a34ee9fc40e..06a9b6229fc5 100644 --- a/apps/server/src/provider/cursorSdk.test.ts +++ b/packages/provider-cursor/src/server/sdk.test.ts @@ -2,9 +2,9 @@ import * as NodeChildProcess from "node:child_process"; import { describe, expect, it } from "vite-plus/test"; -const cursorSdkUrl = new URL("./cursorSdk.ts", import.meta.url).href; +const cursorSdkUrl = new URL("./sdk.ts", import.meta.url).href; -// Loads cursorSdk.ts in a fresh process. @cursor/sdk is stubbed so the guard +// Loads sdk.ts in a fresh process. @cursor/sdk is stubbed so the guard // can be tested without the real package. The vitest worker already has its // own unhandledRejection listener, which would hide the process-exit behavior. const probeProgram = ` diff --git a/apps/server/src/provider/cursorSdk.ts b/packages/provider-cursor/src/server/sdk.ts similarity index 100% rename from apps/server/src/provider/cursorSdk.ts rename to packages/provider-cursor/src/server/sdk.ts diff --git a/apps/server/src/provider/cursorSdkModel.ts b/packages/provider-cursor/src/server/sdkModel.ts similarity index 100% rename from apps/server/src/provider/cursorSdkModel.ts rename to packages/provider-cursor/src/server/sdkModel.ts diff --git a/apps/server/src/provider/Drivers/CursorSkills.test.ts b/packages/provider-cursor/src/server/skills.test.ts similarity index 99% rename from apps/server/src/provider/Drivers/CursorSkills.test.ts rename to packages/provider-cursor/src/server/skills.test.ts index 382f9c6dc54e..7831e5909e0f 100644 --- a/apps/server/src/provider/Drivers/CursorSkills.test.ts +++ b/packages/provider-cursor/src/server/skills.test.ts @@ -12,7 +12,7 @@ import { hasCursorSkillMention, probeCursorSkills, rewriteCursorSkillMentions, -} from "./CursorSkills.ts"; +} from "./skills.ts"; const runNode = ( effect: Effect.Effect, diff --git a/apps/server/src/provider/Drivers/CursorSkills.ts b/packages/provider-cursor/src/server/skills.ts similarity index 100% rename from apps/server/src/provider/Drivers/CursorSkills.ts rename to packages/provider-cursor/src/server/skills.ts diff --git a/apps/server/src/provider/CursorProvider.test.ts b/packages/provider-cursor/src/server/status.test.ts similarity index 97% rename from apps/server/src/provider/CursorProvider.test.ts rename to packages/provider-cursor/src/server/status.test.ts index 7fe35db9ea32..32eedf9e3a9c 100644 --- a/apps/server/src/provider/CursorProvider.test.ts +++ b/packages/provider-cursor/src/server/status.test.ts @@ -2,8 +2,8 @@ import type { SDKModel } from "@cursor/sdk"; import { describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Schema from "effect/Schema"; -import type { CursorSettings } from "@t3tools/contracts"; -import { CursorSettings as CursorSettingsSchema } from "@t3tools/contracts"; +import type { CursorSettings } from "../settings.ts"; +import { CursorSettings as CursorSettingsSchema } from "../settings.ts"; import { createModelCapabilities } from "@t3tools/shared/model"; import { @@ -12,7 +12,7 @@ import { buildCursorProviderSnapshot, buildInitialCursorProviderSnapshot, checkCursorProviderStatus, -} from "./CursorProvider.ts"; +} from "./status.ts"; import * as CursorSdkCatalog from "./CursorSdkCatalog.ts"; const decodeCursorSettings = Schema.decodeSync(CursorSettingsSchema); diff --git a/apps/server/src/provider/CursorProvider.ts b/packages/provider-cursor/src/server/status.ts similarity index 99% rename from apps/server/src/provider/CursorProvider.ts rename to packages/provider-cursor/src/server/status.ts index 473a7bb8c5aa..3b00118688df 100644 --- a/apps/server/src/provider/CursorProvider.ts +++ b/packages/provider-cursor/src/server/status.ts @@ -1,19 +1,19 @@ import type { SDKModel, SDKUser } from "@cursor/sdk"; import type { - CursorSettings, ModelCapabilities, ProviderOptionDescriptor, ServerProviderAuth, ServerProviderModel, ServerProviderState, } from "@t3tools/contracts"; +import type { CursorSettings } from "../settings.ts"; import { createModelCapabilities } from "@t3tools/shared/model"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; import * as Result from "effect/Result"; -import { cursorSdkParameterPriority, cursorSdkProviderOptionId } from "./cursorSdkModel.ts"; +import { cursorSdkParameterPriority, cursorSdkProviderOptionId } from "./sdkModel.ts"; import { buildBooleanOptionDescriptor, buildSelectOptionDescriptor, diff --git a/apps/server/src/textGeneration/CursorTextGeneration.test.ts b/packages/provider-cursor/src/server/textGeneration.test.ts similarity index 98% rename from apps/server/src/textGeneration/CursorTextGeneration.test.ts rename to packages/provider-cursor/src/server/textGeneration.test.ts index 4b3ce0eebbe5..8b007c9dbaf7 100644 --- a/apps/server/src/textGeneration/CursorTextGeneration.test.ts +++ b/packages/provider-cursor/src/server/textGeneration.test.ts @@ -1,5 +1,6 @@ import type { RunResult } from "@cursor/sdk"; -import { CursorSettings, ProviderInstanceId, TextGenerationError } from "@t3tools/contracts"; +import { ProviderInstanceId, TextGenerationError } from "@t3tools/contracts"; +import { CursorSettings } from "../settings.ts"; import { describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -9,7 +10,7 @@ import * as Schema from "effect/Schema"; import { createModelSelection } from "@t3tools/shared/model"; import { beforeEach, vi } from "vite-plus/test"; -import { makeCursorTextGeneration } from "./CursorTextGeneration.ts"; +import { makeCursorTextGeneration } from "./textGeneration.ts"; const cursorSdkMock = vi.hoisted(() => ({ create: vi.fn<(options: unknown) => Promise>(), @@ -24,7 +25,7 @@ const cursorSdkMock = vi.hoisted(() => ({ })), })); -vi.mock("../provider/cursorSdk.ts", () => ({ Agent: { create: cursorSdkMock.create } })); +vi.mock("./sdk.ts", () => ({ Agent: { create: cursorSdkMock.create } })); let hasCustomPolicy = false; const layerFs = FileSystem.layerNoop({ diff --git a/apps/server/src/textGeneration/CursorTextGeneration.ts b/packages/provider-cursor/src/server/textGeneration.ts similarity index 95% rename from apps/server/src/textGeneration/CursorTextGeneration.ts rename to packages/provider-cursor/src/server/textGeneration.ts index fb796438158b..924d620bbc76 100644 --- a/apps/server/src/textGeneration/CursorTextGeneration.ts +++ b/packages/provider-cursor/src/server/textGeneration.ts @@ -2,20 +2,17 @@ import * as NodeOS from "node:os"; import * as FileSystem from "effect/FileSystem"; import type { AgentOptions, RunResult } from "@cursor/sdk"; -import { Agent } from "../provider/cursorSdk.ts"; +import { Agent } from "./sdk.ts"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; -import { - type CursorSettings, - type ProviderSetupError, - TextGenerationError, -} from "@t3tools/contracts"; +import { type ProviderSetupError, TextGenerationError } from "@t3tools/contracts"; +import type { CursorSettings } from "../settings.ts"; import * as TextGenerationOperations from "@t3tools/provider-core/server/textGenerationOperations"; -import { cursorSdkModelSelection } from "../provider/cursorSdkModel.ts"; -import type { CursorAuth } from "../provider/CursorAuth.ts"; +import { cursorSdkModelSelection } from "./sdkModel.ts"; +import type { CursorAuth } from "./auth.ts"; const CURSOR_TIMEOUT_MS = 180_000; diff --git a/apps/server/src/provider/acp/CursorTransportFailure.test.ts b/packages/provider-cursor/src/server/transportFailure.test.ts similarity index 97% rename from apps/server/src/provider/acp/CursorTransportFailure.test.ts rename to packages/provider-cursor/src/server/transportFailure.test.ts index fdde19b934d2..91d4fb9f17e1 100644 --- a/apps/server/src/provider/acp/CursorTransportFailure.test.ts +++ b/packages/provider-cursor/src/server/transportFailure.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vite-plus/test"; -import { CursorTransportFailure } from "./CursorTransportFailure.ts"; +import { CursorTransportFailure } from "./transportFailure.ts"; const diagnostic = "Error: RetriableError: WritableIterable is closed"; diff --git a/apps/server/src/provider/acp/CursorTransportFailure.ts b/packages/provider-cursor/src/server/transportFailure.ts similarity index 100% rename from apps/server/src/provider/acp/CursorTransportFailure.ts rename to packages/provider-cursor/src/server/transportFailure.ts diff --git a/apps/server/src/provider/cursorUsageLimits.test.ts b/packages/provider-cursor/src/server/usageLimits.test.ts similarity index 99% rename from apps/server/src/provider/cursorUsageLimits.test.ts rename to packages/provider-cursor/src/server/usageLimits.test.ts index 5ce2cee7fe8c..634168447d9c 100644 --- a/apps/server/src/provider/cursorUsageLimits.test.ts +++ b/packages/provider-cursor/src/server/usageLimits.test.ts @@ -5,7 +5,7 @@ import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import { HttpClient, HttpClientResponse } from "effect/http"; -import { cursorUsageResponseToLimits, readCursorUsageLimits } from "./cursorUsageLimits.ts"; +import { cursorUsageResponseToLimits, readCursorUsageLimits } from "./usageLimits.ts"; const withNodeServices = (effect: Effect.Effect) => effect.pipe(Effect.provide(NodeServices.layer)); diff --git a/apps/server/src/provider/cursorUsageLimits.ts b/packages/provider-cursor/src/server/usageLimits.ts similarity index 97% rename from apps/server/src/provider/cursorUsageLimits.ts rename to packages/provider-cursor/src/server/usageLimits.ts index 987eeda74850..24c314fe7d7d 100644 --- a/apps/server/src/provider/cursorUsageLimits.ts +++ b/packages/provider-cursor/src/server/usageLimits.ts @@ -1,5 +1,6 @@ import * as NodeOS from "node:os"; -import type { CursorSettings, ServerProviderUsageWindow } from "@t3tools/contracts"; +import type { ServerProviderUsageWindow } from "@t3tools/contracts"; +import type { CursorSettings } from "../settings.ts"; import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { CURSOR_USAGE_WINDOWS } from "@t3tools/shared/usageLimits"; import * as DateTime from "effect/DateTime"; @@ -14,7 +15,7 @@ import { makeUnavailableUsageLimits, makeUsageLimits, } from "@t3tools/provider-core/server/usageLimits"; -import { readMacCursorAccessToken } from "./cursorKeychainToken.ts"; +import { readMacCursorAccessToken } from "./keychainToken.ts"; const CursorCredentials = Schema.Struct({ accessToken: Schema.optional(Schema.String) }); const DEFAULT_CURSOR_API_ENDPOINT = "https://api2.cursor.sh"; diff --git a/packages/provider-cursor/src/settings.ts b/packages/provider-cursor/src/settings.ts new file mode 100644 index 000000000000..d4d5017d4f89 --- /dev/null +++ b/packages/provider-cursor/src/settings.ts @@ -0,0 +1,35 @@ +/** + * Cursor instance settings. Shared by the server driver and the client + * settings form, so it holds only browser-safe schema code. + * + * @module provider-cursor/settings + */ +import { CustomModelSetting, makeProviderSettingsSchema, TrimmedString } from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as Schema from "effect/Schema"; + +export const CursorSettings = makeProviderSettingsSchema( + { + // Off by default like Grok and OpenCode. Users opt in from Settings. + enabled: Schema.Boolean.pipe( + Schema.withDecodingDefault(Effect.succeed(false)), + Schema.annotateKey({ providerSettingsForm: { hidden: true } }), + ), + // Keep V1's CLI configuration when V2 rewrites the shared settings file. + // V2's Cursor SDK does not use these fields. + binaryPath: Schema.optionalKey(TrimmedString).pipe( + Schema.annotateKey({ providerSettingsForm: { hidden: true } }), + ), + apiEndpoint: Schema.optionalKey(TrimmedString).pipe( + Schema.annotateKey({ providerSettingsForm: { hidden: true } }), + ), + customModels: Schema.Array(CustomModelSetting).pipe( + Schema.withDecodingDefault(Effect.succeed([])), + Schema.annotateKey({ providerSettingsForm: { hidden: true } }), + ), + }, + { + order: [], + }, +); +export type CursorSettings = typeof CursorSettings.Type; diff --git a/packages/provider-cursor/src/testing.ts b/packages/provider-cursor/src/testing.ts new file mode 100644 index 000000000000..1357074782f5 --- /dev/null +++ b/packages/provider-cursor/src/testing.ts @@ -0,0 +1,13 @@ +/** + * Cursor internals the server's replay testkit, fixture recorder, and + * orchestration tests use directly. + * + * @module provider-cursor/testing + */ +export { + CURSOR_DEFAULT_INSTANCE_ID, + CURSOR_DRIVER_KIND, + CursorProviderCapabilitiesV2, + cursorSdkModelSelection, + makeCursorAgentOptions, +} from "./server/adapter.ts"; diff --git a/packages/provider-cursor/tsconfig.json b/packages/provider-cursor/tsconfig.json new file mode 100644 index 000000000000..374bac55202d --- /dev/null +++ b/packages/provider-cursor/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["src"] +} diff --git a/packages/provider-testing/src/host.ts b/packages/provider-testing/src/host.ts index 6c9a9e113472..eb1d07af6c86 100644 --- a/packages/provider-testing/src/host.ts +++ b/packages/provider-testing/src/host.ts @@ -10,6 +10,7 @@ import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as Stream from "effect/Stream"; @@ -39,6 +40,7 @@ export const layerTestProviderHost = ( yield* fileSystem.makeDirectory(directory, { recursive: true }).pipe(Effect.orDie); } const settings = options.settings ?? DEFAULT_SERVER_SETTINGS; + const credentials = new Map(); return ProviderHost.ProviderHost.of({ paths: { cwd: options.cwd ?? process.cwd(), baseDir, stateDir, providerStatusCacheDir }, settings: { @@ -49,6 +51,17 @@ export const layerTestProviderHost = ( shouldRunBackgroundWork: () => Effect.succeed(options.runBackgroundWork ?? true), // Tests store attachments flat under the attachments directory by id. resolveAttachmentPath: (attachment) => path.join(attachmentsDir, attachment.id), + // Credentials live in memory for the layer's lifetime. + credentials: (namespace, bindingId) => + Effect.sync(() => { + const key = `${namespace}:${bindingId}`; + return { + binding: { owner: "t3" as const, key }, + get: Effect.sync(() => Option.fromUndefinedOr(credentials.get(key))), + set: (value: Uint8Array) => Effect.sync(() => void credentials.set(key, value)), + remove: Effect.sync(() => void credentials.delete(key)), + }; + }), }); }), ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39a350f843f4..85e33ad5cabe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -356,6 +356,9 @@ importers: '@t3tools/provider-core': specifier: workspace:* version: link:../../packages/provider-core + '@t3tools/provider-cursor': + specifier: workspace:* + version: link:../../packages/provider-cursor '@t3tools/provider-muse': specifier: workspace:* version: link:../../packages/provider-muse @@ -600,6 +603,9 @@ importers: '@t3tools/provider-core': specifier: workspace:* version: link:../../packages/provider-core + '@t3tools/provider-cursor': + specifier: workspace:* + version: link:../../packages/provider-cursor '@t3tools/provider-muse': specifier: workspace:* version: link:../../packages/provider-muse @@ -736,6 +742,9 @@ importers: '@t3tools/provider-core': specifier: workspace:* version: link:../../packages/provider-core + '@t3tools/provider-cursor': + specifier: workspace:* + version: link:../../packages/provider-cursor '@t3tools/provider-muse': specifier: workspace:* version: link:../../packages/provider-muse @@ -1108,6 +1117,46 @@ importers: specifier: 1.0.0 version: 1.0.0(@types/node@24.12.4)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + packages/provider-cursor: + dependencies: + '@cursor/sdk': + specifier: 1.0.35 + version: 1.0.35 + '@napi-rs/keyring': + specifier: ^1.3.0 + version: 1.3.0 + '@t3tools/contracts': + specifier: workspace:* + version: link:../contracts + '@t3tools/provider-core': + specifier: workspace:* + version: link:../provider-core + '@t3tools/shared': + specifier: workspace:* + version: link:../shared + effect: + specifier: 4.0.1 + version: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + yaml: + specifier: ^2.9.0 + version: 2.9.0 + devDependencies: + '@effect/platform-node': + specifier: 4.0.1 + version: 4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(redis@6.2.1)(utf-8-validate@6.0.6) + '@effect/vitest': + specifier: 4.0.1 + version: 4.0.1(patch_hash=359f6fb2f7b3ec145bb72208edb9034f02489791aa2491a55cdbd69bd56ee0d2)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + '@t3tools/provider-testing': + specifier: workspace:* + version: link:../provider-testing + '@types/node': + specifier: 24.12.4 + version: 24.12.4 + vite-plus: + specifier: 1.0.0 + version: 1.0.0(@types/node@24.12.4)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + packages/provider-muse: dependencies: '@muse-code/sdk': diff --git a/scripts/lib/cursor-sdk-packaging.test.ts b/scripts/lib/cursor-sdk-packaging.test.ts index 1eabc6664e1a..efaaa96b4954 100644 --- a/scripts/lib/cursor-sdk-packaging.test.ts +++ b/scripts/lib/cursor-sdk-packaging.test.ts @@ -79,7 +79,7 @@ it("loads packaged Cursor catalog chunks without credentials or checkout depende entry, ` import assert from 'node:assert/strict'; - import { Cursor } from ${JSON.stringify(NodePath.join(repoRoot, "apps/server/src/provider/cursorSdk.ts"))}; + import { Cursor } from ${JSON.stringify(NodePath.join(repoRoot, "packages/provider-cursor/src/server/sdk.ts"))}; for (const [operation, request] of [ ['Cursor.models.list', () => Cursor.models.list({ apiKey: '' })], ['Cursor.me', () => Cursor.me({ apiKey: '' })], diff --git a/scripts/release-smoke.ts b/scripts/release-smoke.ts index 400fc17efab4..6c5a1707854f 100644 --- a/scripts/release-smoke.ts +++ b/scripts/release-smoke.ts @@ -31,6 +31,7 @@ const workspaceFiles = [ "packages/provider-core/package.json", "packages/provider-testing/package.json", "packages/provider-pi/package.json", + "packages/provider-cursor/package.json", "packages/provider-muse/package.json", "packages/provider-opencode/package.json", "packages/effect-acp/package.json",