dsh-subscriptions 0.1.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/LICENSE +21 -0
- package/README.md +129 -0
- package/cordis.patch.yml +12 -0
- package/lib/auth/claude-code-creds.d.ts +23 -0
- package/lib/auth/claude-code-creds.js +172 -0
- package/lib/auth/device-flow.d.ts +55 -0
- package/lib/auth/device-flow.js +177 -0
- package/lib/auth/jwt.d.ts +10 -0
- package/lib/auth/jwt.js +25 -0
- package/lib/auth/oauth-flow.d.ts +91 -0
- package/lib/auth/oauth-flow.js +227 -0
- package/lib/auth/pkce.d.ts +31 -0
- package/lib/auth/pkce.js +35 -0
- package/lib/auth/rpc.d.ts +209 -0
- package/lib/auth/rpc.js +386 -0
- package/lib/auth/store.d.ts +196 -0
- package/lib/auth/store.js +468 -0
- package/lib/client/CursorCard.d.ts +9 -0
- package/lib/client/CursorCard.js +113 -0
- package/lib/client/ExternalUsageCards.d.ts +9 -0
- package/lib/client/ExternalUsageCards.js +72 -0
- package/lib/client/ImageGallery.d.ts +54 -0
- package/lib/client/ImageGallery.js +112 -0
- package/lib/client/ImageGenerateToolview.d.ts +48 -0
- package/lib/client/ImageGenerateToolview.js +133 -0
- package/lib/client/ProviderAccountManager.d.ts +13 -0
- package/lib/client/ProviderAccountManager.js +147 -0
- package/lib/client/ProviderModelEditor.d.ts +39 -0
- package/lib/client/ProviderModelEditor.js +107 -0
- package/lib/client/SpeedSelect.d.ts +71 -0
- package/lib/client/SpeedSelect.js +177 -0
- package/lib/client/SubscriptionUsageBadge.d.ts +116 -0
- package/lib/client/SubscriptionUsageBadge.js +543 -0
- package/lib/client/SubscriptionsSection.d.ts +140 -0
- package/lib/client/SubscriptionsSection.js +544 -0
- package/lib/client/UsageBadgeDisplaySetting.d.ts +7 -0
- package/lib/client/UsageBadgeDisplaySetting.js +36 -0
- package/lib/client/VideoGenerateToolview.d.ts +42 -0
- package/lib/client/VideoGenerateToolview.js +175 -0
- package/lib/client/account-preferences.d.ts +24 -0
- package/lib/client/account-preferences.js +21 -0
- package/lib/client/fast-command.d.ts +2 -0
- package/lib/client/fast-command.js +4 -0
- package/lib/client/host-contract.d.ts +90 -0
- package/lib/client/host-contract.js +101 -0
- package/lib/client/host-icons.d.ts +24 -0
- package/lib/client/host-icons.js +21 -0
- package/lib/client/index.d.ts +27 -0
- package/lib/client/index.js +136 -0
- package/lib/client/locales.d.ts +297 -0
- package/lib/client/locales.js +295 -0
- package/lib/client/subscription-card-styles.d.ts +3 -0
- package/lib/client/subscription-card-styles.js +53 -0
- package/lib/client/subscriptions-rpc.d.ts +14 -0
- package/lib/client/subscriptions-rpc.js +34 -0
- package/lib/client/usage-badge-preferences.d.ts +11 -0
- package/lib/client/usage-badge-preferences.js +41 -0
- package/lib/client.js +4472 -0
- package/lib/client.js.map +1 -0
- package/lib/compat.d.ts +36 -0
- package/lib/compat.js +20 -0
- package/lib/http.d.ts +6 -0
- package/lib/http.js +15 -0
- package/lib/index.d.ts +80 -0
- package/lib/index.js +14018 -0
- package/lib/model-defaults.d.ts +23 -0
- package/lib/model-defaults.js +237 -0
- package/lib/provider-settings.d.ts +39 -0
- package/lib/provider-settings.js +151 -0
- package/lib/providers/account-preferences.d.ts +41 -0
- package/lib/providers/account-preferences.js +153 -0
- package/lib/providers/accounts.d.ts +112 -0
- package/lib/providers/accounts.js +142 -0
- package/lib/providers/antigravity-oauth-client.d.ts +7 -0
- package/lib/providers/antigravity-oauth-client.js +7 -0
- package/lib/providers/antigravity.d.ts +110 -0
- package/lib/providers/antigravity.js +516 -0
- package/lib/providers/catalog-store.d.ts +48 -0
- package/lib/providers/catalog-store.js +243 -0
- package/lib/providers/claude-cli-version.d.ts +15 -0
- package/lib/providers/claude-cli-version.js +24 -0
- package/lib/providers/claude.d.ts +181 -0
- package/lib/providers/claude.js +680 -0
- package/lib/providers/codex-client-version.d.ts +8 -0
- package/lib/providers/codex-client-version.js +17 -0
- package/lib/providers/codex-search.d.ts +29 -0
- package/lib/providers/codex-search.js +198 -0
- package/lib/providers/codex.d.ts +229 -0
- package/lib/providers/codex.js +821 -0
- package/lib/providers/common.d.ts +389 -0
- package/lib/providers/common.js +656 -0
- package/lib/providers/copilot.d.ts +344 -0
- package/lib/providers/copilot.js +888 -0
- package/lib/providers/cursor-adapter.d.ts +15 -0
- package/lib/providers/cursor-adapter.js +39 -0
- package/lib/providers/cursor-auth.d.ts +43 -0
- package/lib/providers/cursor-auth.js +169 -0
- package/lib/providers/cursor-model-settings.d.ts +15 -0
- package/lib/providers/cursor-model-settings.js +51 -0
- package/lib/providers/cursor-usage.d.ts +9 -0
- package/lib/providers/cursor-usage.js +112 -0
- package/lib/providers/external-usage-controller.d.ts +19 -0
- package/lib/providers/external-usage-controller.js +32 -0
- package/lib/providers/external-usage.d.ts +8 -0
- package/lib/providers/external-usage.js +82 -0
- package/lib/providers/grok.d.ts +205 -0
- package/lib/providers/grok.js +713 -0
- package/lib/providers/image-pool.d.ts +30 -0
- package/lib/providers/image-pool.js +89 -0
- package/lib/providers/npm-cli-version.d.ts +56 -0
- package/lib/providers/npm-cli-version.js +98 -0
- package/lib/providers/pool-family.d.ts +56 -0
- package/lib/providers/pool-family.js +45 -0
- package/lib/providers/pool-health.d.ts +74 -0
- package/lib/providers/pool-health.js +150 -0
- package/lib/providers/pool-usage.d.ts +86 -0
- package/lib/providers/pool-usage.js +223 -0
- package/lib/providers/pool.d.ts +108 -0
- package/lib/providers/pool.js +368 -0
- package/lib/providers/rate-limit.d.ts +192 -0
- package/lib/providers/rate-limit.js +338 -0
- package/lib/tools/image-generate.d.ts +128 -0
- package/lib/tools/image-generate.js +438 -0
- package/lib/tools/registration.d.ts +15 -0
- package/lib/tools/registration.js +24 -0
- package/lib/tools/video-generate.d.ts +89 -0
- package/lib/tools/video-generate.js +257 -0
- package/lib/tools/x-search.d.ts +58 -0
- package/lib/tools/x-search.js +197 -0
- package/lib/translate/anthropic.d.ts +161 -0
- package/lib/translate/anthropic.js +515 -0
- package/lib/translate/antigravity-schema.d.ts +4 -0
- package/lib/translate/antigravity-schema.js +145 -0
- package/lib/translate/antigravity-thinking.d.ts +5 -0
- package/lib/translate/antigravity-thinking.js +44 -0
- package/lib/translate/antigravity.d.ts +114 -0
- package/lib/translate/antigravity.js +366 -0
- package/lib/translate/chat-completions.d.ts +120 -0
- package/lib/translate/chat-completions.js +382 -0
- package/lib/translate/resolved.d.ts +80 -0
- package/lib/translate/resolved.js +118 -0
- package/lib/translate/responses.d.ts +179 -0
- package/lib/translate/responses.js +408 -0
- package/lib/translate/sse.d.ts +21 -0
- package/lib/translate/sse.js +56 -0
- package/package.json +114 -0
- package/vendor/cursor/LICENSE +21 -0
- package/vendor/cursor/index.d.ts +18 -0
- package/vendor/cursor/index.js +3574 -0
- package/vendor/cursor/proto.js +254 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
2
|
+
import { CursorAdapter } from 'dsh-subscriptions/cursor-transport';
|
|
3
|
+
/**
|
|
4
|
+
* The imported Cursor transport still reads tool results as user-role content
|
|
5
|
+
* blocks. DSH 0.1.7 carries them as first-class role=tool messages instead.
|
|
6
|
+
*/
|
|
7
|
+
export declare function projectCursorMessages(messages: GenerateOptions['messages']): unknown[];
|
|
8
|
+
export declare class CursorCompatAdapter extends CursorAdapter {
|
|
9
|
+
private readonly visibleModels;
|
|
10
|
+
constructor(options: ConstructorParameters<typeof CursorAdapter>[0] & {
|
|
11
|
+
visibleModels?: () => readonly string[] | undefined;
|
|
12
|
+
});
|
|
13
|
+
listModels(provider: string): Promise<import("@deepseek-ai/dsh-llm").LlmModelInfo[]>;
|
|
14
|
+
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
type: 'tool-result',
|
|
13
|
+
toolCallId: message.toolCallId,
|
|
14
|
+
isError: message.isError,
|
|
15
|
+
content: message.content,
|
|
16
|
+
}],
|
|
17
|
+
}
|
|
18
|
+
: message);
|
|
19
|
+
}
|
|
20
|
+
export class CursorCompatAdapter extends CursorAdapter {
|
|
21
|
+
visibleModels;
|
|
22
|
+
constructor(options) {
|
|
23
|
+
super(options);
|
|
24
|
+
this.visibleModels = options.visibleModels;
|
|
25
|
+
}
|
|
26
|
+
async listModels(provider) {
|
|
27
|
+
const models = await super.listModels(provider);
|
|
28
|
+
const visible = this.visibleModels?.();
|
|
29
|
+
return visible === undefined ? models : models.filter(model => visible.includes(model.id));
|
|
30
|
+
}
|
|
31
|
+
stream(options) {
|
|
32
|
+
return super.stream({
|
|
33
|
+
...options,
|
|
34
|
+
// Existing DSH sessions may still hold this retired selection.
|
|
35
|
+
model: options.model === 'composer-2' ? 'composer-2.5' : options.model,
|
|
36
|
+
messages: projectCursorMessages(options.messages),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
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
|
+
export const CURSOR_CREDENTIAL_REF = credentialRef('CURSOR_SUBSCRIPTION_OAUTH');
|
|
5
|
+
const API_ORIGIN = 'https://api2.cursor.sh';
|
|
6
|
+
const REFRESH_AHEAD_MS = 5 * 60_000;
|
|
7
|
+
function parseCredential(value) {
|
|
8
|
+
if (value === undefined || value.length === 0)
|
|
9
|
+
return undefined;
|
|
10
|
+
const parsed = JSON.parse(value);
|
|
11
|
+
if (parsed === null || typeof parsed !== 'object')
|
|
12
|
+
throw new Error('Cursor credential is malformed');
|
|
13
|
+
const item = parsed;
|
|
14
|
+
if (item.type !== 'oauth' || typeof item.access !== 'string' || item.access.length === 0
|
|
15
|
+
|| typeof item.refresh !== 'string' || typeof item.expires !== 'number' || !Number.isFinite(item.expires)) {
|
|
16
|
+
throw new Error('Cursor credential is malformed');
|
|
17
|
+
}
|
|
18
|
+
return { type: 'oauth', access: item.access, refresh: item.refresh, expires: item.expires };
|
|
19
|
+
}
|
|
20
|
+
function tokenExpiry(token, now) {
|
|
21
|
+
try {
|
|
22
|
+
const payload = token.split('.')[1];
|
|
23
|
+
if (payload === undefined)
|
|
24
|
+
return now + 24 * 60 * 60_000;
|
|
25
|
+
const data = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
|
|
26
|
+
const exp = data?.exp;
|
|
27
|
+
if (typeof exp === 'number' && Number.isFinite(exp))
|
|
28
|
+
return exp * 1000;
|
|
29
|
+
}
|
|
30
|
+
catch { /* JWTs without exp still receive a bounded lifetime. */ }
|
|
31
|
+
return now + 24 * 60 * 60_000;
|
|
32
|
+
}
|
|
33
|
+
function abortableDelay(ms, signal) {
|
|
34
|
+
if (signal.aborted)
|
|
35
|
+
return Promise.reject(signal.reason);
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const timer = setTimeout(() => { signal.removeEventListener('abort', aborted); resolve(); }, ms);
|
|
38
|
+
function aborted() { clearTimeout(timer); reject(signal.reason); }
|
|
39
|
+
signal.addEventListener('abort', aborted, { once: true });
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export class CursorAuth {
|
|
43
|
+
credentials;
|
|
44
|
+
http;
|
|
45
|
+
now;
|
|
46
|
+
loginTask;
|
|
47
|
+
error;
|
|
48
|
+
refreshTask;
|
|
49
|
+
constructor(credentials, http = fetch, now = Date.now) {
|
|
50
|
+
this.credentials = credentials;
|
|
51
|
+
this.http = http;
|
|
52
|
+
this.now = now;
|
|
53
|
+
}
|
|
54
|
+
async read() {
|
|
55
|
+
return parseCredential((await this.credentials.resolve(CURSOR_CREDENTIAL_REF))?.value);
|
|
56
|
+
}
|
|
57
|
+
async status() {
|
|
58
|
+
const current = await this.read();
|
|
59
|
+
return {
|
|
60
|
+
authenticated: current !== undefined,
|
|
61
|
+
busy: this.loginTask !== undefined,
|
|
62
|
+
...(current === undefined ? {} : { expiresAt: current.expires }),
|
|
63
|
+
...(this.error === undefined ? {} : { error: this.error }),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async accessToken({ signal } = {}) {
|
|
67
|
+
signal?.throwIfAborted();
|
|
68
|
+
const current = await this.read();
|
|
69
|
+
if (current === undefined)
|
|
70
|
+
throw new Error('Cursor is not signed in');
|
|
71
|
+
if (current.expires - this.now() > REFRESH_AHEAD_MS)
|
|
72
|
+
return current.access;
|
|
73
|
+
if (current.refresh.length === 0)
|
|
74
|
+
throw new Error('Cursor sign-in needs to be renewed');
|
|
75
|
+
if (this.refreshTask === undefined) {
|
|
76
|
+
const pending = this.refresh(current, signal);
|
|
77
|
+
this.refreshTask = pending;
|
|
78
|
+
void pending.finally(() => { if (this.refreshTask === pending)
|
|
79
|
+
this.refreshTask = undefined; }).catch(() => undefined);
|
|
80
|
+
}
|
|
81
|
+
return (await this.refreshTask).access;
|
|
82
|
+
}
|
|
83
|
+
async refresh(current, signal) {
|
|
84
|
+
const response = await this.http(`${API_ORIGIN}/auth/exchange_user_api_key`, {
|
|
85
|
+
method: 'POST', redirect: 'error',
|
|
86
|
+
headers: { authorization: `Bearer ${current.refresh}`, accept: 'application/json', 'content-type': 'application/json' },
|
|
87
|
+
body: '{}',
|
|
88
|
+
...(signal === undefined ? {} : { signal }),
|
|
89
|
+
});
|
|
90
|
+
if (!response.ok)
|
|
91
|
+
throw new Error(`Cursor token refresh failed (HTTP ${response.status})`);
|
|
92
|
+
const body = await response.json();
|
|
93
|
+
if (typeof body.accessToken !== 'string' || body.accessToken.length === 0) {
|
|
94
|
+
throw new Error('Cursor token refresh returned no access token');
|
|
95
|
+
}
|
|
96
|
+
const next = {
|
|
97
|
+
type: 'oauth', access: body.accessToken,
|
|
98
|
+
refresh: typeof body.refreshToken === 'string' && body.refreshToken.length > 0 ? body.refreshToken : current.refresh,
|
|
99
|
+
expires: tokenExpiry(body.accessToken, this.now()),
|
|
100
|
+
};
|
|
101
|
+
const latest = await this.read();
|
|
102
|
+
if (latest === undefined)
|
|
103
|
+
throw new Error('Cursor signed out during token refresh');
|
|
104
|
+
if (latest.refresh !== current.refresh)
|
|
105
|
+
return latest;
|
|
106
|
+
await this.credentials.set(CURSOR_CREDENTIAL_REF, JSON.stringify(next));
|
|
107
|
+
return next;
|
|
108
|
+
}
|
|
109
|
+
/** Return a Cursor browser URL immediately; polling continues on the host. */
|
|
110
|
+
async login() {
|
|
111
|
+
await this.cancel();
|
|
112
|
+
this.error = undefined;
|
|
113
|
+
const verifier = randomBytes(96).toString('base64url');
|
|
114
|
+
const challenge = createHash('sha256').update(verifier).digest('base64url');
|
|
115
|
+
const uuid = randomUUID();
|
|
116
|
+
const url = new URL('https://cursor.com/loginDeepControl');
|
|
117
|
+
url.search = new URLSearchParams({ challenge, uuid, mode: 'login', redirectTarget: 'cli' }).toString();
|
|
118
|
+
const abort = new AbortController();
|
|
119
|
+
const promise = this.pollLogin(uuid, verifier, abort.signal)
|
|
120
|
+
.catch(error => { if (!abort.signal.aborted)
|
|
121
|
+
this.error = error instanceof Error ? error.message : String(error); })
|
|
122
|
+
.finally(() => { if (this.loginTask?.abort === abort)
|
|
123
|
+
this.loginTask = undefined; });
|
|
124
|
+
this.loginTask = { abort, promise };
|
|
125
|
+
return { authorizeUrl: url.href };
|
|
126
|
+
}
|
|
127
|
+
async pollLogin(uuid, verifier, signal) {
|
|
128
|
+
let delay = 1000;
|
|
129
|
+
for (let attempt = 0; attempt < 150; attempt++) {
|
|
130
|
+
await abortableDelay(delay, signal);
|
|
131
|
+
const url = `${API_ORIGIN}/auth/poll?uuid=${encodeURIComponent(uuid)}&verifier=${encodeURIComponent(verifier)}`;
|
|
132
|
+
const response = await this.http(url, { headers: { accept: 'application/json' }, redirect: 'error', signal });
|
|
133
|
+
if (response.status === 404) {
|
|
134
|
+
delay = Math.min(Math.ceil(delay * 1.2), 10_000);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (!response.ok)
|
|
138
|
+
throw new Error(`Cursor login failed (HTTP ${response.status})`);
|
|
139
|
+
const body = await response.json();
|
|
140
|
+
signal.throwIfAborted();
|
|
141
|
+
if (typeof body.accessToken !== 'string' || body.accessToken.length === 0) {
|
|
142
|
+
throw new Error('Cursor login returned no access token');
|
|
143
|
+
}
|
|
144
|
+
if (typeof body.refreshToken !== 'string' || body.refreshToken.length === 0) {
|
|
145
|
+
throw new Error('Cursor login returned no refresh token');
|
|
146
|
+
}
|
|
147
|
+
const value = {
|
|
148
|
+
type: 'oauth', access: body.accessToken,
|
|
149
|
+
refresh: body.refreshToken,
|
|
150
|
+
expires: tokenExpiry(body.accessToken, this.now()),
|
|
151
|
+
};
|
|
152
|
+
await this.credentials.set(CURSOR_CREDENTIAL_REF, JSON.stringify(value));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
throw new Error('Cursor login timed out');
|
|
156
|
+
}
|
|
157
|
+
async cancel() {
|
|
158
|
+
const task = this.loginTask;
|
|
159
|
+
task?.abort.abort(new Error('Cursor login cancelled'));
|
|
160
|
+
await task?.promise;
|
|
161
|
+
if (this.loginTask === task)
|
|
162
|
+
this.loginTask = undefined;
|
|
163
|
+
this.error = undefined;
|
|
164
|
+
}
|
|
165
|
+
async logout() {
|
|
166
|
+
await this.cancel();
|
|
167
|
+
await this.credentials.unset(CURSOR_CREDENTIAL_REF);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
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) || models.some(id => typeof id !== 'string' || !id.trim())) {
|
|
13
|
+
throw new Error('visibleModels must be an array of model ids');
|
|
14
|
+
}
|
|
15
|
+
return { visibleModels: [...new Set(models)] };
|
|
16
|
+
}
|
|
17
|
+
/** Durable visibility preference; account tokens remain in DSH credentials. */
|
|
18
|
+
export class CursorModelSettingsStore {
|
|
19
|
+
path;
|
|
20
|
+
current = {};
|
|
21
|
+
writes = Promise.resolve();
|
|
22
|
+
constructor(path = dshHomePath('plugins', 'subscriptions', 'cursor-model-settings.json')) {
|
|
23
|
+
this.path = path;
|
|
24
|
+
try {
|
|
25
|
+
this.current = validateCursorModelSettings(JSON.parse(readFileSync(path, 'utf8')));
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error.code !== 'ENOENT')
|
|
29
|
+
throw new Error(`Cannot read Cursor model settings at ${path}`, { cause: error });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
get() { return structuredClone(this.current); }
|
|
33
|
+
visibleModels() { return this.current.visibleModels; }
|
|
34
|
+
set(input) {
|
|
35
|
+
const validated = validateCursorModelSettings(input);
|
|
36
|
+
const run = this.writes.then(async () => {
|
|
37
|
+
await mkdir(dirname(this.path), { recursive: true });
|
|
38
|
+
const temporary = `${this.path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
39
|
+
try {
|
|
40
|
+
await writeFile(temporary, JSON.stringify(validated, null, 2), { mode: 0o600 });
|
|
41
|
+
await rename(temporary, this.path);
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
await rm(temporary, { force: true });
|
|
45
|
+
}
|
|
46
|
+
this.current = validated;
|
|
47
|
+
});
|
|
48
|
+
this.writes = run.catch(() => undefined);
|
|
49
|
+
return run;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
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>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/** Cursor individual-account quota projection from its dashboard endpoints. */
|
|
2
|
+
const ORIGIN = 'https://cursor.com';
|
|
3
|
+
function record(value) {
|
|
4
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
5
|
+
? value
|
|
6
|
+
: undefined;
|
|
7
|
+
}
|
|
8
|
+
function number(value) {
|
|
9
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
10
|
+
return value;
|
|
11
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
12
|
+
const parsed = Number(value);
|
|
13
|
+
if (Number.isFinite(parsed))
|
|
14
|
+
return parsed;
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
/** Cursor's WorkOS user id from the access JWT, without the identity prefix. */
|
|
19
|
+
export function cursorUserId(token) {
|
|
20
|
+
try {
|
|
21
|
+
const parts = token.split('.');
|
|
22
|
+
if (parts.length !== 3 || !parts[1])
|
|
23
|
+
return undefined;
|
|
24
|
+
const claims = record(JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8')));
|
|
25
|
+
const raw = claims?.sub;
|
|
26
|
+
if (typeof raw !== 'string' || raw.length === 0)
|
|
27
|
+
return undefined;
|
|
28
|
+
const id = raw.includes('|') ? raw.split('|').at(-1) : raw;
|
|
29
|
+
return id !== undefined && /^[A-Za-z0-9_-]+$/.test(id) ? id : undefined;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function dashboardJson(url, cookie, http, signal) {
|
|
36
|
+
const response = await http(url, {
|
|
37
|
+
headers: {
|
|
38
|
+
accept: 'application/json',
|
|
39
|
+
cookie,
|
|
40
|
+
origin: ORIGIN,
|
|
41
|
+
referer: `${ORIGIN}/dashboard`,
|
|
42
|
+
},
|
|
43
|
+
...(signal === undefined ? {} : { signal }),
|
|
44
|
+
});
|
|
45
|
+
if (!response.ok)
|
|
46
|
+
throw new Error(`Cursor usage endpoint returned HTTP ${response.status}`);
|
|
47
|
+
return response.json();
|
|
48
|
+
}
|
|
49
|
+
function legacyRequestWindow(body, resetsAt) {
|
|
50
|
+
const usage = record(body);
|
|
51
|
+
if (usage === undefined)
|
|
52
|
+
return undefined;
|
|
53
|
+
const gpt4 = record(usage['gpt-4']);
|
|
54
|
+
const candidate = gpt4 !== undefined && number(gpt4.numRequests) !== undefined ? gpt4 : Object.values(usage)
|
|
55
|
+
.map(record)
|
|
56
|
+
.filter((value) => value !== undefined)
|
|
57
|
+
.sort((a, b) => (number(b.maxRequestUsage) ?? 0) - (number(a.maxRequestUsage) ?? 0))[0];
|
|
58
|
+
const used = number(candidate?.numRequests);
|
|
59
|
+
const limit = number(candidate?.maxRequestUsage);
|
|
60
|
+
if (used === undefined || limit === undefined || limit <= 0)
|
|
61
|
+
return undefined;
|
|
62
|
+
return {
|
|
63
|
+
kind: 'other', scope: 'Included requests',
|
|
64
|
+
usedPercent: Math.max(0, Math.min(100, used / limit * 100)),
|
|
65
|
+
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Read Cursor's individual dashboard quota using an OAuth access token.
|
|
70
|
+
* The dashboard cookie is request-local; no token enters ProviderUsage.
|
|
71
|
+
*/
|
|
72
|
+
export async function fetchCursorUsage(accessToken, http = fetch, signal) {
|
|
73
|
+
const userId = cursorUserId(accessToken);
|
|
74
|
+
if (userId === undefined)
|
|
75
|
+
throw new Error('Cursor session token has no usable user identity');
|
|
76
|
+
const cookie = `WorkosCursorSessionToken=${userId}::${accessToken}`;
|
|
77
|
+
const [summaryResult, legacyResult] = await Promise.allSettled([
|
|
78
|
+
dashboardJson(`${ORIGIN}/api/usage-summary`, cookie, http, signal),
|
|
79
|
+
dashboardJson(`${ORIGIN}/api/usage?user=${encodeURIComponent(userId)}`, cookie, http, signal),
|
|
80
|
+
]);
|
|
81
|
+
if (summaryResult.status === 'rejected' && legacyResult.status === 'rejected') {
|
|
82
|
+
throw summaryResult.reason instanceof Error ? summaryResult.reason : new Error('Cursor usage is unavailable');
|
|
83
|
+
}
|
|
84
|
+
const summary = summaryResult.status === 'fulfilled' ? record(summaryResult.value) : undefined;
|
|
85
|
+
const plan = record(record(summary?.individualUsage)?.plan) ?? record(record(summary?.teamUsage)?.plan);
|
|
86
|
+
const resetValue = summary?.billingCycleEnd;
|
|
87
|
+
const parsedReset = typeof resetValue === 'string' ? Date.parse(resetValue) : NaN;
|
|
88
|
+
const resetsAt = Number.isFinite(parsedReset) ? parsedReset : undefined;
|
|
89
|
+
const windows = [];
|
|
90
|
+
for (const [field, scope] of [
|
|
91
|
+
['totalPercentUsed', 'Included'],
|
|
92
|
+
['autoPercentUsed', 'Cursor Models'],
|
|
93
|
+
['apiPercentUsed', 'Other Models'],
|
|
94
|
+
]) {
|
|
95
|
+
const usedPercent = number(plan?.[field]);
|
|
96
|
+
if (usedPercent === undefined || usedPercent < 0 || usedPercent > 100)
|
|
97
|
+
continue;
|
|
98
|
+
windows.push({ kind: 'other', scope, usedPercent, ...(resetsAt === undefined ? {} : { resetsAt }) });
|
|
99
|
+
}
|
|
100
|
+
const legacy = legacyRequestWindow(legacyResult.status === 'fulfilled' ? legacyResult.value : undefined, resetsAt);
|
|
101
|
+
if (legacy !== undefined)
|
|
102
|
+
windows.push(legacy);
|
|
103
|
+
if (windows.length === 0 && summary?.isUnlimited !== true) {
|
|
104
|
+
throw new Error('Cursor usage response has no quota windows');
|
|
105
|
+
}
|
|
106
|
+
const membershipType = summary?.membershipType;
|
|
107
|
+
return {
|
|
108
|
+
supported: true,
|
|
109
|
+
windows,
|
|
110
|
+
...(typeof membershipType === 'string' && membershipType.length > 0 ? { plan: membershipType } : {}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ProviderUsage } from './common.js';
|
|
2
|
+
/** Usage-only sources whose model routes are supplied by the DSH base install. */
|
|
3
|
+
export type ExternalUsageSource = 'opencode-go' | 'kimi-code';
|
|
4
|
+
export declare const EXTERNAL_USAGE_SOURCES: readonly ExternalUsageSource[];
|
|
5
|
+
export interface ExternalUsageStatus {
|
|
6
|
+
configured: boolean;
|
|
7
|
+
}
|
|
8
|
+
type ResolveCredential = (name: string) => Promise<{
|
|
9
|
+
value: string;
|
|
10
|
+
} | undefined>;
|
|
11
|
+
export declare class ExternalUsageController {
|
|
12
|
+
private readonly resolveCredential;
|
|
13
|
+
private readonly http;
|
|
14
|
+
private readonly refs;
|
|
15
|
+
constructor(resolveCredential: ResolveCredential, http?: typeof fetch, refs?: Partial<Record<ExternalUsageSource, string>>);
|
|
16
|
+
status(): Promise<Record<ExternalUsageSource, ExternalUsageStatus>>;
|
|
17
|
+
usage(source: ExternalUsageSource, signal?: AbortSignal): Promise<ProviderUsage>;
|
|
18
|
+
}
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { fetchKimiCodeUsage, fetchOpenCodeGoUsage } from './external-usage.js';
|
|
2
|
+
export const EXTERNAL_USAGE_SOURCES = ['opencode-go', 'kimi-code'];
|
|
3
|
+
const DEFAULT_REFS = {
|
|
4
|
+
'opencode-go': 'OPENCODE_GO_API_KEY',
|
|
5
|
+
'kimi-code': 'KIMI_CODING_API_KEY',
|
|
6
|
+
};
|
|
7
|
+
export class ExternalUsageController {
|
|
8
|
+
resolveCredential;
|
|
9
|
+
http;
|
|
10
|
+
refs;
|
|
11
|
+
constructor(resolveCredential, http = fetch, refs = {}) {
|
|
12
|
+
this.resolveCredential = resolveCredential;
|
|
13
|
+
this.http = http;
|
|
14
|
+
this.refs = { ...DEFAULT_REFS, ...refs };
|
|
15
|
+
}
|
|
16
|
+
async status() {
|
|
17
|
+
const [go, kimi] = await Promise.all(EXTERNAL_USAGE_SOURCES.map(async (source) => {
|
|
18
|
+
const value = await this.resolveCredential(this.refs[source]);
|
|
19
|
+
return typeof value?.value === 'string' && value.value.length > 0;
|
|
20
|
+
}));
|
|
21
|
+
return { 'opencode-go': { configured: go ?? false }, 'kimi-code': { configured: kimi ?? false } };
|
|
22
|
+
}
|
|
23
|
+
async usage(source, signal) {
|
|
24
|
+
const credential = await this.resolveCredential(this.refs[source]);
|
|
25
|
+
if (credential?.value === undefined || credential.value.length === 0) {
|
|
26
|
+
throw new Error(`${source} API key is not configured`);
|
|
27
|
+
}
|
|
28
|
+
return source === 'opencode-go'
|
|
29
|
+
? fetchOpenCodeGoUsage(credential.value, this.http, signal)
|
|
30
|
+
: fetchKimiCodeUsage(credential.value, this.http, signal);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Usage readers for subscriptions whose model adapters are built into DSH. */
|
|
2
|
+
import type { ProviderUsage } from './common.js';
|
|
3
|
+
type HttpFetch = typeof fetch;
|
|
4
|
+
/** Read the three account-wide OpenCode Go windows from its Go gateway. */
|
|
5
|
+
export declare function fetchOpenCodeGoUsage(apiKey: string, http?: HttpFetch, signal?: AbortSignal): Promise<ProviderUsage>;
|
|
6
|
+
/** Read the Kimi Code plan windows using the same API as the Kimi Code CLI. */
|
|
7
|
+
export declare function fetchKimiCodeUsage(apiKey: string, http?: HttpFetch, signal?: AbortSignal): Promise<ProviderUsage>;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** Usage readers for subscriptions whose model adapters are built into DSH. */
|
|
2
|
+
function record(value) {
|
|
3
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
4
|
+
? value
|
|
5
|
+
: undefined;
|
|
6
|
+
}
|
|
7
|
+
function percent(value) {
|
|
8
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 100
|
|
9
|
+
? value
|
|
10
|
+
: undefined;
|
|
11
|
+
}
|
|
12
|
+
function resetTime(value) {
|
|
13
|
+
if (typeof value !== 'string')
|
|
14
|
+
return undefined;
|
|
15
|
+
const parsed = Date.parse(value);
|
|
16
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
17
|
+
}
|
|
18
|
+
async function getUsage(url, apiKey, http, signal) {
|
|
19
|
+
const response = await http(url, {
|
|
20
|
+
headers: { accept: 'application/json', authorization: `Bearer ${apiKey}` },
|
|
21
|
+
...(signal === undefined ? {} : { signal }),
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok)
|
|
24
|
+
throw new Error(`Usage endpoint returned HTTP ${response.status}`);
|
|
25
|
+
return response.json();
|
|
26
|
+
}
|
|
27
|
+
/** Read the three account-wide OpenCode Go windows from its Go gateway. */
|
|
28
|
+
export async function fetchOpenCodeGoUsage(apiKey, http = fetch, signal) {
|
|
29
|
+
const body = record(await getUsage('https://opencode.ai/zen/go/v1/usage', apiKey, http, signal));
|
|
30
|
+
const usage = record(body?.usage);
|
|
31
|
+
if (usage === undefined)
|
|
32
|
+
throw new Error('OpenCode Go usage response has no usage object');
|
|
33
|
+
const windows = [];
|
|
34
|
+
for (const [field, kind, scope] of [
|
|
35
|
+
['rolling', 'session', undefined],
|
|
36
|
+
['weekly', 'weekly', undefined],
|
|
37
|
+
['monthly', 'other', 'Monthly'],
|
|
38
|
+
]) {
|
|
39
|
+
const value = record(usage[field]);
|
|
40
|
+
const usedPercent = percent(value?.percent);
|
|
41
|
+
if (usedPercent === undefined)
|
|
42
|
+
continue;
|
|
43
|
+
const resetsAt = resetTime(value?.resetsAt);
|
|
44
|
+
windows.push({
|
|
45
|
+
kind,
|
|
46
|
+
...(scope === undefined ? {} : { scope }),
|
|
47
|
+
usedPercent,
|
|
48
|
+
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (windows.length === 0)
|
|
52
|
+
throw new Error('OpenCode Go usage response has no valid windows');
|
|
53
|
+
return { supported: true, windows, plan: 'OpenCode Go' };
|
|
54
|
+
}
|
|
55
|
+
/** Read the Kimi Code plan windows using the same API as the Kimi Code CLI. */
|
|
56
|
+
export async function fetchKimiCodeUsage(apiKey, http = fetch, signal) {
|
|
57
|
+
const body = record(await getUsage('https://api.kimi.com/coding/v1/usages', apiKey, http, signal));
|
|
58
|
+
if (body === undefined)
|
|
59
|
+
throw new Error('Kimi Code usage response is not an object');
|
|
60
|
+
const usages = record(body.usages);
|
|
61
|
+
if (usages === undefined)
|
|
62
|
+
throw new Error('Kimi Code usage response has no usages object');
|
|
63
|
+
const windows = [];
|
|
64
|
+
for (const [field, kind, scope] of [
|
|
65
|
+
['limit_5h', 'session', undefined],
|
|
66
|
+
['limit_7d', 'weekly', undefined],
|
|
67
|
+
['limit_month_total', 'other', 'Monthly'],
|
|
68
|
+
]) {
|
|
69
|
+
const value = record(usages[field]);
|
|
70
|
+
const ratio = value?.used_ratio ?? value?.usedRatio;
|
|
71
|
+
if (typeof ratio !== 'number' || !Number.isFinite(ratio) || ratio < 0 || ratio > 1)
|
|
72
|
+
continue;
|
|
73
|
+
const resetsAt = resetTime(value?.reset_time ?? value?.resetAt);
|
|
74
|
+
windows.push({
|
|
75
|
+
kind,
|
|
76
|
+
...(scope === undefined ? {} : { scope }),
|
|
77
|
+
usedPercent: ratio * 100,
|
|
78
|
+
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return { supported: true, windows, plan: 'Kimi Code' };
|
|
82
|
+
}
|