Open History to see the last commit
1import type { ServerProviderUsageWindow } from "@t3tools/contracts";
2import type { CursorSettings } from "../settings.ts";
3import { CURSOR_USAGE_WINDOWS } from "@t3tools/shared/usageLimits";
4import * as DateTime from "effect/DateTime";
5import * as Effect from "effect/Effect";
6import * as Option from "effect/Option";
7import * as Schema from "effect/Schema";
8import * as HttpClient from "effect/http/HttpClient";
9import * as HttpClientRequest from "effect/http/HttpClientRequest";
10import * as HttpClientResponse from "effect/http/HttpClientResponse";
11import {
12 clampPercent,
13 makeUnavailableUsageLimits,
14 makeUsageLimits,
15} from "@t3tools/provider-core/server/usageLimits";
16import { DEFAULT_CURSOR_API_ENDPOINT, exchangeCursorApiKey } from "./apiKey.ts";
17
18const CursorUsageResponse = Schema.Struct({
19 billingCycleStart: Schema.optional(Schema.Union([Schema.String, Schema.Number])),
20 billingCycleEnd: Schema.optional(Schema.Union([Schema.String, Schema.Number])),
21 planUsage: Schema.optional(
22 Schema.Struct({
23 totalPercentUsed: Schema.optional(Schema.Number),
24 autoPercentUsed: Schema.optional(Schema.Number),
25 apiPercentUsed: Schema.optional(Schema.Number),
26 }),
27 ),
28});
29
30/** Cursor's dashboard percentages include bonus usage; spend / limit does not. */
31export function cursorUsageResponseToLimits(
32 response: typeof CursorUsageResponse.Type,
33 checkedAt: string,
34) {
35 const reset = DateTime.make(Number(response.billingCycleEnd));
36 const resetsAt =
37 Number(response.billingCycleEnd) > 0 && Option.isSome(reset)
38 ? DateTime.formatIso(reset.value)
39 : undefined;
40 const cycleStart = Number(response.billingCycleStart);
41 const windowDurationMins =
42 resetsAt && cycleStart > 0
43 ? Math.round((Number(response.billingCycleEnd) - cycleStart) / 60_000)
44 : 0;
45 const windows: ServerProviderUsageWindow[] = [];
46 if (response.planUsage) {
47 for (const { id, label } of CURSOR_USAGE_WINDOWS) {
48 const usedPercent = response.planUsage[id];
49 if (usedPercent === undefined || !Number.isFinite(usedPercent)) continue;
50 windows.push({
51 id,
52 kind: "monthly",
53 label,
54 usedPercent: clampPercent(usedPercent),
55 ...(resetsAt ? { resetsAt } : {}),
56 ...(windowDurationMins > 0 ? { windowDurationMins } : {}),
57 });
58 }
59 }
60 return windows.length > 0
61 ? makeUsageLimits({ checkedAt, windows })
62 : makeUnavailableUsageLimits({ checkedAt, reason: "unsupported" });
63}
64
65export const readCursorUsageLimits = Effect.fn("readCursorUsageLimits")(function* (
66 settings: Pick<CursorSettings, "apiEndpoint">,
67 environment: NodeJS.ProcessEnv = process.env,
68) {
69 const checkedAt = DateTime.formatIso(yield* DateTime.now);
70 return yield* Effect.gen(function* () {
71 const endpoint = (
72 settings.apiEndpoint?.trim() ||
73 environment.CURSOR_API_ENDPOINT?.trim() ||
74 DEFAULT_CURSOR_API_ENDPOINT
75 ).replace(/\/$/, "");
76 let token = environment.CURSOR_AUTH_TOKEN?.trim();
77 const apiKey = environment.CURSOR_API_KEY?.trim();
78 if (!token && apiKey) {
79 if (endpoint !== DEFAULT_CURSOR_API_ENDPOINT) {
80 return makeUnavailableUsageLimits({
81 checkedAt,
82 reason: "unsupported",
83 message: "Cursor usage for an API key requires the default Cursor endpoint.",
84 });
85 }
86 token = yield* exchangeCursorApiKey(apiKey);
87 }
88 if (!token) return makeUnavailableUsageLimits({ checkedAt, reason: "unsupported" });
89 const client = yield* HttpClient.HttpClient;
90 const response = yield* client.execute(
91 HttpClientRequest.post(`${endpoint}/aiserver.v1.DashboardService/GetCurrentPeriodUsage`).pipe(
92 HttpClientRequest.bearerToken(token),
93 HttpClientRequest.setHeaders({
94 "connect-protocol-version": "1",
95 "x-cursor-client-type": "cli",
96 }),
97 HttpClientRequest.bodyJsonUnsafe({}),
98 ),
99 );
100 const body = yield* HttpClientResponse.schemaBodyJson(CursorUsageResponse)(
101 yield* HttpClientResponse.filterStatusOk(response),
102 );
103 return cursorUsageResponseToLimits(body, checkedAt);
104 }).pipe(
105 Effect.timeout("10 seconds"),
106 Effect.orElseSucceed(() =>
107 makeUnavailableUsageLimits({
108 checkedAt,
109 reason: "probeFailed",
110 message: "Cursor could not read usage limits.",
111 }),
112 ),
113 );
114});