1import type { SDKModel, SDKUser } from "@cursor/sdk";
2import * as Cache from "effect/Cache";
3import * as Context from "effect/Context";
4import * as Duration from "effect/Duration";
5import * as Effect from "effect/Effect";
6import * as Exit from "effect/Exit";
7import * as Layer from "effect/Layer";
8import * as Schema from "effect/Schema";
9
10import { AuthenticationError, Cursor, CursorSdkError } from "./sdk.ts";
11
12export interface CursorSdkCatalogSnapshot {
13 readonly user: SDKUser;
14 readonly models: ReadonlyArray<SDKModel>;
15}
16
17export class CursorSdkCatalogError extends Schema.TaggedError<CursorSdkCatalogError>()(
18 "CursorSdkCatalogError",
19 {
20 authenticationFailure: Schema.Boolean,
21 cause: Schema.Defect(),
22 },
23) {
24 override get message(): string {
25 return this.authenticationFailure
26 ? "Cursor SDK authentication failed."
27 : "Cursor SDK catalog request failed.";
28 }
29}
30
31export class CursorSdkCatalog extends Context.Service<
32 CursorSdkCatalog,
33 {
34 readonly read: (
35 apiKey: string,
36 ) => Effect.Effect<CursorSdkCatalogSnapshot, CursorSdkCatalogError>;
37 }
38>()("@t3tools/provider-cursor/server/CursorSdkCatalog") {}
39
40function isAuthenticationFailure(cause: unknown): boolean {
41 return (
42 cause instanceof AuthenticationError ||
43 (cause instanceof CursorSdkError && cause.status === 401)
44 );
45}
46
47export interface CursorSdkCatalogProbes {
48 readonly readUser: (apiKey: string) => Effect.Effect<SDKUser, CursorSdkCatalogError>;
49 readonly readModels: (
50 apiKey: string,
51 ) => Effect.Effect<ReadonlyArray<SDKModel>, CursorSdkCatalogError>;
52}
53
54const sdkRequest = <A>(tryRequest: () => Promise<A>) =>
55 Effect.tryPromise({
56 try: tryRequest,
57 catch: (cause) =>
58 new CursorSdkCatalogError({
59 authenticationFailure: isAuthenticationFailure(cause),
60 cause,
61 }),
62 });
63
64const liveProbes: CursorSdkCatalogProbes = {
65 readUser: (apiKey) => sdkRequest(() => Cursor.me({ apiKey })),
66 readModels: (apiKey) => sdkRequest(() => Cursor.models.list({ apiKey })),
67};
68
69export const makeCursorSdkCatalog = Effect.fn("CursorSdkCatalog.make")(function* (
70 probes: CursorSdkCatalogProbes = liveProbes,
71) {
72 const modelCache = yield* Cache.makeWith(probes.readModels, {
73 capacity: 32,
74 timeToLive: (exit) =>
75 Exit.isSuccess(exit) && exit.value.length > 0 ? Duration.minutes(30) : Duration.zero,
76 });
77 return CursorSdkCatalog.of({
78 read: (apiKey) =>
79 Effect.all(
80 {
81 user: probes.readUser(apiKey),
82 models: Cache.get(modelCache, apiKey),
83 },
84 { concurrency: "unbounded" },
85 ),
86 });
87});
88
89export const layer = Layer.effect(CursorSdkCatalog, makeCursorSdkCatalog());
90
91export function layerTest(
92 read: CursorSdkCatalog["Service"]["read"],
93): Layer.Layer<CursorSdkCatalog> {
94 return Layer.succeed(CursorSdkCatalog, CursorSdkCatalog.of({ read }));
95}