dsh-subscriptions 0.2.3 → 0.3.1

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.
Files changed (41) hide show
  1. package/README.md +9 -21
  2. package/lib/auth/rpc.d.ts +2 -20
  3. package/lib/auth/rpc.js +3 -49
  4. package/lib/client/ProviderAccountManager.d.ts +1 -1
  5. package/lib/client/ProviderAccountManager.js +86 -110
  6. package/lib/client/ProviderModelEditor.d.ts +1 -1
  7. package/lib/client/ProviderModelEditor.js +2 -6
  8. package/lib/client/SubscriptionUsageBadge.d.ts +8 -2
  9. package/lib/client/SubscriptionUsageBadge.js +24 -17
  10. package/lib/client/SubscriptionsSection.js +1 -2
  11. package/lib/client/locales.d.ts +2 -32
  12. package/lib/client/locales.js +2 -32
  13. package/lib/client/model-filter.js +1 -1
  14. package/lib/client/subscription-card-styles.d.ts +1 -1
  15. package/lib/client/subscription-card-styles.js +1 -1
  16. package/lib/client.js +234 -518
  17. package/lib/client.js.map +1 -1
  18. package/lib/index.js +208 -3305
  19. package/lib/providers/claude.d.ts +3 -1
  20. package/lib/providers/claude.js +2 -2
  21. package/lib/providers/ollama-settings.d.ts +1 -1
  22. package/lib/providers/ollama-settings.js +1 -1
  23. package/lib/providers/pool-usage.d.ts +10 -9
  24. package/lib/providers/pool-usage.js +30 -19
  25. package/lib/providers/pool.d.ts +6 -6
  26. package/lib/providers/pool.js +24 -12
  27. package/package.json +1 -7
  28. package/lib/client/CursorCard.d.ts +0 -9
  29. package/lib/client/CursorCard.js +0 -151
  30. package/lib/providers/cursor-adapter.d.ts +0 -50
  31. package/lib/providers/cursor-adapter.js +0 -304
  32. package/lib/providers/cursor-auth.d.ts +0 -43
  33. package/lib/providers/cursor-auth.js +0 -221
  34. package/lib/providers/cursor-model-settings.d.ts +0 -15
  35. package/lib/providers/cursor-model-settings.js +0 -60
  36. package/lib/providers/cursor-usage.d.ts +0 -9
  37. package/lib/providers/cursor-usage.js +0 -131
  38. package/vendor/cursor/LICENSE +0 -21
  39. package/vendor/cursor/index.d.ts +0 -18
  40. package/vendor/cursor/index.js +0 -3574
  41. package/vendor/cursor/proto.js +0 -254
