dsh-subscriptions 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -32
- package/lib/auth/rpc.d.ts +10 -24
- package/lib/auth/rpc.js +18 -51
- package/lib/client/ProviderAccountManager.d.ts +1 -1
- package/lib/client/ProviderAccountManager.js +86 -110
- package/lib/client/ProviderModelEditor.d.ts +1 -1
- package/lib/client/ProviderModelEditor.js +61 -43
- package/lib/client/SpeedSelect.js +13 -12
- package/lib/client/SubscriptionUsageBadge.d.ts +1 -1
- package/lib/client/SubscriptionUsageBadge.js +1 -13
- package/lib/client/SubscriptionsSection.d.ts +2 -0
- package/lib/client/SubscriptionsSection.js +10 -4
- package/lib/client/locales.d.ts +12 -32
- package/lib/client/locales.js +14 -34
- package/lib/client/model-filter.d.ts +37 -0
- package/lib/client/model-filter.js +45 -0
- package/lib/client/subscription-card-styles.d.ts +1 -1
- package/lib/client/subscription-card-styles.js +1 -1
- package/lib/client/usage-window-filter.d.ts +7 -0
- package/lib/client/usage-window-filter.js +12 -0
- package/lib/client.js +374 -584
- package/lib/client.js.map +1 -1
- package/lib/index.js +249 -3322
- package/lib/provider-settings.d.ts +5 -0
- package/lib/provider-settings.js +21 -0
- package/lib/providers/ollama-settings.d.ts +1 -1
- package/lib/providers/ollama-settings.js +1 -1
- package/lib/translate/antigravity.js +10 -2
- package/package.json +2 -7
- package/lib/client/CursorCard.d.ts +0 -9
- package/lib/client/CursorCard.js +0 -151
- package/lib/providers/cursor-adapter.d.ts +0 -50
- package/lib/providers/cursor-adapter.js +0 -304
- package/lib/providers/cursor-auth.d.ts +0 -43
- package/lib/providers/cursor-auth.js +0 -221
- package/lib/providers/cursor-model-settings.d.ts +0 -15
- package/lib/providers/cursor-model-settings.js +0 -60
- package/lib/providers/cursor-usage.d.ts +0 -9
- package/lib/providers/cursor-usage.js +0 -131
- package/vendor/cursor/LICENSE +0 -21
- package/vendor/cursor/index.d.ts +0 -18
- package/vendor/cursor/index.js +0 -3574
- package/vendor/cursor/proto.js +0 -254
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/** Cursor OAuth session and browser-login lifecycle. Credentials stay in DSH. */
|
|
2
|
-
export declare const CURSOR_CREDENTIAL_REF: import("@deepseek-ai/dsh-credentials").CredentialRef;
|
|
3
|
-
export interface CursorCredential {
|
|
4
|
-
type: 'oauth';
|
|
5
|
-
access: string;
|
|
6
|
-
refresh: string;
|
|
7
|
-
expires: number;
|
|
8
|
-
}
|
|
9
|
-
export interface CursorAuthStatus {
|
|
10
|
-
authenticated: boolean;
|
|
11
|
-
busy: boolean;
|
|
12
|
-
expiresAt?: number;
|
|
13
|
-
error?: string;
|
|
14
|
-
}
|
|
15
|
-
export interface CursorCredentialService {
|
|
16
|
-
resolve(ref: typeof CURSOR_CREDENTIAL_REF): Promise<{
|
|
17
|
-
value: string;
|
|
18
|
-
} | undefined>;
|
|
19
|
-
set(ref: typeof CURSOR_CREDENTIAL_REF, value: string): Promise<void>;
|
|
20
|
-
unset(ref: typeof CURSOR_CREDENTIAL_REF): Promise<void>;
|
|
21
|
-
}
|
|
22
|
-
export declare class CursorAuth {
|
|
23
|
-
private readonly credentials;
|
|
24
|
-
private readonly http;
|
|
25
|
-
private readonly now;
|
|
26
|
-
private loginTask;
|
|
27
|
-
private error;
|
|
28
|
-
private refreshTask;
|
|
29
|
-
constructor(credentials: CursorCredentialService, http?: typeof fetch, now?: () => number);
|
|
30
|
-
private read;
|
|
31
|
-
status(): Promise<CursorAuthStatus>;
|
|
32
|
-
accessToken({ signal, }?: {
|
|
33
|
-
signal?: AbortSignal;
|
|
34
|
-
}): Promise<string>;
|
|
35
|
-
private refresh;
|
|
36
|
-
/** Return a Cursor browser URL immediately; polling continues on the host. */
|
|
37
|
-
login(): Promise<{
|
|
38
|
-
authorizeUrl: string;
|
|
39
|
-
}>;
|
|
40
|
-
private pollLogin;
|
|
41
|
-
cancel(): Promise<void>;
|
|
42
|
-
logout(): Promise<void>;
|
|
43
|
-
}
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
/** Cursor OAuth session and browser-login lifecycle. Credentials stay in DSH. */
|
|
2
|
-
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
3
|
-
import { credentialRef } from '@deepseek-ai/dsh-credentials';
|
|
4
|
-
import { parseProviderJson } from './common.js';
|
|
5
|
-
export const CURSOR_CREDENTIAL_REF = credentialRef('CURSOR_SUBSCRIPTION_OAUTH');
|
|
6
|
-
const API_ORIGIN = 'https://api2.cursor.sh';
|
|
7
|
-
const REFRESH_AHEAD_MS = 5 * 60_000;
|
|
8
|
-
async function parseCredential(value) {
|
|
9
|
-
if (value === undefined || value.length === 0)
|
|
10
|
-
return undefined;
|
|
11
|
-
// A parse failure here is a local shape error, not a provider signal. That is a
|
|
12
|
-
// deliberate change: the vendor classifies a stream failure from the message
|
|
13
|
-
// text, and the raw JSON.parse message used to carry a body excerpt, so a
|
|
14
|
-
// corrupt credential whose body happened to contain a word like `quota` was
|
|
15
|
-
// classified as a rate limit. Sanitising the message removes that accident, so
|
|
16
|
-
// a malformed credential now fails as a general provider error instead.
|
|
17
|
-
const parsed = await parseProviderJson(value, 'Cursor stored credential');
|
|
18
|
-
if (parsed === null || typeof parsed !== 'object')
|
|
19
|
-
throw new Error('Cursor credential is malformed');
|
|
20
|
-
const item = parsed;
|
|
21
|
-
if (item.type !== 'oauth' ||
|
|
22
|
-
typeof item.access !== 'string' ||
|
|
23
|
-
item.access.length === 0 ||
|
|
24
|
-
typeof item.refresh !== 'string' ||
|
|
25
|
-
typeof item.expires !== 'number' ||
|
|
26
|
-
!Number.isFinite(item.expires)) {
|
|
27
|
-
throw new Error('Cursor credential is malformed');
|
|
28
|
-
}
|
|
29
|
-
return {
|
|
30
|
-
type: 'oauth',
|
|
31
|
-
access: item.access,
|
|
32
|
-
refresh: item.refresh,
|
|
33
|
-
expires: item.expires,
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
function tokenExpiry(token, now) {
|
|
37
|
-
try {
|
|
38
|
-
const payload = token.split('.')[1];
|
|
39
|
-
if (payload === undefined)
|
|
40
|
-
return now + 24 * 60 * 60_000;
|
|
41
|
-
const data = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
|
|
42
|
-
const exp = data?.exp;
|
|
43
|
-
if (typeof exp === 'number' && Number.isFinite(exp))
|
|
44
|
-
return exp * 1000;
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
/* JWTs without exp still receive a bounded lifetime. */
|
|
48
|
-
}
|
|
49
|
-
return now + 24 * 60 * 60_000;
|
|
50
|
-
}
|
|
51
|
-
function abortableDelay(ms, signal) {
|
|
52
|
-
if (signal.aborted)
|
|
53
|
-
return Promise.reject(signal.reason);
|
|
54
|
-
return new Promise((resolve, reject) => {
|
|
55
|
-
const timer = setTimeout(() => {
|
|
56
|
-
signal.removeEventListener('abort', aborted);
|
|
57
|
-
resolve();
|
|
58
|
-
}, ms);
|
|
59
|
-
function aborted() {
|
|
60
|
-
clearTimeout(timer);
|
|
61
|
-
reject(signal.reason);
|
|
62
|
-
}
|
|
63
|
-
signal.addEventListener('abort', aborted, { once: true });
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
export class CursorAuth {
|
|
67
|
-
credentials;
|
|
68
|
-
http;
|
|
69
|
-
now;
|
|
70
|
-
loginTask;
|
|
71
|
-
error;
|
|
72
|
-
refreshTask;
|
|
73
|
-
constructor(credentials, http = fetch, now = Date.now) {
|
|
74
|
-
this.credentials = credentials;
|
|
75
|
-
this.http = http;
|
|
76
|
-
this.now = now;
|
|
77
|
-
}
|
|
78
|
-
async read() {
|
|
79
|
-
return parseCredential((await this.credentials.resolve(CURSOR_CREDENTIAL_REF))?.value);
|
|
80
|
-
}
|
|
81
|
-
async status() {
|
|
82
|
-
const current = await this.read();
|
|
83
|
-
return {
|
|
84
|
-
authenticated: current !== undefined,
|
|
85
|
-
busy: this.loginTask !== undefined,
|
|
86
|
-
...(current === undefined ? {} : { expiresAt: current.expires }),
|
|
87
|
-
...(this.error === undefined ? {} : { error: this.error }),
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
async accessToken({ signal, } = {}) {
|
|
91
|
-
signal?.throwIfAborted();
|
|
92
|
-
const current = await this.read();
|
|
93
|
-
if (current === undefined)
|
|
94
|
-
throw new Error('Cursor is not signed in');
|
|
95
|
-
if (current.expires - this.now() > REFRESH_AHEAD_MS)
|
|
96
|
-
return current.access;
|
|
97
|
-
if (current.refresh.length === 0)
|
|
98
|
-
throw new Error('Cursor sign-in needs to be renewed');
|
|
99
|
-
if (this.refreshTask === undefined) {
|
|
100
|
-
const pending = this.refresh(current, signal);
|
|
101
|
-
this.refreshTask = pending;
|
|
102
|
-
void pending
|
|
103
|
-
.finally(() => {
|
|
104
|
-
if (this.refreshTask === pending)
|
|
105
|
-
this.refreshTask = undefined;
|
|
106
|
-
})
|
|
107
|
-
.catch(() => undefined);
|
|
108
|
-
}
|
|
109
|
-
return (await this.refreshTask).access;
|
|
110
|
-
}
|
|
111
|
-
async refresh(current, signal) {
|
|
112
|
-
const response = await this.http(`${API_ORIGIN}/auth/exchange_user_api_key`, {
|
|
113
|
-
method: 'POST',
|
|
114
|
-
redirect: 'error',
|
|
115
|
-
headers: {
|
|
116
|
-
authorization: `Bearer ${current.refresh}`,
|
|
117
|
-
accept: 'application/json',
|
|
118
|
-
'content-type': 'application/json',
|
|
119
|
-
},
|
|
120
|
-
body: '{}',
|
|
121
|
-
...(signal === undefined ? {} : { signal }),
|
|
122
|
-
});
|
|
123
|
-
if (!response.ok)
|
|
124
|
-
throw new Error(`Cursor token refresh failed (HTTP ${response.status})`);
|
|
125
|
-
const body = (await parseProviderJson(response, 'Cursor token refresh'));
|
|
126
|
-
if (typeof body.accessToken !== 'string' || body.accessToken.length === 0) {
|
|
127
|
-
throw new Error('Cursor token refresh returned no access token');
|
|
128
|
-
}
|
|
129
|
-
const next = {
|
|
130
|
-
type: 'oauth',
|
|
131
|
-
access: body.accessToken,
|
|
132
|
-
refresh: typeof body.refreshToken === 'string' && body.refreshToken.length > 0
|
|
133
|
-
? body.refreshToken
|
|
134
|
-
: current.refresh,
|
|
135
|
-
expires: tokenExpiry(body.accessToken, this.now()),
|
|
136
|
-
};
|
|
137
|
-
const latest = await this.read();
|
|
138
|
-
if (latest === undefined)
|
|
139
|
-
throw new Error('Cursor signed out during token refresh');
|
|
140
|
-
if (latest.refresh !== current.refresh)
|
|
141
|
-
return latest;
|
|
142
|
-
await this.credentials.set(CURSOR_CREDENTIAL_REF, JSON.stringify(next));
|
|
143
|
-
return next;
|
|
144
|
-
}
|
|
145
|
-
/** Return a Cursor browser URL immediately; polling continues on the host. */
|
|
146
|
-
async login() {
|
|
147
|
-
await this.cancel();
|
|
148
|
-
this.error = undefined;
|
|
149
|
-
const verifier = randomBytes(96).toString('base64url');
|
|
150
|
-
const challenge = createHash('sha256').update(verifier).digest('base64url');
|
|
151
|
-
const uuid = randomUUID();
|
|
152
|
-
const url = new URL('https://cursor.com/loginDeepControl');
|
|
153
|
-
url.search = new URLSearchParams({
|
|
154
|
-
challenge,
|
|
155
|
-
uuid,
|
|
156
|
-
mode: 'login',
|
|
157
|
-
redirectTarget: 'cli',
|
|
158
|
-
}).toString();
|
|
159
|
-
const abort = new AbortController();
|
|
160
|
-
const promise = this.pollLogin(uuid, verifier, abort.signal)
|
|
161
|
-
.catch((error) => {
|
|
162
|
-
if (!abort.signal.aborted)
|
|
163
|
-
this.error = error instanceof Error ? error.message : String(error);
|
|
164
|
-
})
|
|
165
|
-
.finally(() => {
|
|
166
|
-
if (this.loginTask?.abort === abort)
|
|
167
|
-
this.loginTask = undefined;
|
|
168
|
-
});
|
|
169
|
-
this.loginTask = { abort, promise };
|
|
170
|
-
return { authorizeUrl: url.href };
|
|
171
|
-
}
|
|
172
|
-
async pollLogin(uuid, verifier, signal) {
|
|
173
|
-
let delay = 1000;
|
|
174
|
-
for (let attempt = 0; attempt < 150; attempt++) {
|
|
175
|
-
await abortableDelay(delay, signal);
|
|
176
|
-
const url = `${API_ORIGIN}/auth/poll?uuid=${encodeURIComponent(uuid)}&verifier=${encodeURIComponent(verifier)}`;
|
|
177
|
-
const response = await this.http(url, {
|
|
178
|
-
headers: { accept: 'application/json' },
|
|
179
|
-
redirect: 'error',
|
|
180
|
-
signal,
|
|
181
|
-
});
|
|
182
|
-
if (response.status === 404) {
|
|
183
|
-
delay = Math.min(Math.ceil(delay * 1.2), 10_000);
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
if (!response.ok)
|
|
187
|
-
throw new Error(`Cursor login failed (HTTP ${response.status})`);
|
|
188
|
-
const body = (await parseProviderJson(response, 'Cursor login poll'));
|
|
189
|
-
signal.throwIfAborted();
|
|
190
|
-
if (typeof body.accessToken !== 'string' ||
|
|
191
|
-
body.accessToken.length === 0) {
|
|
192
|
-
throw new Error('Cursor login returned no access token');
|
|
193
|
-
}
|
|
194
|
-
if (typeof body.refreshToken !== 'string' ||
|
|
195
|
-
body.refreshToken.length === 0) {
|
|
196
|
-
throw new Error('Cursor login returned no refresh token');
|
|
197
|
-
}
|
|
198
|
-
const value = {
|
|
199
|
-
type: 'oauth',
|
|
200
|
-
access: body.accessToken,
|
|
201
|
-
refresh: body.refreshToken,
|
|
202
|
-
expires: tokenExpiry(body.accessToken, this.now()),
|
|
203
|
-
};
|
|
204
|
-
await this.credentials.set(CURSOR_CREDENTIAL_REF, JSON.stringify(value));
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
throw new Error('Cursor login timed out');
|
|
208
|
-
}
|
|
209
|
-
async cancel() {
|
|
210
|
-
const task = this.loginTask;
|
|
211
|
-
task?.abort.abort(new Error('Cursor login cancelled'));
|
|
212
|
-
await task?.promise;
|
|
213
|
-
if (this.loginTask === task)
|
|
214
|
-
this.loginTask = undefined;
|
|
215
|
-
this.error = undefined;
|
|
216
|
-
}
|
|
217
|
-
async logout() {
|
|
218
|
-
await this.cancel();
|
|
219
|
-
await this.credentials.unset(CURSOR_CREDENTIAL_REF);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/** Cursor currently has one credential, so only provider-wide model visibility is configurable. */
|
|
2
|
-
export interface CursorModelSettings {
|
|
3
|
-
visibleModels?: string[];
|
|
4
|
-
}
|
|
5
|
-
export declare function validateCursorModelSettings(input: unknown): CursorModelSettings;
|
|
6
|
-
/** Durable visibility preference; account tokens remain in DSH credentials. */
|
|
7
|
-
export declare class CursorModelSettingsStore {
|
|
8
|
-
readonly path: string;
|
|
9
|
-
private current;
|
|
10
|
-
private writes;
|
|
11
|
-
constructor(path?: string);
|
|
12
|
-
get(): CursorModelSettings;
|
|
13
|
-
visibleModels(): readonly string[] | undefined;
|
|
14
|
-
set(input: unknown): Promise<void>;
|
|
15
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { mkdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { dirname } from 'node:path';
|
|
4
|
-
import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
5
|
-
export function validateCursorModelSettings(input) {
|
|
6
|
-
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
7
|
-
throw new Error('Cursor model settings must be an object');
|
|
8
|
-
}
|
|
9
|
-
const models = input.visibleModels;
|
|
10
|
-
if (models === undefined)
|
|
11
|
-
return {};
|
|
12
|
-
if (!Array.isArray(models) ||
|
|
13
|
-
models.some((id) => typeof id !== 'string' || !id.trim())) {
|
|
14
|
-
throw new Error('visibleModels must be an array of model ids');
|
|
15
|
-
}
|
|
16
|
-
return { visibleModels: [...new Set(models)] };
|
|
17
|
-
}
|
|
18
|
-
/** Durable visibility preference; account tokens remain in DSH credentials. */
|
|
19
|
-
export class CursorModelSettingsStore {
|
|
20
|
-
path;
|
|
21
|
-
current = {};
|
|
22
|
-
writes = Promise.resolve();
|
|
23
|
-
constructor(path = dshHomePath('plugins', 'subscriptions', 'cursor-model-settings.json')) {
|
|
24
|
-
this.path = path;
|
|
25
|
-
try {
|
|
26
|
-
this.current = validateCursorModelSettings(JSON.parse(readFileSync(path, 'utf8')));
|
|
27
|
-
}
|
|
28
|
-
catch (error) {
|
|
29
|
-
if (error.code !== 'ENOENT')
|
|
30
|
-
throw new Error(`Cannot read Cursor model settings at ${path}`, {
|
|
31
|
-
cause: error,
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
get() {
|
|
36
|
-
return structuredClone(this.current);
|
|
37
|
-
}
|
|
38
|
-
visibleModels() {
|
|
39
|
-
return this.current.visibleModels;
|
|
40
|
-
}
|
|
41
|
-
set(input) {
|
|
42
|
-
const validated = validateCursorModelSettings(input);
|
|
43
|
-
const run = this.writes.then(async () => {
|
|
44
|
-
await mkdir(dirname(this.path), { recursive: true });
|
|
45
|
-
const temporary = `${this.path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
46
|
-
try {
|
|
47
|
-
await writeFile(temporary, JSON.stringify(validated, null, 2), {
|
|
48
|
-
mode: 0o600,
|
|
49
|
-
});
|
|
50
|
-
await rename(temporary, this.path);
|
|
51
|
-
}
|
|
52
|
-
finally {
|
|
53
|
-
await rm(temporary, { force: true });
|
|
54
|
-
}
|
|
55
|
-
this.current = validated;
|
|
56
|
-
});
|
|
57
|
-
this.writes = run.catch(() => undefined);
|
|
58
|
-
return run;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/** Cursor individual-account quota projection from its dashboard endpoints. */
|
|
2
|
-
import type { ProviderUsage } from './common.js';
|
|
3
|
-
/** Cursor's WorkOS user id from the access JWT, without the identity prefix. */
|
|
4
|
-
export declare function cursorUserId(token: string): string | undefined;
|
|
5
|
-
/**
|
|
6
|
-
* Read Cursor's individual dashboard quota using an OAuth access token.
|
|
7
|
-
* The dashboard cookie is request-local; no token enters ProviderUsage.
|
|
8
|
-
*/
|
|
9
|
-
export declare function fetchCursorUsage(accessToken: string, http?: typeof fetch, signal?: AbortSignal): Promise<ProviderUsage>;
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
/** Cursor individual-account quota projection from its dashboard endpoints. */
|
|
2
|
-
import { parseProviderJson } from './common.js';
|
|
3
|
-
const ORIGIN = 'https://cursor.com';
|
|
4
|
-
function record(value) {
|
|
5
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
6
|
-
? value
|
|
7
|
-
: undefined;
|
|
8
|
-
}
|
|
9
|
-
function number(value) {
|
|
10
|
-
if (typeof value === 'number' && Number.isFinite(value))
|
|
11
|
-
return value;
|
|
12
|
-
if (typeof value === 'string' && value.trim() !== '') {
|
|
13
|
-
const parsed = Number(value);
|
|
14
|
-
if (Number.isFinite(parsed))
|
|
15
|
-
return parsed;
|
|
16
|
-
}
|
|
17
|
-
return undefined;
|
|
18
|
-
}
|
|
19
|
-
/** Cursor's WorkOS user id from the access JWT, without the identity prefix. */
|
|
20
|
-
export function cursorUserId(token) {
|
|
21
|
-
try {
|
|
22
|
-
const parts = token.split('.');
|
|
23
|
-
if (parts.length !== 3 || !parts[1])
|
|
24
|
-
return undefined;
|
|
25
|
-
const claims = record(JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8')));
|
|
26
|
-
const raw = claims?.sub;
|
|
27
|
-
if (typeof raw !== 'string' || raw.length === 0)
|
|
28
|
-
return undefined;
|
|
29
|
-
const id = raw.includes('|') ? raw.split('|').at(-1) : raw;
|
|
30
|
-
return id !== undefined && /^[A-Za-z0-9_-]+$/.test(id) ? id : undefined;
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
return undefined;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
async function dashboardJson(url, cookie, http, signal) {
|
|
37
|
-
const response = await http(url, {
|
|
38
|
-
headers: {
|
|
39
|
-
accept: 'application/json',
|
|
40
|
-
cookie,
|
|
41
|
-
origin: ORIGIN,
|
|
42
|
-
referer: `${ORIGIN}/dashboard`,
|
|
43
|
-
},
|
|
44
|
-
...(signal === undefined ? {} : { signal }),
|
|
45
|
-
});
|
|
46
|
-
if (!response.ok)
|
|
47
|
-
throw new Error(`Cursor usage endpoint returned HTTP ${response.status}`);
|
|
48
|
-
return parseProviderJson(response, 'Cursor usage');
|
|
49
|
-
}
|
|
50
|
-
function legacyRequestWindow(body, resetsAt) {
|
|
51
|
-
const usage = record(body);
|
|
52
|
-
if (usage === undefined)
|
|
53
|
-
return undefined;
|
|
54
|
-
const gpt4 = record(usage['gpt-4']);
|
|
55
|
-
const candidate = gpt4 !== undefined && number(gpt4.numRequests) !== undefined
|
|
56
|
-
? gpt4
|
|
57
|
-
: Object.values(usage)
|
|
58
|
-
.map(record)
|
|
59
|
-
.filter((value) => value !== undefined)
|
|
60
|
-
.sort((a, b) => (number(b.maxRequestUsage) ?? 0) -
|
|
61
|
-
(number(a.maxRequestUsage) ?? 0))[0];
|
|
62
|
-
const used = number(candidate?.numRequests);
|
|
63
|
-
const limit = number(candidate?.maxRequestUsage);
|
|
64
|
-
if (used === undefined || limit === undefined || limit <= 0)
|
|
65
|
-
return undefined;
|
|
66
|
-
return {
|
|
67
|
-
kind: 'other',
|
|
68
|
-
scope: 'Included requests',
|
|
69
|
-
usedPercent: Math.max(0, Math.min(100, (used / limit) * 100)),
|
|
70
|
-
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Read Cursor's individual dashboard quota using an OAuth access token.
|
|
75
|
-
* The dashboard cookie is request-local; no token enters ProviderUsage.
|
|
76
|
-
*/
|
|
77
|
-
export async function fetchCursorUsage(accessToken, http = fetch, signal) {
|
|
78
|
-
const userId = cursorUserId(accessToken);
|
|
79
|
-
if (userId === undefined)
|
|
80
|
-
throw new Error('Cursor session token has no usable user identity');
|
|
81
|
-
const cookie = `WorkosCursorSessionToken=${userId}::${accessToken}`;
|
|
82
|
-
const [summaryResult, legacyResult] = await Promise.allSettled([
|
|
83
|
-
dashboardJson(`${ORIGIN}/api/usage-summary`, cookie, http, signal),
|
|
84
|
-
dashboardJson(`${ORIGIN}/api/usage?user=${encodeURIComponent(userId)}`, cookie, http, signal),
|
|
85
|
-
]);
|
|
86
|
-
if (summaryResult.status === 'rejected' &&
|
|
87
|
-
legacyResult.status === 'rejected') {
|
|
88
|
-
throw summaryResult.reason instanceof Error
|
|
89
|
-
? summaryResult.reason
|
|
90
|
-
: new Error('Cursor usage is unavailable');
|
|
91
|
-
}
|
|
92
|
-
const summary = summaryResult.status === 'fulfilled'
|
|
93
|
-
? record(summaryResult.value)
|
|
94
|
-
: undefined;
|
|
95
|
-
const plan = record(record(summary?.individualUsage)?.plan) ??
|
|
96
|
-
record(record(summary?.teamUsage)?.plan);
|
|
97
|
-
const resetValue = summary?.billingCycleEnd;
|
|
98
|
-
const parsedReset = typeof resetValue === 'string' ? Date.parse(resetValue) : NaN;
|
|
99
|
-
const resetsAt = Number.isFinite(parsedReset) ? parsedReset : undefined;
|
|
100
|
-
const startValue = summary?.billingCycleStart;
|
|
101
|
-
const parsedStart = typeof startValue === 'string' ? Date.parse(startValue) : NaN;
|
|
102
|
-
const timing = {
|
|
103
|
-
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
104
|
-
...(Number.isFinite(parsedStart) ? { startsAt: parsedStart } : {}),
|
|
105
|
-
};
|
|
106
|
-
const windows = [];
|
|
107
|
-
for (const [field, scope] of [
|
|
108
|
-
['totalPercentUsed', 'Included'],
|
|
109
|
-
['autoPercentUsed', 'Cursor Models'],
|
|
110
|
-
['apiPercentUsed', 'Other Models'],
|
|
111
|
-
]) {
|
|
112
|
-
const usedPercent = number(plan?.[field]);
|
|
113
|
-
if (usedPercent === undefined || usedPercent < 0 || usedPercent > 100)
|
|
114
|
-
continue;
|
|
115
|
-
windows.push({ kind: 'other', scope, usedPercent, ...timing });
|
|
116
|
-
}
|
|
117
|
-
const legacy = legacyRequestWindow(legacyResult.status === 'fulfilled' ? legacyResult.value : undefined, resetsAt);
|
|
118
|
-
if (legacy !== undefined)
|
|
119
|
-
windows.push({ ...legacy, ...timing });
|
|
120
|
-
if (windows.length === 0 && summary?.isUnlimited !== true) {
|
|
121
|
-
throw new Error('Cursor usage response has no quota windows');
|
|
122
|
-
}
|
|
123
|
-
const membershipType = summary?.membershipType;
|
|
124
|
-
return {
|
|
125
|
-
supported: true,
|
|
126
|
-
windows,
|
|
127
|
-
...(typeof membershipType === 'string' && membershipType.length > 0
|
|
128
|
-
? { plan: membershipType }
|
|
129
|
-
: {}),
|
|
130
|
-
};
|
|
131
|
-
}
|
package/vendor/cursor/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 dsh-cursor-subscription contributors
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
package/vendor/cursor/index.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { LlmAdapter } from '@deepseek-ai/dsh-llm'
|
|
2
|
-
import type { GenerateOptions, LlmModelInfo, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm'
|
|
3
|
-
|
|
4
|
-
export class CursorAdapter extends LlmAdapter {
|
|
5
|
-
constructor(options: {
|
|
6
|
-
auth: { accessToken(options?: { signal?: AbortSignal }): Promise<string> }
|
|
7
|
-
resolveAttachments?: () => unknown
|
|
8
|
-
fetchModels?: (access: string) => Promise<{ id: string; name: string }[]>
|
|
9
|
-
createAgentRun?: (access: string) => unknown
|
|
10
|
-
logger?: { warn?(message: string, ...args: unknown[]): void }
|
|
11
|
-
})
|
|
12
|
-
providerInfo(provider: string): { id: string; name: string }
|
|
13
|
-
listModels(provider: string): Promise<LlmModelInfo[]>
|
|
14
|
-
listModelsForRpc(options?: { force?: boolean; signal?: AbortSignal }): Promise<{ id: string; name: string }[]>
|
|
15
|
-
invalidateModels(): void
|
|
16
|
-
resolveModel(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>
|
|
17
|
-
stream(options: GenerateOptions): AsyncIterable<StreamChunk>
|
|
18
|
-
}
|