/** * CursorAdapterLive — Cursor CLI (`agent acp`) via ACP. * * @module CursorAdapterLive */import * as nodePath from "node:path"; import { ApprovalRequestId, type ProviderOptionSelection, EventId, type ProviderApprovalDecision, type ProviderInteractionMode, type ProviderRuntimeEvent, type ProviderSession, type ProviderUserInputAnswers, RuntimeRequestId, type RuntimeMode, type ThreadId, TurnId,} from "@t3tools/contracts";import { DateTime, Deferred, Effect, Exit, Fiber, FileSystem, Layer, Option, PubSub, Random, Scope, Semaphore, Stream, SynchronizedRef,} from "effect";import { ChildProcessSpawner } from "effect/unstable/process";import type * as EffectAcpSchema from "effect-acp/schema"; import { resolveAttachmentPath } from "../../attachmentStore.ts";import { ServerConfig } from "../../config.ts";import { ServerSettingsService } from "../../serverSettings.ts";import { ProviderAdapterProcessError, ProviderAdapterRequestError, ProviderAdapterSessionNotFoundError, ProviderAdapterValidationError,} from "../Errors.ts";import { acpPermissionOutcome, mapAcpToAdapterError } from "../acp/AcpAdapterSupport.ts";import { type AcpSessionRuntimeShape } from "../acp/AcpSessionRuntime.ts";import { makeAcpAssistantItemEvent, makeAcpContentDeltaEvent, makeAcpPlanUpdatedEvent, makeAcpRequestOpenedEvent, makeAcpRequestResolvedEvent, makeAcpToolCallEvent,} from "../acp/AcpCoreRuntimeEvents.ts";import { type AcpSessionMode, type AcpSessionModeState, parsePermissionRequest,} from "../acp/AcpRuntimeModel.ts";import { makeAcpNativeLoggers } from "../acp/AcpNativeLogging.ts";import { applyCursorAcpModelSelection, makeCursorAcpRuntime } from "../acp/CursorAcpSupport.ts";import { CursorAskQuestionRequest, CursorCreatePlanRequest, CursorUpdateTodosRequest, extractAskQuestions, extractPlanMarkdown, extractTodosAsPlan,} from "../acp/CursorAcpExtension.ts";import { CursorAdapter, type CursorAdapterShape } from "../Services/CursorAdapter.ts";import { resolveCursorAcpBaseModelId } from "./CursorProvider.ts";import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; const PROVIDER = "cursor" as const;const CURSOR_RESUME_VERSION = 1 as const;const ACP_PLAN_MODE_ALIASES = ["plan", "architect"];const ACP_IMPLEMENT_MODE_ALIASES = ["code", "agent", "default", "chat", "implement"];const ACP_APPROVAL_MODE_ALIASES = ["ask"]; export interface CursorAdapterLiveOptions { readonly nativeEventLogPath?: string; readonly nativeEventLogger?: EventNdjsonLogger;} interface PendingApproval { readonly decision: Deferred.Deferred<ProviderApprovalDecision>; readonly kind: string | "unknown";} interface PendingUserInput { readonly answers: Deferred.Deferred<ProviderUserInputAnswers>;} interface CursorSessionContext { readonly threadId: ThreadId; session: ProviderSession; readonly scope: Scope.Closeable; readonly acp: AcpSessionRuntimeShape; notificationFiber: Fiber.Fiber<void, never> | undefined; readonly pendingApprovals: Map<ApprovalRequestId, PendingApproval>; readonly pendingUserInputs: Map<ApprovalRequestId, PendingUserInput>; readonly turns: Array<{ id: TurnId; items: Array<unknown> }>; lastPlanFingerprint: string | undefined; activeTurnId: TurnId | undefined; stopped: boolean;} function settlePendingApprovalsAsCancelled( pendingApprovals: ReadonlyMap<ApprovalRequestId, PendingApproval>,): Effect.Effect<void> { const pendingEntries = Array.from(pendingApprovals.values()); return Effect.forEach( pendingEntries, (pending) => Deferred.succeed(pending.decision, "cancel").pipe(Effect.ignore), { discard: true, }, );} function settlePendingUserInputsAsEmptyAnswers( pendingUserInputs: ReadonlyMap<ApprovalRequestId, PendingUserInput>,): Effect.Effect<void> { const pendingEntries = Array.from(pendingUserInputs.values()); return Effect.forEach( pendingEntries, (pending) => Deferred.succeed(pending.answers, {}).pipe(Effect.ignore), { discard: true, }, );} function isRecord(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value);} function parseCursorResume(raw: unknown): { sessionId: string } | undefined { if (!isRecord(raw)) return undefined; if (raw.schemaVersion !== CURSOR_RESUME_VERSION) return undefined; if (typeof raw.sessionId !== "string" || !raw.sessionId.trim()) return undefined; return { sessionId: raw.sessionId.trim() };} function normalizeModeSearchText(mode: AcpSessionMode): string { return [mode.id, mode.name, mode.description] .filter((value): value is string => typeof value === "string" && value.length > 0) .join(" ") .toLowerCase() .replace(/[^a-z0-9]+/g, " ") .trim();} function findModeByAliases( modes: ReadonlyArray<AcpSessionMode>, aliases: ReadonlyArray<string>,): AcpSessionMode | undefined { const normalizedAliases = aliases.map((alias) => alias.toLowerCase()); for (const alias of normalizedAliases) { const exact = modes.find((mode) => { const id = mode.id.toLowerCase(); const name = mode.name.toLowerCase(); return id === alias || name === alias; }); if (exact) { return exact; } } for (const alias of normalizedAliases) { const partial = modes.find((mode) => normalizeModeSearchText(mode).includes(alias)); if (partial) { return partial; } } return undefined;} function isPlanMode(mode: AcpSessionMode): boolean { return findModeByAliases([mode], ACP_PLAN_MODE_ALIASES) !== undefined;} function resolveRequestedModeId(input: { readonly interactionMode: ProviderInteractionMode | undefined; readonly runtimeMode: RuntimeMode; readonly modeState: AcpSessionModeState | undefined;}): string | undefined { const modeState = input.modeState; if (!modeState) { return undefined; } if (input.interactionMode === "plan") { return findModeByAliases(modeState.availableModes, ACP_PLAN_MODE_ALIASES)?.id; } if (input.runtimeMode === "approval-required") { return ( findModeByAliases(modeState.availableModes, ACP_APPROVAL_MODE_ALIASES)?.id ?? findModeByAliases(modeState.availableModes, ACP_IMPLEMENT_MODE_ALIASES)?.id ?? modeState.availableModes.find((mode) => !isPlanMode(mode))?.id ?? modeState.currentModeId ); } return ( findModeByAliases(modeState.availableModes, ACP_IMPLEMENT_MODE_ALIASES)?.id ?? findModeByAliases(modeState.availableModes, ACP_APPROVAL_MODE_ALIASES)?.id ?? modeState.availableModes.find((mode) => !isPlanMode(mode))?.id ?? modeState.currentModeId );} function applyRequestedSessionConfiguration<E>(input: { readonly runtime: AcpSessionRuntimeShape; readonly runtimeMode: RuntimeMode; readonly interactionMode: ProviderInteractionMode | undefined; readonly modelSelection: | { readonly model: string; readonly options?: ReadonlyArray<ProviderOptionSelection> | null | undefined; } | undefined; readonly mapError: (context: { readonly cause: import("effect-acp/errors").AcpError; readonly method: "session/set_config_option" | "session/set_mode"; }) => E;}): Effect.Effect<void, E> { return Effect.gen(function* () { if (input.modelSelection) { yield* applyCursorAcpModelSelection({ runtime: input.runtime, model: input.modelSelection.model, selections: input.modelSelection.options, mapError: ({ cause }) => input.mapError({ cause, method: "session/set_config_option", }), }); } const requestedModeId = resolveRequestedModeId({ interactionMode: input.interactionMode, runtimeMode: input.runtimeMode, modeState: yield* input.runtime.getModeState, }); if (!requestedModeId) { return; } yield* input.runtime.setMode(requestedModeId).pipe( Effect.mapError((cause) => input.mapError({ cause, method: "session/set_mode", }), ), ); });} function selectAutoApprovedPermissionOption( request: EffectAcpSchema.RequestPermissionRequest,): string | undefined { const allowAlwaysOption = request.options.find((option) => option.kind === "allow_always"); if (typeof allowAlwaysOption?.optionId === "string" && allowAlwaysOption.optionId.trim()) { return allowAlwaysOption.optionId.trim(); } const allowOnceOption = request.options.find((option) => option.kind === "allow_once"); if (typeof allowOnceOption?.optionId === "string" && allowOnceOption.optionId.trim()) { return allowOnceOption.optionId.trim(); } return undefined;} function makeCursorAdapter(options?: CursorAdapterLiveOptions) { return Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; const serverConfig = yield* Effect.service(ServerConfig); const serverSettingsService = yield* ServerSettingsService; const nativeEventLogger = options?.nativeEventLogger ?? (options?.nativeEventLogPath !== undefined ? yield* makeEventNdjsonLogger(options.nativeEventLogPath, { stream: "native", }) : undefined); const managedNativeEventLogger = options?.nativeEventLogger === undefined ? nativeEventLogger : undefined; const sessions = new Map<ThreadId, CursorSessionContext>(); const threadLocksRef = yield* SynchronizedRef.make(new Map<string, Semaphore.Semaphore>()); const runtimeEventPubSub = yield* PubSub.unbounded<ProviderRuntimeEvent>(); const nowIso = Effect.map(DateTime.now, DateTime.formatIso); const nextEventId = Effect.map(Random.nextUUIDv4, (id) => EventId.make(id)); const makeEventStamp = () => Effect.all({ eventId: nextEventId, createdAt: nowIso }); const offerRuntimeEvent = (event: ProviderRuntimeEvent) => PubSub.publish(runtimeEventPubSub, event).pipe(Effect.asVoid); const getThreadSemaphore = (threadId: string) => SynchronizedRef.modifyEffect(threadLocksRef, (current) => { const existing: Option.Option<Semaphore.Semaphore> = Option.fromNullishOr( current.get(threadId), ); return Option.match(existing, { onNone: () => Semaphore.make(1).pipe( Effect.map((semaphore) => { const next = new Map(current); next.set(threadId, semaphore); return [semaphore, next] as const; }), ), onSome: (semaphore) => Effect.succeed([semaphore, current] as const), }); }); const withThreadLock = <A, E, R>(threadId: string, effect: Effect.Effect<A, E, R>) => Effect.flatMap(getThreadSemaphore(threadId), (semaphore) => semaphore.withPermit(effect)); const logNative = ( threadId: ThreadId, method: string, payload: unknown, _source: "acp.jsonrpc" | "acp.cursor.extension", ) => Effect.gen(function* () { if (!nativeEventLogger) return; const observedAt = new Date().toISOString(); yield* nativeEventLogger.write( { observedAt, event: { id: crypto.randomUUID(), kind: "notification", provider: PROVIDER, createdAt: observedAt, method, threadId, payload, }, }, threadId, ); }); const emitPlanUpdate = ( ctx: CursorSessionContext, payload: { readonly explanation?: string | null; readonly plan: ReadonlyArray<{ readonly step: string; readonly status: "pending" | "inProgress" | "completed"; }>; }, rawPayload: unknown, source: "acp.jsonrpc" | "acp.cursor.extension", method: string, ) => Effect.gen(function* () { const fingerprint = `${ctx.activeTurnId ?? "no-turn"}:${JSON.stringify(payload)}`; if (ctx.lastPlanFingerprint === fingerprint) { return; } ctx.lastPlanFingerprint = fingerprint; yield* offerRuntimeEvent( makeAcpPlanUpdatedEvent({ stamp: yield* makeEventStamp(), provider: PROVIDER, threadId: ctx.threadId, turnId: ctx.activeTurnId, payload, source, method, rawPayload, }), ); }); const requireSession = ( threadId: ThreadId, ): Effect.Effect<CursorSessionContext, ProviderAdapterSessionNotFoundError> => { const ctx = sessions.get(threadId); if (!ctx || ctx.stopped) { return Effect.fail( new ProviderAdapterSessionNotFoundError({ provider: PROVIDER, threadId }), ); } return Effect.succeed(ctx); }; const stopSessionInternal = (ctx: CursorSessionContext) => Effect.gen(function* () { if (ctx.stopped) return; ctx.stopped = true; yield* settlePendingApprovalsAsCancelled(ctx.pendingApprovals); yield* settlePendingUserInputsAsEmptyAnswers(ctx.pendingUserInputs); if (ctx.notificationFiber) { yield* Fiber.interrupt(ctx.notificationFiber); } yield* Effect.ignore(Scope.close(ctx.scope, Exit.void)); sessions.delete(ctx.threadId); yield* offerRuntimeEvent({ type: "session.exited", ...(yield* makeEventStamp()), provider: PROVIDER, threadId: ctx.threadId, payload: { exitKind: "graceful" }, }); }); const startSession: CursorAdapterShape["startSession"] = (input) => withThreadLock( input.threadId, Effect.gen(function* () { if (input.provider !== undefined && input.provider !== PROVIDER) { return yield* new ProviderAdapterValidationError({ provider: PROVIDER, operation: "startSession", issue: `Expected provider '${PROVIDER}' but received '${input.provider}'.`, }); } if (!input.cwd?.trim()) { return yield* new ProviderAdapterValidationError({ provider: PROVIDER, operation: "startSession", issue: "cwd is required and must be non-empty.", }); } const cwd = nodePath.resolve(input.cwd.trim()); const cursorModelSelection = input.modelSelection?.provider === "cursor" ? input.modelSelection : undefined; const existing = sessions.get(input.threadId); if (existing && !existing.stopped) { yield* stopSessionInternal(existing); } const cursorSettings = yield* serverSettingsService.getSettings.pipe( Effect.map((settings) => settings.providers.cursor), Effect.mapError( (error) => new ProviderAdapterProcessError({ provider: PROVIDER, threadId: input.threadId, detail: error.message, cause: error, }), ), ); const pendingApprovals = new Map<ApprovalRequestId, PendingApproval>(); const pendingUserInputs = new Map<ApprovalRequestId, PendingUserInput>(); const sessionScope = yield* Scope.make("sequential"); let sessionScopeTransferred = false; yield* Effect.addFinalizer(() => sessionScopeTransferred ? Effect.void : Scope.close(sessionScope, Exit.void), ); let ctx!: CursorSessionContext; const resumeSessionId = parseCursorResume(input.resumeCursor)?.sessionId; const acpNativeLoggers = makeAcpNativeLoggers({ nativeEventLogger, provider: PROVIDER, threadId: input.threadId, }); const acp = yield* makeCursorAcpRuntime({ cursorSettings, childProcessSpawner, cwd, ...(resumeSessionId ? { resumeSessionId } : {}), clientInfo: { name: "t3-code", version: "0.0.0" }, ...acpNativeLoggers, }).pipe( Effect.provideService(Scope.Scope, sessionScope), Effect.mapError( (cause) => new ProviderAdapterProcessError({ provider: PROVIDER, threadId: input.threadId, detail: cause.message, cause, }), ), ); const started = yield* Effect.gen(function* () { yield* acp.handleExtRequest("cursor/ask_question", CursorAskQuestionRequest, (params) => Effect.gen(function* () { yield* logNative(Lines 1–500 of 1057. Load more
/** * CursorAdapterLive — Cursor CLI (`agent acp`) via ACP. * * @module CursorAdapterLive */ import * as nodePath from "node:path"; import { ApprovalRequestId, type ProviderOptionSelection, EventId, type ProviderApprovalDecision, type ProviderInteractionMode, type ProviderRuntimeEvent, type ProviderSession, type ProviderUserInputAnswers, RuntimeRequestId, type RuntimeMode, type ThreadId, TurnId, } from "@t3tools/contracts"; import { DateTime, Deferred, Effect, Exit, Fiber, FileSystem, Layer, Option, PubSub, Random, Scope, Semaphore, Stream, SynchronizedRef, } from "effect"; import { ChildProcessSpawner } from "effect/unstable/process"; import type * as EffectAcpSchema from "effect-acp/schema"; import { resolveAttachmentPath } from "../../attachmentStore.ts"; import { ServerConfig } from "../../config.ts"; import { ServerSettingsService } from "../../serverSettings.ts"; import { ProviderAdapterProcessError, ProviderAdapterRequestError, ProviderAdapterSessionNotFoundError, ProviderAdapterValidationError, } from "../Errors.ts"; import { acpPermissionOutcome, mapAcpToAdapterError } from "../acp/AcpAdapterSupport.ts"; import { type AcpSessionRuntimeShape } from "../acp/AcpSessionRuntime.ts"; import { makeAcpAssistantItemEvent, makeAcpContentDeltaEvent, makeAcpPlanUpdatedEvent, makeAcpRequestOpenedEvent, makeAcpRequestResolvedEvent, makeAcpToolCallEvent, } from "../acp/AcpCoreRuntimeEvents.ts"; import { type AcpSessionMode, type AcpSessionModeState, parsePermissionRequest, } from "../acp/AcpRuntimeModel.ts"; import { makeAcpNativeLoggers } from "../acp/AcpNativeLogging.ts"; import { applyCursorAcpModelSelection, makeCursorAcpRuntime } from "../acp/CursorAcpSupport.ts"; import { CursorAskQuestionRequest, CursorCreatePlanRequest, CursorUpdateTodosRequest, extractAskQuestions, extractPlanMarkdown, extractTodosAsPlan, } from "../acp/CursorAcpExtension.ts"; import { CursorAdapter, type CursorAdapterShape } from "../Services/CursorAdapter.ts"; import { resolveCursorAcpBaseModelId } from "./CursorProvider.ts"; import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; const PROVIDER = "cursor" as const; const CURSOR_RESUME_VERSION = 1 as const; const ACP_PLAN_MODE_ALIASES = ["plan", "architect"]; const ACP_IMPLEMENT_MODE_ALIASES = ["code", "agent", "default", "chat", "implement"]; const ACP_APPROVAL_MODE_ALIASES = ["ask"]; export interface CursorAdapterLiveOptions { readonly nativeEventLogPath?: string; readonly nativeEventLogger?: EventNdjsonLogger; } interface PendingApproval { readonly decision: Deferred.Deferred<ProviderApprovalDecision>; readonly kind: string | "unknown"; } interface PendingUserInput { readonly answers: Deferred.Deferred<ProviderUserInputAnswers>; } interface CursorSessionContext { readonly threadId: ThreadId; session: ProviderSession; readonly scope: Scope.Closeable; readonly acp: AcpSessionRuntimeShape; notificationFiber: Fiber.Fiber<void, never> | undefined; readonly pendingApprovals: Map<ApprovalRequestId, PendingApproval>; readonly pendingUserInputs: Map<ApprovalRequestId, PendingUserInput>; readonly turns: Array<{ id: TurnId; items: Array<unknown> }>; lastPlanFingerprint: string | undefined; activeTurnId: TurnId | undefined; stopped: boolean; } function settlePendingApprovalsAsCancelled( pendingApprovals: ReadonlyMap<ApprovalRequestId, PendingApproval>, ): Effect.Effect<void> { const pendingEntries = Array.from(pendingApprovals.values()); return Effect.forEach( pendingEntries, (pending) => Deferred.succeed(pending.decision, "cancel").pipe(Effect.ignore), { discard: true, }, ); } function settlePendingUserInputsAsEmptyAnswers( pendingUserInputs: ReadonlyMap<ApprovalRequestId, PendingUserInput>, ): Effect.Effect<void> { const pendingEntries = Array.from(pendingUserInputs.values()); return Effect.forEach( pendingEntries, (pending) => Deferred.succeed(pending.answers, {}).pipe(Effect.ignore), { discard: true, }, ); } function isRecord(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value); } function parseCursorResume(raw: unknown): { sessionId: string } | undefined { if (!isRecord(raw)) return undefined; if (raw.schemaVersion !== CURSOR_RESUME_VERSION) return undefined; if (typeof raw.sessionId !== "string" || !raw.sessionId.trim()) return undefined; return { sessionId: raw.sessionId.trim() }; } function normalizeModeSearchText(mode: AcpSessionMode): string { return [mode.id, mode.name, mode.description] .filter((value): value is string => typeof value === "string" && value.length > 0) .join(" ") .toLowerCase() .replace(/[^a-z0-9]+/g, " ") .trim(); } function findModeByAliases( modes: ReadonlyArray<AcpSessionMode>, aliases: ReadonlyArray<string>, ): AcpSessionMode | undefined { const normalizedAliases = aliases.map((alias) => alias.toLowerCase()); for (const alias of normalizedAliases) { const exact = modes.find((mode) => { const id = mode.id.toLowerCase(); const name = mode.name.toLowerCase(); return id === alias || name === alias; }); if (exact) { return exact; } } for (const alias of normalizedAliases) { const partial = modes.find((mode) => normalizeModeSearchText(mode).includes(alias)); if (partial) { return partial; } } return undefined; } function isPlanMode(mode: AcpSessionMode): boolean { return findModeByAliases([mode], ACP_PLAN_MODE_ALIASES) !== undefined; } function resolveRequestedModeId(input: { readonly interactionMode: ProviderInteractionMode | undefined; readonly runtimeMode: RuntimeMode; readonly modeState: AcpSessionModeState | undefined; }): string | undefined { const modeState = input.modeState; if (!modeState) { return undefined; } if (input.interactionMode === "plan") { return findModeByAliases(modeState.availableModes, ACP_PLAN_MODE_ALIASES)?.id; } if (input.runtimeMode === "approval-required") { return ( findModeByAliases(modeState.availableModes, ACP_APPROVAL_MODE_ALIASES)?.id ?? findModeByAliases(modeState.availableModes, ACP_IMPLEMENT_MODE_ALIASES)?.id ?? modeState.availableModes.find((mode) => !isPlanMode(mode))?.id ?? modeState.currentModeId ); } return ( findModeByAliases(modeState.availableModes, ACP_IMPLEMENT_MODE_ALIASES)?.id ?? findModeByAliases(modeState.availableModes, ACP_APPROVAL_MODE_ALIASES)?.id ?? modeState.availableModes.find((mode) => !isPlanMode(mode))?.id ?? modeState.currentModeId ); } function applyRequestedSessionConfiguration<E>(input: { readonly runtime: AcpSessionRuntimeShape; readonly runtimeMode: RuntimeMode; readonly interactionMode: ProviderInteractionMode | undefined; readonly modelSelection: | { readonly model: string; readonly options?: ReadonlyArray<ProviderOptionSelection> | null | undefined; } | undefined; readonly mapError: (context: { readonly cause: import("effect-acp/errors").AcpError; readonly method: "session/set_config_option" | "session/set_mode"; }) => E; }): Effect.Effect<void, E> { return Effect.gen(function* () { if (input.modelSelection) { yield* applyCursorAcpModelSelection({ runtime: input.runtime, model: input.modelSelection.model, selections: input.modelSelection.options, mapError: ({ cause }) => input.mapError({ cause, method: "session/set_config_option", }), }); } const requestedModeId = resolveRequestedModeId({ interactionMode: input.interactionMode, runtimeMode: input.runtimeMode, modeState: yield* input.runtime.getModeState, }); if (!requestedModeId) { return; } yield* input.runtime.setMode(requestedModeId).pipe( Effect.mapError((cause) => input.mapError({ cause, method: "session/set_mode", }), ), ); }); } function selectAutoApprovedPermissionOption( request: EffectAcpSchema.RequestPermissionRequest, ): string | undefined { const allowAlwaysOption = request.options.find((option) => option.kind === "allow_always"); if (typeof allowAlwaysOption?.optionId === "string" && allowAlwaysOption.optionId.trim()) { return allowAlwaysOption.optionId.trim(); } const allowOnceOption = request.options.find((option) => option.kind === "allow_once"); if (typeof allowOnceOption?.optionId === "string" && allowOnceOption.optionId.trim()) { return allowOnceOption.optionId.trim(); } return undefined; } function makeCursorAdapter(options?: CursorAdapterLiveOptions) { return Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; const serverConfig = yield* Effect.service(ServerConfig); const serverSettingsService = yield* ServerSettingsService; const nativeEventLogger = options?.nativeEventLogger ?? (options?.nativeEventLogPath !== undefined ? yield* makeEventNdjsonLogger(options.nativeEventLogPath, { stream: "native", }) : undefined); const managedNativeEventLogger = options?.nativeEventLogger === undefined ? nativeEventLogger : undefined; const sessions = new Map<ThreadId, CursorSessionContext>(); const threadLocksRef = yield* SynchronizedRef.make(new Map<string, Semaphore.Semaphore>()); const runtimeEventPubSub = yield* PubSub.unbounded<ProviderRuntimeEvent>(); const nowIso = Effect.map(DateTime.now, DateTime.formatIso); const nextEventId = Effect.map(Random.nextUUIDv4, (id) => EventId.make(id)); const makeEventStamp = () => Effect.all({ eventId: nextEventId, createdAt: nowIso }); const offerRuntimeEvent = (event: ProviderRuntimeEvent) => PubSub.publish(runtimeEventPubSub, event).pipe(Effect.asVoid); const getThreadSemaphore = (threadId: string) => SynchronizedRef.modifyEffect(threadLocksRef, (current) => { const existing: Option.Option<Semaphore.Semaphore> = Option.fromNullishOr( current.get(threadId), ); return Option.match(existing, { onNone: () => Semaphore.make(1).pipe( Effect.map((semaphore) => { const next = new Map(current); next.set(threadId, semaphore); return [semaphore, next] as const; }), ), onSome: (semaphore) => Effect.succeed([semaphore, current] as const), }); }); const withThreadLock = <A, E, R>(threadId: string, effect: Effect.Effect<A, E, R>) => Effect.flatMap(getThreadSemaphore(threadId), (semaphore) => semaphore.withPermit(effect)); const logNative = ( threadId: ThreadId, method: string, payload: unknown, _source: "acp.jsonrpc" | "acp.cursor.extension", ) => Effect.gen(function* () { if (!nativeEventLogger) return; const observedAt = new Date().toISOString(); yield* nativeEventLogger.write( { observedAt, event: { id: crypto.randomUUID(), kind: "notification", provider: PROVIDER, createdAt: observedAt, method, threadId, payload, }, }, threadId, ); }); const emitPlanUpdate = ( ctx: CursorSessionContext, payload: { readonly explanation?: string | null; readonly plan: ReadonlyArray<{ readonly step: string; readonly status: "pending" | "inProgress" | "completed"; }>; }, rawPayload: unknown, source: "acp.jsonrpc" | "acp.cursor.extension", method: string, ) => Effect.gen(function* () { const fingerprint = `${ctx.activeTurnId ?? "no-turn"}:${JSON.stringify(payload)}`; if (ctx.lastPlanFingerprint === fingerprint) { return; } ctx.lastPlanFingerprint = fingerprint; yield* offerRuntimeEvent( makeAcpPlanUpdatedEvent({ stamp: yield* makeEventStamp(), provider: PROVIDER, threadId: ctx.threadId, turnId: ctx.activeTurnId, payload, source, method, rawPayload, }), ); }); const requireSession = ( threadId: ThreadId, ): Effect.Effect<CursorSessionContext, ProviderAdapterSessionNotFoundError> => { const ctx = sessions.get(threadId); if (!ctx || ctx.stopped) { return Effect.fail( new ProviderAdapterSessionNotFoundError({ provider: PROVIDER, threadId }), ); } return Effect.succeed(ctx); }; const stopSessionInternal = (ctx: CursorSessionContext) => Effect.gen(function* () { if (ctx.stopped) return; ctx.stopped = true; yield* settlePendingApprovalsAsCancelled(ctx.pendingApprovals); yield* settlePendingUserInputsAsEmptyAnswers(ctx.pendingUserInputs); if (ctx.notificationFiber) { yield* Fiber.interrupt(ctx.notificationFiber); } yield* Effect.ignore(Scope.close(ctx.scope, Exit.void)); sessions.delete(ctx.threadId); yield* offerRuntimeEvent({ type: "session.exited", ...(yield* makeEventStamp()), provider: PROVIDER, threadId: ctx.threadId, payload: { exitKind: "graceful" }, }); }); const startSession: CursorAdapterShape["startSession"] = (input) => withThreadLock( input.threadId, Effect.gen(function* () { if (input.provider !== undefined && input.provider !== PROVIDER) { return yield* new ProviderAdapterValidationError({ provider: PROVIDER, operation: "startSession", issue: `Expected provider '${PROVIDER}' but received '${input.provider}'.`, }); } if (!input.cwd?.trim()) { return yield* new ProviderAdapterValidationError({ provider: PROVIDER, operation: "startSession", issue: "cwd is required and must be non-empty.", }); } const cwd = nodePath.resolve(input.cwd.trim()); const cursorModelSelection = input.modelSelection?.provider === "cursor" ? input.modelSelection : undefined; const existing = sessions.get(input.threadId); if (existing && !existing.stopped) { yield* stopSessionInternal(existing); } const cursorSettings = yield* serverSettingsService.getSettings.pipe( Effect.map((settings) => settings.providers.cursor), Effect.mapError( (error) => new ProviderAdapterProcessError({ provider: PROVIDER, threadId: input.threadId, detail: error.message, cause: error, }), ), ); const pendingApprovals = new Map<ApprovalRequestId, PendingApproval>(); const pendingUserInputs = new Map<ApprovalRequestId, PendingUserInput>(); const sessionScope = yield* Scope.make("sequential"); let sessionScopeTransferred = false; yield* Effect.addFinalizer(() => sessionScopeTransferred ? Effect.void : Scope.close(sessionScope, Exit.void), ); let ctx!: CursorSessionContext; const resumeSessionId = parseCursorResume(input.resumeCursor)?.sessionId; const acpNativeLoggers = makeAcpNativeLoggers({ nativeEventLogger, provider: PROVIDER, threadId: input.threadId, }); const acp = yield* makeCursorAcpRuntime({ cursorSettings, childProcessSpawner, cwd, ...(resumeSessionId ? { resumeSessionId } : {}), clientInfo: { name: "t3-code", version: "0.0.0" }, ...acpNativeLoggers, }).pipe( Effect.provideService(Scope.Scope, sessionScope), Effect.mapError( (cause) => new ProviderAdapterProcessError({ provider: PROVIDER, threadId: input.threadId, detail: cause.message, cause, }), ), ); const started = yield* Effect.gen(function* () { yield* acp.handleExtRequest("cursor/ask_question", CursorAskQuestionRequest, (params) => Effect.gen(function* () { yield* logNative(