@@ -1,304 +0,0 @@
1
- import { CursorAdapter } from 'dsh-subscriptions/cursor-transport';
2
- /**
3
- * The imported Cursor transport still reads tool results as user-role content
4
- * blocks. DSH 0.1.7 carries them as first-class role=tool messages instead.
5
- */
6
- export function projectCursorMessages(messages) {
7
- return messages.map((message) => message.role === 'tool'
8
- ? {
9
- ...message,
10
- role: 'user',
11
- content: [
12
- {
13
- type: 'tool-result',
14
- toolCallId: message.toolCallId,
15
- isError: message.isError,
16
- content: message.content,
17
- },
18
- ],
19
- }
20
- : message);
21
- }
22
- /**
23
- * Vendor `LlmError` codes whose whole message is a local literal (no response
24
- * header, trailer, body or end-stream text). Provider-derived failures can only
25
- * carry a `classifyCursorError` code or `CURSOR_PROTOCOL`, never these, so the
26
- * code is a sound discriminator for them.
27
- */
28
- const LOCAL_VENDOR_CODES = new Set([
29
- 'UNSUPPORTED_OPTION',
30
- 'UNSUPPORTED_CONTENT',
31
- 'TOOL_LIMIT',
32
- ]);
33
- /**
34
- * Plain-error messages the vendor throws as fixed literals (`vendor/cursor/
35
- * index.js`: `waitForResponse`, the request write, the tool-continuation write,
36
- * the stream `close` handler, the frame reader, the stall watchdog and the
37
- * settings validation). None
38
- * interpolates response text, so they are matched by exact message, the same way
39
- * the auth messages are. A provider that echoed one of these sentences would
40
- * only make the user read a local literal.
41
- */
42
- const LOCAL_VENDOR_MESSAGES = new Set([
43
- 'Cursor HTTP response timeout',
44
- 'Cursor HTTP stream closed before response',
45
- 'Cursor agent bridge closed before accepting the request',
46
- 'Cursor tool continuation bridge closed before accepting the result',
47
- 'Cursor sent an unreadable compressed frame',
48
- 'Cursor stream idle timeout',
49
- // `resolveCursorSettings`, run at the top of every stream.
50
- 'cursor-subscription: maxToolRounds must be an integer between 1 and 1000',
51
- 'cursor-subscription: retryCount must be an integer between 0 and 10',
52
- 'cursor-subscription: retryIntervalMs must be an integer between 0 and 300000',
53
- 'cursor-subscription: retryHttpStatusCodes must contain only HTTP status integers from 400 to 599',
54
- 'cursor-subscription: retryHttpStatusCodes must not contain duplicates',
55
- 'cursor-subscription: replayHistoryEachStep must be a boolean',
56
- ]);
57
- /**
58
- * The same, for the two literals that embed a number the vendor computes: the
59
- * response status (`Number(headers[':status'])`, so digits or `NaN`) and the
60
- * configured progress timeout.
61
- */
62
- const LOCAL_VENDOR_PATTERNS = [
63
- /^Cursor agent returned HTTP \d{3}$/,
64
- /^Cursor stream progress timeout: no content for \d+ms$/,
65
- ];
66
- /**
67
- * Operating-system network error codes. An `Error` carrying one of these on
68
- * itself, or on its `cause` (Node wraps a refused connection in
69
- * `ERR_HTTP2_STREAM_CANCEL`), was produced by the local socket layer, so its
70
- * message is local: `connect ECONNREFUSED 127.0.0.1:443`, `getaddrinfo ENOTFOUND
71
- * host`. This is a property of the thrown object, which a provider cannot
72
- * forge. It is deliberately not a test on the vendor's `TRANSPORT` code: the
73
- * vendor derives that code from message text, so a provider's end-stream
74
- * message or `grpc-message` containing "connection" or "socket" gets it too.
75
- * Transport failures with no such code (TLS and certificate errors, HTTP/2
76
- * protocol errors) are not recognised and stay replaced.
77
- */
78
- const LOCAL_SYSTEM_ERRNO = new Set([
79
- 'ECONNREFUSED',
80
- 'ECONNRESET',
81
- 'ECONNABORTED',
82
- 'ENOTFOUND',
83
- 'EAI_AGAIN',
84
- 'ETIMEDOUT',
85
- 'ENETUNREACH',
86
- 'ENETDOWN',
87
- 'EHOSTUNREACH',
88
- 'EHOSTDOWN',
89
- 'EPIPE',
90
- ]);
91
- function errorCode(value) {
92
- return typeof value === 'object' && value !== null
93
- ? value.code
94
- : undefined;
95
- }
96
- function isLocalSystemError(error) {
97
- if (!(error instanceof Error))
98
- return false;
99
- const own = errorCode(error);
100
- const cause = errorCode(error.cause);
101
- return ((typeof own === 'string' && LOCAL_SYSTEM_ERRNO.has(own)) ||
102
- (typeof cause === 'string' && LOCAL_SYSTEM_ERRNO.has(cause)));
103
- }
104
- function messageOf(error) {
105
- return error instanceof Error ? error.message : String(error);
106
- }
107
- /** Bound on remembered local failure messages; they are short and few. */
108
- const MAX_LOCAL_MESSAGES = 64;
109
- /**
110
- * Remembers the messages of failures raised by this machine rather than read
111
- * from the provider: whatever the auth service threw, whatever the host
112
- * attachment store threw while reading an image, and a socket-layer network
113
- * error seen on the run. The vendor stream turns each of these into
114
- * `finish/error` carrying exactly `error.message`, so a failure whose message
115
- * equals a remembered one originated locally and may keep its text. The match
116
- * is by exact text: a provider that echoed a remembered message would only
117
- * display a local literal.
118
- */
119
- class LocalFailureMessages {
120
- messages = new Set();
121
- /** Wrap `auth` so its failures are remembered and rethrown unchanged. */
122
- wrap(auth) {
123
- return {
124
- accessToken: async (options) => {
125
- try {
126
- return await auth.accessToken(options);
127
- }
128
- catch (error) {
129
- this.remember(messageOf(error));
130
- throw error;
131
- }
132
- },
133
- };
134
- }
135
- /**
136
- * Wrap the host attachment store the vendor reads images through, so a read
137
- * failure keeps its own message. The store is a local service; its errors
138
- * carry no provider text. Anything that is not a store passes through.
139
- */
140
- wrapAttachments(resolve) {
141
- return () => {
142
- const store = resolve();
143
- const readImage = store
144
- ?.readImage;
145
- if (typeof readImage !== 'function')
146
- return store;
147
- return Object.create(store, {
148
- readImage: {
149
- value: async (...args) => {
150
- try {
151
- return await readImage.apply(store, args);
152
- }
153
- catch (error) {
154
- this.remember(messageOf(error));
155
- throw error;
156
- }
157
- },
158
- },
159
- });
160
- };
161
- }
162
- /**
163
- * Observe a vendor run's two failure channels, the response promise and the
164
- * frame reader, and remember the message of an error the socket layer raised.
165
- * The run's behavior is unchanged: the same error is rethrown.
166
- */
167
- observeRun(run) {
168
- if (typeof run !== 'object' || run === null)
169
- return run;
170
- const target = run;
171
- const observe = (error) => {
172
- if (isLocalSystemError(error))
173
- this.remember(messageOf(error));
174
- throw error;
175
- };
176
- const wait = target.waitForResponse;
177
- if (typeof wait === 'function') {
178
- target.waitForResponse = (...args) => Promise.resolve(wait.apply(target, args)).catch(observe);
179
- }
180
- const frames = target.frames;
181
- const next = frames?.next;
182
- if (frames !== undefined && typeof next === 'function') {
183
- frames.next = (...args) => Promise.resolve(next.apply(frames, args)).catch(observe);
184
- }
185
- return run;
186
- }
187
- has(message) {
188
- return this.messages.has(message);
189
- }
190
- remember(message) {
191
- // An empty message displays nothing; remembering it would exempt every
192
- // empty failure from the boundary for no benefit.
193
- if (message === '')
194
- return;
195
- this.messages.delete(message);
196
- this.messages.add(message);
197
- if (this.messages.size > MAX_LOCAL_MESSAGES) {
198
- const oldest = this.messages.values().next().value;
199
- if (oldest !== undefined)
200
- this.messages.delete(oldest);
201
- }
202
- }
203
- }
204
- function isVendorLocalMessage(message) {
205
- return (LOCAL_VENDOR_MESSAGES.has(message) ||
206
- LOCAL_VENDOR_PATTERNS.some((pattern) => pattern.test(message)));
207
- }
208
- /**
209
- * Cursor adapter for the DSH host.
210
- *
211
- * Failure text contract: `finish/error` messages that originate from the
212
- * vendor stream and can quote provider-controlled text (the response content
213
- * type, trailers, end-stream errors, a frame-reader or other error whose message
214
- * is not one of the literals below, and a transport-coded failure that is not a
215
- * socket-layer error) are replaced by `<code> [provider response text omitted]`. Failures that are
216
- * wholly local keep their own message and code:
217
- *
218
- * - anything the auth service threw (not signed in, sign-in expired, token
219
- * refresh status), and a host attachment read failure;
220
- * - the vendor's own request validation, by code (`UNSUPPORTED_OPTION`,
221
- * `UNSUPPORTED_CONTENT`, `TOOL_LIMIT`);
222
- * - the vendor's fixed-literal plain errors, by exact message: the HTTP status
223
- * (`Cursor agent returned HTTP 429`), the response, idle and progress
224
- * timeouts, a closed bridge or stream, an unreadable compressed frame, and
225
- * an invalid Cursor setting;
226
- * - a network error raised by the operating system (`ECONNREFUSED`,
227
- * `ENOTFOUND`, ...), recognised from the thrown error's own code, not from
228
- * the vendor's classification.
229
- *
230
- * Not a provider-origin class: a network error such as `ECONNREFUSED` is a
231
- * local failure (no network, daemon down, DNS), and is kept. What is still
232
- * replaced although it is probably local is a transport failure with no
233
- * socket-layer errno: a TLS or certificate failure and an HTTP/2 protocol
234
- * error. The vendor's `TRANSPORT` code cannot decide those, because
235
- * `classifyCursorError` derives it from message text (`network`, `connection`,
236
- * `socket`, `fetch`, `ECONN`, `http2`) and a provider's end-stream message or
237
- * `grpc-message` can contain any of them; so the code is never trusted alone.
238
- * Telling these apart would need a check on the thrown error object, as the
239
- * errno check above does, extended to the TLS and HTTP/2 error codes. That is
240
- * not done here and is not verified.
241
- */
242
- export class CursorCompatAdapter extends CursorAdapter {
243
- visibleModels;
244
- localFailures;
245
- constructor(options) {
246
- const localFailures = new LocalFailureMessages();
247
- super({
248
- ...options,
249
- auth: localFailures.wrap(options.auth),
250
- ...(options.resolveAttachments === undefined
251
- ? {}
252
- : {
253
- resolveAttachments: localFailures.wrapAttachments(options.resolveAttachments),
254
- }),
255
- });
256
- this.localFailures = localFailures;
257
- this.visibleModels = options.visibleModels;
258
- // The vendor assigns `createAgentRun` (the injected one, or its own
259
- // `AgentRun` factory) in its constructor but does not type it as a member.
260
- const vendor = this;
261
- const createRun = vendor.createAgentRun;
262
- vendor.createAgentRun = (access) => localFailures.observeRun(createRun(access));
263
- }
264
- async listModels(provider) {
265
- const models = await super.listModels(provider);
266
- const visible = this.visibleModels?.();
267
- return visible === undefined
268
- ? models
269
- : models.filter((model) => visible.includes(model.id));
270
- }
271
- async *stream(options) {
272
- for await (const chunk of super.stream({
273
- ...options,
274
- // Existing DSH sessions may still hold this retired selection.
275
- model: options.model === 'composer-2' ? 'composer-2.5' : options.model,
276
- messages: projectCursorMessages(options.messages),
277
- })) {
278
- if (chunk.type === 'finish' &&
279
- chunk.reason.kind === 'error' &&
280
- !LOCAL_VENDOR_CODES.has(chunk.reason.failure.code) &&
281
- !isVendorLocalMessage(chunk.reason.failure.message) &&
282
- !this.localFailures.has(chunk.reason.failure.message)) {
283
- // The vendor uses raw headers, trailers and end-stream errors for protocol
284
- // checks and classification before this boundary. Keep its code and retry
285
- // decisions; do not reclassify the replacement text. "Safe" here means no
286
- // provider-controlled free text is displayed in failures, not that no
287
- // secret can exist anywhere internally.
288
- yield {
289
- ...chunk,
290
- reason: {
291
- ...chunk.reason,
292
- failure: {
293
- ...chunk.reason.failure,
294
- message: `${chunk.reason.failure.code} [provider response text omitted]`,
295
- },
296
- },
297
- };
298
- }
299
- else {
300
- yield chunk;
301
- }
302
- }
303
- }
304
- }
@@ -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>;