1057 lines · 36.5 KB · TypeScript
RawDownload
/** * 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.