Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 28 additions & 31 deletions apps/server/src/provider/Layers/CursorProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ import { ProviderDriverKind } from "@t3tools/contracts";
import type * as EffectAcpSchema from "effect-acp/schema";
import * as Cause from "effect/Cause";
import * as DateTime from "effect/DateTime";
import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
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 Result from "effect/Result";
import * as Schema from "effect/Schema";
import { HttpClient } from "effect/unstable/http";
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
import {
Expand Down Expand Up @@ -53,8 +55,8 @@ const EMPTY_CAPABILITIES: ModelCapabilities = createModelCapabilities({
optionDescriptors: [],
});

const CURSOR_ACP_MODEL_DISCOVERY_TIMEOUT_MS = 15_000;
const CURSOR_ACP_MODEL_CAPABILITY_TIMEOUT = "4 seconds";
const CURSOR_ACP_MODEL_DISCOVERY_TIMEOUT = Duration.seconds(15);
const CURSOR_ACP_MODEL_CAPABILITY_TIMEOUT = Duration.seconds(4);
const CURSOR_ACP_MODEL_DISCOVERY_CONCURRENCY = 4;
const CURSOR_PARAMETERIZED_MODEL_PICKER_MIN_VERSION_DATE = 2026_04_08;
export const CURSOR_PARAMETERIZED_MODEL_PICKER_CAPABILITIES = {
Expand Down Expand Up @@ -756,11 +758,22 @@ export function buildCursorProviderSnapshot(input: {
});
}

interface CursorAboutJsonPayload {
readonly cliVersion?: unknown;
readonly subscriptionTier?: unknown;
readonly userEmail?: unknown;
}
const CursorCliConfigJson = Schema.Struct({
channel: Schema.optional(Schema.String),
});
const decodeCursorCliConfigJson = Schema.decodeUnknownOption(
Schema.fromJsonString(CursorCliConfigJson),
);

const CursorAboutJsonPayload = Schema.Struct({
cliVersion: Schema.optional(Schema.Unknown),
subscriptionTier: Schema.optional(Schema.Unknown),
userEmail: Schema.optional(Schema.Unknown),
});
type CursorAboutJsonPayload = typeof CursorAboutJsonPayload.Type;
const decodeCursorAboutJsonPayload = Schema.decodeUnknownOption(
Schema.fromJsonString(CursorAboutJsonPayload),
);

export function parseCursorVersionDate(version: string | null | undefined): number | undefined {
const match = version?.trim().match(/^(\d{4})\.(\d{2})\.(\d{2})(?:\b|-|$)/);
Expand All @@ -772,21 +785,13 @@ export function parseCursorVersionDate(version: string | null | undefined): numb
}

export function parseCursorCliConfigChannel(raw: string): string | undefined {
try {
const parsed = JSON.parse(raw) as unknown;
if (
typeof parsed === "object" &&
parsed !== null &&
"channel" in parsed &&
typeof parsed.channel === "string"
) {
const channel = parsed.channel.trim().toLowerCase();
return channel.length > 0 ? channel : undefined;
}
} catch {
const parsed = decodeCursorCliConfigJson(raw);
if (Option.isNone(parsed)) {
return undefined;
}
return undefined;

const channel = parsed.value.channel?.trim().toLowerCase();
return channel && channel.length > 0 ? channel : undefined;
}

function toTitleCaseWords(value: string): string {
Expand Down Expand Up @@ -835,15 +840,7 @@ function parseCursorAboutJsonPayload(raw: string): CursorAboutJsonPayload | unde
if (!trimmed.startsWith("{")) {
return undefined;
}
try {
const parsed = JSON.parse(trimmed) as unknown;
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
return undefined;
}
return parsed as CursorAboutJsonPayload;
} catch {
return undefined;
}
return Option.getOrUndefined(decodeCursorAboutJsonPayload(trimmed));
}

function hasOwn(record: object, key: string): boolean {
Expand Down Expand Up @@ -1184,7 +1181,7 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")(
if (parsed.auth.status !== "unauthenticated") {
const discoveryExit = yield* Effect.exit(
discoverCursorModelsViaAcp(cursorSettings, environment).pipe(
Effect.timeoutOption(CURSOR_ACP_MODEL_DISCOVERY_TIMEOUT_MS),
Effect.timeoutOption(CURSOR_ACP_MODEL_DISCOVERY_TIMEOUT),
),
);
if (Exit.isFailure(discoveryExit)) {
Expand All @@ -1193,7 +1190,7 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")(
});
discoveryWarning = "Cursor ACP model discovery failed. Check server logs for details.";
} else if (Option.isNone(discoveryExit.value)) {
discoveryWarning = `Cursor ACP model discovery timed out after ${CURSOR_ACP_MODEL_DISCOVERY_TIMEOUT_MS}ms.`;
discoveryWarning = `Cursor ACP model discovery timed out after ${Duration.toMillis(CURSOR_ACP_MODEL_DISCOVERY_TIMEOUT)}ms.`;
} else if (discoveryExit.value.value.length === 0) {
discoveryWarning = "Cursor ACP model discovery returned no built-in models.";
} else {
Expand Down
40 changes: 15 additions & 25 deletions apps/server/src/vcs/VcsProjectConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as Context from "effect/Context";
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 Schema from "effect/Schema";

Expand All @@ -15,16 +16,9 @@ const ProjectVcsConfig = Schema.Struct({
),
vcsKind: Schema.optional(VcsDriverKind),
});
const isProjectVcsConfig = Schema.is(ProjectVcsConfig);
const decodeProjectVcsConfig = Schema.decodeUnknownOption(Schema.fromJsonString(ProjectVcsConfig));

interface ProjectVcsConfigFile {
readonly vcs?:
| {
readonly kind?: VcsDriverKindType | undefined;
}
| undefined;
readonly vcsKind?: VcsDriverKindType | undefined;
}
type ProjectVcsConfigFile = typeof ProjectVcsConfig.Type;

export interface VcsProjectConfigResolveInput {
readonly cwd: string;
Expand All @@ -45,13 +39,8 @@ function configuredKind(config: ProjectVcsConfigFile): VcsDriverKindType | "auto
return config.vcs?.kind ?? config.vcsKind ?? "auto";
}

function parseConfig(raw: string): ProjectVcsConfigFile | null {
try {
const parsed = JSON.parse(raw) as unknown;
return isProjectVcsConfig(parsed) ? parsed : null;
} catch {
return null;
}
function parseConfig(raw: string): Option.Option<ProjectVcsConfigFile> {
return decodeProjectVcsConfig(raw);
}

export const make = Effect.fn("makeVcsProjectConfig")(function* () {
Expand All @@ -63,12 +52,12 @@ export const make = Effect.fn("makeVcsProjectConfig")(function* () {
while (true) {
const candidate = path.join(current, ".t3code", "vcs.json");
if (yield* fileSystem.exists(candidate).pipe(Effect.orElseSucceed(() => false))) {
return candidate;
return Option.some(candidate);
}

const parent = path.dirname(current);
if (parent === current) {
return null;
return Option.none<string>();
}
current = parent;
}
Expand All @@ -78,26 +67,27 @@ export const make = Effect.fn("makeVcsProjectConfig")(function* () {
configPath: string,
) {
const raw = yield* fileSystem.readFileString(configPath).pipe(
Effect.map(Option.some),
Effect.catch((error) =>
Effect.logWarning("failed to read VCS project config", {
configPath,
error,
}).pipe(Effect.as(null)),
}).pipe(Effect.as(Option.none<string>())),
),
);
if (raw === null) {
if (Option.isNone(raw)) {
return "auto" as const;
}

const parsed = parseConfig(raw);
if (parsed === null) {
const parsed = parseConfig(raw.value);
if (Option.isNone(parsed)) {
yield* Effect.logWarning("invalid VCS project config", {
configPath,
});
return "auto" as const;
}

return configuredKind(parsed);
return configuredKind(parsed.value);
});

const resolveKind: VcsProjectConfigShape["resolveKind"] = Effect.fn(
Expand All @@ -108,11 +98,11 @@ export const make = Effect.fn("makeVcsProjectConfig")(function* () {
}

const configPath = yield* findConfigPath(input.cwd);
if (configPath === null) {
if (Option.isNone(configPath)) {
return "auto";
}

return yield* readConfiguredKind(configPath);
return yield* readConfiguredKind(configPath.value);
});

return VcsProjectConfig.of({
Expand Down
Loading