dsh-subscriptions 0.2.0 → 0.2.3

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 (105) hide show
  1. package/README.md +220 -180
  2. package/lib/auth/claude-code-creds.js +31 -9
  3. package/lib/auth/device-flow.js +16 -9
  4. package/lib/auth/oauth-flow.js +13 -10
  5. package/lib/auth/rpc.d.ts +8 -4
  6. package/lib/auth/rpc.js +89 -25
  7. package/lib/auth/store.js +80 -31
  8. package/lib/client/CursorCard.d.ts +1 -1
  9. package/lib/client/CursorCard.js +50 -16
  10. package/lib/client/ExternalUsageCards.d.ts +1 -1
  11. package/lib/client/ExternalUsageCards.js +37 -16
  12. package/lib/client/ImageGallery.d.ts +2 -2
  13. package/lib/client/ImageGallery.js +92 -33
  14. package/lib/client/ImageGenerateToolview.js +50 -14
  15. package/lib/client/OllamaCard.d.ts +1 -1
  16. package/lib/client/OllamaCard.js +26 -8
  17. package/lib/client/OllamaSettingsManager.js +113 -30
  18. package/lib/client/ProviderAccountManager.d.ts +1 -1
  19. package/lib/client/ProviderAccountManager.js +164 -49
  20. package/lib/client/ProviderModelEditor.js +137 -34
  21. package/lib/client/SpeedSelect.d.ts +62 -14
  22. package/lib/client/SpeedSelect.js +261 -49
  23. package/lib/client/SubscriptionUsageBadge.d.ts +4 -10
  24. package/lib/client/SubscriptionUsageBadge.js +300 -112
  25. package/lib/client/SubscriptionsSection.d.ts +2 -0
  26. package/lib/client/SubscriptionsSection.js +265 -85
  27. package/lib/client/UsageBadgeDisplaySetting.js +35 -12
  28. package/lib/client/UsageMeter.d.ts +1 -1
  29. package/lib/client/UsageMeter.js +62 -24
  30. package/lib/client/VideoGenerateToolview.js +69 -17
  31. package/lib/client/account-preferences.d.ts +8 -0
  32. package/lib/client/account-preferences.js +20 -5
  33. package/lib/client/host-contract.d.ts +11 -2
  34. package/lib/client/host-contract.js +20 -3
  35. package/lib/client/index.d.ts +5 -5
  36. package/lib/client/index.js +34 -12
  37. package/lib/client/locales.d.ts +22 -0
  38. package/lib/client/locales.js +73 -51
  39. package/lib/client/model-filter.d.ts +37 -0
  40. package/lib/client/model-filter.js +45 -0
  41. package/lib/client/ollama-draft.js +21 -12
  42. package/lib/client/reset-action.js +6 -4
  43. package/lib/client/reset-credits-view.d.ts +1 -1
  44. package/lib/client/reset-credits-view.js +66 -17
  45. package/lib/client/subscription-card-styles.js +94 -24
  46. package/lib/client/usage-color-preferences.js +4 -2
  47. package/lib/client/usage-pace.js +30 -12
  48. package/lib/client/usage-window-filter.d.ts +7 -0
  49. package/lib/client/usage-window-filter.js +12 -0
  50. package/lib/client.js +480 -136
  51. package/lib/client.js.map +1 -1
  52. package/lib/http.js +1 -1
  53. package/lib/index.d.ts +3 -3
  54. package/lib/index.js +167 -76
  55. package/lib/model-defaults.js +4 -2
  56. package/lib/provider-settings.d.ts +17 -0
  57. package/lib/provider-settings.js +79 -16
  58. package/lib/providers/account-preferences.js +51 -22
  59. package/lib/providers/accounts.js +15 -9
  60. package/lib/providers/antigravity-oauth-client.js +2 -2
  61. package/lib/providers/antigravity.js +141 -68
  62. package/lib/providers/catalog-store.js +120 -41
  63. package/lib/providers/claude-cli-version.js +3 -3
  64. package/lib/providers/claude.d.ts +9 -2
  65. package/lib/providers/claude.js +153 -90
  66. package/lib/providers/codex-client-version.js +3 -3
  67. package/lib/providers/codex-search.js +40 -13
  68. package/lib/providers/codex.js +196 -112
  69. package/lib/providers/common.js +71 -39
  70. package/lib/providers/copilot.js +143 -82
  71. package/lib/providers/cursor-adapter.js +42 -18
  72. package/lib/providers/cursor-auth.d.ts +1 -1
  73. package/lib/providers/cursor-auth.js +69 -24
  74. package/lib/providers/cursor-model-settings.js +14 -5
  75. package/lib/providers/cursor-usage.js +27 -13
  76. package/lib/providers/external-usage-controller.js +14 -5
  77. package/lib/providers/external-usage.js +59 -23
  78. package/lib/providers/grok.js +152 -89
  79. package/lib/providers/image-pool.js +29 -11
  80. package/lib/providers/minimax-usage.js +31 -11
  81. package/lib/providers/npm-cli-version.js +10 -3
  82. package/lib/providers/ollama-settings.js +24 -8
  83. package/lib/providers/ollama-web.js +29 -11
  84. package/lib/providers/ollama.js +129 -66
  85. package/lib/providers/pool-family.js +11 -3
  86. package/lib/providers/pool-health.js +28 -8
  87. package/lib/providers/pool-usage.js +16 -9
  88. package/lib/providers/pool.d.ts +8 -0
  89. package/lib/providers/pool.js +81 -42
  90. package/lib/providers/rate-limit.js +8 -2
  91. package/lib/providers/replay.js +40 -17
  92. package/lib/providers/reset-redemption.js +18 -6
  93. package/lib/tools/image-generate.js +123 -65
  94. package/lib/tools/registration.js +5 -2
  95. package/lib/tools/video-generate.js +51 -31
  96. package/lib/tools/x-search.js +50 -22
  97. package/lib/translate/anthropic.js +85 -37
  98. package/lib/translate/antigravity-schema.js +71 -28
  99. package/lib/translate/antigravity-thinking.js +22 -9
  100. package/lib/translate/antigravity.js +151 -52
  101. package/lib/translate/chat-completions.js +67 -23
  102. package/lib/translate/resolved.js +55 -21
  103. package/lib/translate/responses.js +69 -29
  104. package/lib/translate/sse.js +4 -1
  105. package/package.json +27 -4
@@ -18,11 +18,20 @@ async function parseCredential(value) {
18
18
  if (parsed === null || typeof parsed !== 'object')
19
19
  throw new Error('Cursor credential is malformed');
20
20
  const item = parsed;
21
- if (item.type !== 'oauth' || typeof item.access !== 'string' || item.access.length === 0
22
- || typeof item.refresh !== 'string' || typeof item.expires !== 'number' || !Number.isFinite(item.expires)) {
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)) {
23
27
  throw new Error('Cursor credential is malformed');
24
28
  }
25
- return { type: 'oauth', access: item.access, refresh: item.refresh, expires: item.expires };
29
+ return {
30
+ type: 'oauth',
31
+ access: item.access,
32
+ refresh: item.refresh,
33
+ expires: item.expires,
34
+ };
26
35
  }
27
36
  function tokenExpiry(token, now) {
28
37
  try {
@@ -34,15 +43,23 @@ function tokenExpiry(token, now) {
34
43
  if (typeof exp === 'number' && Number.isFinite(exp))
35
44
  return exp * 1000;
36
45
  }
37
- catch { /* JWTs without exp still receive a bounded lifetime. */ }
46
+ catch {
47
+ /* JWTs without exp still receive a bounded lifetime. */
48
+ }
38
49
  return now + 24 * 60 * 60_000;
39
50
  }
40
51
  function abortableDelay(ms, signal) {
41
52
  if (signal.aborted)
42
53
  return Promise.reject(signal.reason);
43
54
  return new Promise((resolve, reject) => {
44
- const timer = setTimeout(() => { signal.removeEventListener('abort', aborted); resolve(); }, ms);
45
- function aborted() { clearTimeout(timer); reject(signal.reason); }
55
+ const timer = setTimeout(() => {
56
+ signal.removeEventListener('abort', aborted);
57
+ resolve();
58
+ }, ms);
59
+ function aborted() {
60
+ clearTimeout(timer);
61
+ reject(signal.reason);
62
+ }
46
63
  signal.addEventListener('abort', aborted, { once: true });
47
64
  });
48
65
  }
@@ -70,7 +87,7 @@ export class CursorAuth {
70
87
  ...(this.error === undefined ? {} : { error: this.error }),
71
88
  };
72
89
  }
73
- async accessToken({ signal } = {}) {
90
+ async accessToken({ signal, } = {}) {
74
91
  signal?.throwIfAborted();
75
92
  const current = await this.read();
76
93
  if (current === undefined)
@@ -82,27 +99,39 @@ export class CursorAuth {
82
99
  if (this.refreshTask === undefined) {
83
100
  const pending = this.refresh(current, signal);
84
101
  this.refreshTask = pending;
85
- void pending.finally(() => { if (this.refreshTask === pending)
86
- this.refreshTask = undefined; }).catch(() => undefined);
102
+ void pending
103
+ .finally(() => {
104
+ if (this.refreshTask === pending)
105
+ this.refreshTask = undefined;
106
+ })
107
+ .catch(() => undefined);
87
108
  }
88
109
  return (await this.refreshTask).access;
89
110
  }
90
111
  async refresh(current, signal) {
91
112
  const response = await this.http(`${API_ORIGIN}/auth/exchange_user_api_key`, {
92
- method: 'POST', redirect: 'error',
93
- headers: { authorization: `Bearer ${current.refresh}`, accept: 'application/json', 'content-type': 'application/json' },
113
+ method: 'POST',
114
+ redirect: 'error',
115
+ headers: {
116
+ authorization: `Bearer ${current.refresh}`,
117
+ accept: 'application/json',
118
+ 'content-type': 'application/json',
119
+ },
94
120
  body: '{}',
95
121
  ...(signal === undefined ? {} : { signal }),
96
122
  });
97
123
  if (!response.ok)
98
124
  throw new Error(`Cursor token refresh failed (HTTP ${response.status})`);
99
- const body = await parseProviderJson(response, 'Cursor token refresh');
125
+ const body = (await parseProviderJson(response, 'Cursor token refresh'));
100
126
  if (typeof body.accessToken !== 'string' || body.accessToken.length === 0) {
101
127
  throw new Error('Cursor token refresh returned no access token');
102
128
  }
103
129
  const next = {
104
- type: 'oauth', access: body.accessToken,
105
- refresh: typeof body.refreshToken === 'string' && body.refreshToken.length > 0 ? body.refreshToken : current.refresh,
130
+ type: 'oauth',
131
+ access: body.accessToken,
132
+ refresh: typeof body.refreshToken === 'string' && body.refreshToken.length > 0
133
+ ? body.refreshToken
134
+ : current.refresh,
106
135
  expires: tokenExpiry(body.accessToken, this.now()),
107
136
  };
108
137
  const latest = await this.read();
@@ -121,13 +150,22 @@ export class CursorAuth {
121
150
  const challenge = createHash('sha256').update(verifier).digest('base64url');
122
151
  const uuid = randomUUID();
123
152
  const url = new URL('https://cursor.com/loginDeepControl');
124
- url.search = new URLSearchParams({ challenge, uuid, mode: 'login', redirectTarget: 'cli' }).toString();
153
+ url.search = new URLSearchParams({
154
+ challenge,
155
+ uuid,
156
+ mode: 'login',
157
+ redirectTarget: 'cli',
158
+ }).toString();
125
159
  const abort = new AbortController();
126
160
  const promise = this.pollLogin(uuid, verifier, abort.signal)
127
- .catch(error => { if (!abort.signal.aborted)
128
- this.error = error instanceof Error ? error.message : String(error); })
129
- .finally(() => { if (this.loginTask?.abort === abort)
130
- this.loginTask = undefined; });
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
+ });
131
169
  this.loginTask = { abort, promise };
132
170
  return { authorizeUrl: url.href };
133
171
  }
@@ -136,23 +174,30 @@ export class CursorAuth {
136
174
  for (let attempt = 0; attempt < 150; attempt++) {
137
175
  await abortableDelay(delay, signal);
138
176
  const url = `${API_ORIGIN}/auth/poll?uuid=${encodeURIComponent(uuid)}&verifier=${encodeURIComponent(verifier)}`;
139
- const response = await this.http(url, { headers: { accept: 'application/json' }, redirect: 'error', signal });
177
+ const response = await this.http(url, {
178
+ headers: { accept: 'application/json' },
179
+ redirect: 'error',
180
+ signal,
181
+ });
140
182
  if (response.status === 404) {
141
183
  delay = Math.min(Math.ceil(delay * 1.2), 10_000);
142
184
  continue;
143
185
  }
144
186
  if (!response.ok)
145
187
  throw new Error(`Cursor login failed (HTTP ${response.status})`);
146
- const body = await parseProviderJson(response, 'Cursor login poll');
188
+ const body = (await parseProviderJson(response, 'Cursor login poll'));
147
189
  signal.throwIfAborted();
148
- if (typeof body.accessToken !== 'string' || body.accessToken.length === 0) {
190
+ if (typeof body.accessToken !== 'string' ||
191
+ body.accessToken.length === 0) {
149
192
  throw new Error('Cursor login returned no access token');
150
193
  }
151
- if (typeof body.refreshToken !== 'string' || body.refreshToken.length === 0) {
194
+ if (typeof body.refreshToken !== 'string' ||
195
+ body.refreshToken.length === 0) {
152
196
  throw new Error('Cursor login returned no refresh token');
153
197
  }
154
198
  const value = {
155
- type: 'oauth', access: body.accessToken,
199
+ type: 'oauth',
200
+ access: body.accessToken,
156
201
  refresh: body.refreshToken,
157
202
  expires: tokenExpiry(body.accessToken, this.now()),
158
203
  };
@@ -9,7 +9,8 @@ export function validateCursorModelSettings(input) {
9
9
  const models = input.visibleModels;
10
10
  if (models === undefined)
11
11
  return {};
12
- if (!Array.isArray(models) || models.some(id => typeof id !== 'string' || !id.trim())) {
12
+ if (!Array.isArray(models) ||
13
+ models.some((id) => typeof id !== 'string' || !id.trim())) {
13
14
  throw new Error('visibleModels must be an array of model ids');
14
15
  }
15
16
  return { visibleModels: [...new Set(models)] };
@@ -26,18 +27,26 @@ export class CursorModelSettingsStore {
26
27
  }
27
28
  catch (error) {
28
29
  if (error.code !== 'ENOENT')
29
- throw new Error(`Cannot read Cursor model settings at ${path}`, { cause: error });
30
+ throw new Error(`Cannot read Cursor model settings at ${path}`, {
31
+ cause: error,
32
+ });
30
33
  }
31
34
  }
32
- get() { return structuredClone(this.current); }
33
- visibleModels() { return this.current.visibleModels; }
35
+ get() {
36
+ return structuredClone(this.current);
37
+ }
38
+ visibleModels() {
39
+ return this.current.visibleModels;
40
+ }
34
41
  set(input) {
35
42
  const validated = validateCursorModelSettings(input);
36
43
  const run = this.writes.then(async () => {
37
44
  await mkdir(dirname(this.path), { recursive: true });
38
45
  const temporary = `${this.path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
39
46
  try {
40
- await writeFile(temporary, JSON.stringify(validated, null, 2), { mode: 0o600 });
47
+ await writeFile(temporary, JSON.stringify(validated, null, 2), {
48
+ mode: 0o600,
49
+ });
41
50
  await rename(temporary, this.path);
42
51
  }
43
52
  finally {
@@ -52,17 +52,21 @@ function legacyRequestWindow(body, resetsAt) {
52
52
  if (usage === undefined)
53
53
  return undefined;
54
54
  const gpt4 = record(usage['gpt-4']);
55
- const candidate = gpt4 !== undefined && number(gpt4.numRequests) !== undefined ? gpt4 : Object.values(usage)
56
- .map(record)
57
- .filter((value) => value !== undefined)
58
- .sort((a, b) => (number(b.maxRequestUsage) ?? 0) - (number(a.maxRequestUsage) ?? 0))[0];
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];
59
62
  const used = number(candidate?.numRequests);
60
63
  const limit = number(candidate?.maxRequestUsage);
61
64
  if (used === undefined || limit === undefined || limit <= 0)
62
65
  return undefined;
63
66
  return {
64
- kind: 'other', scope: 'Included requests',
65
- usedPercent: Math.max(0, Math.min(100, used / limit * 100)),
67
+ kind: 'other',
68
+ scope: 'Included requests',
69
+ usedPercent: Math.max(0, Math.min(100, (used / limit) * 100)),
66
70
  ...(resetsAt === undefined ? {} : { resetsAt }),
67
71
  };
68
72
  }
@@ -79,18 +83,26 @@ export async function fetchCursorUsage(accessToken, http = fetch, signal) {
79
83
  dashboardJson(`${ORIGIN}/api/usage-summary`, cookie, http, signal),
80
84
  dashboardJson(`${ORIGIN}/api/usage?user=${encodeURIComponent(userId)}`, cookie, http, signal),
81
85
  ]);
82
- if (summaryResult.status === 'rejected' && legacyResult.status === 'rejected') {
83
- throw summaryResult.reason instanceof Error ? summaryResult.reason : new Error('Cursor usage is unavailable');
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');
84
91
  }
85
- const summary = summaryResult.status === 'fulfilled' ? record(summaryResult.value) : undefined;
86
- const plan = record(record(summary?.individualUsage)?.plan) ?? record(record(summary?.teamUsage)?.plan);
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);
87
97
  const resetValue = summary?.billingCycleEnd;
88
98
  const parsedReset = typeof resetValue === 'string' ? Date.parse(resetValue) : NaN;
89
99
  const resetsAt = Number.isFinite(parsedReset) ? parsedReset : undefined;
90
100
  const startValue = summary?.billingCycleStart;
91
101
  const parsedStart = typeof startValue === 'string' ? Date.parse(startValue) : NaN;
92
- const timing = { ...(resetsAt === undefined ? {} : { resetsAt }),
93
- ...(Number.isFinite(parsedStart) ? { startsAt: parsedStart } : {}) };
102
+ const timing = {
103
+ ...(resetsAt === undefined ? {} : { resetsAt }),
104
+ ...(Number.isFinite(parsedStart) ? { startsAt: parsedStart } : {}),
105
+ };
94
106
  const windows = [];
95
107
  for (const [field, scope] of [
96
108
  ['totalPercentUsed', 'Included'],
@@ -112,6 +124,8 @@ export async function fetchCursorUsage(accessToken, http = fetch, signal) {
112
124
  return {
113
125
  supported: true,
114
126
  windows,
115
- ...(typeof membershipType === 'string' && membershipType.length > 0 ? { plan: membershipType } : {}),
127
+ ...(typeof membershipType === 'string' && membershipType.length > 0
128
+ ? { plan: membershipType }
129
+ : {}),
116
130
  };
117
131
  }
@@ -1,14 +1,20 @@
1
1
  import { fetchMiniMaxUsage } from './minimax-usage.js';
2
- import { fetchKimiCodeUsage, fetchOllamaUsage, fetchOpenCodeGoUsage } from './external-usage.js';
3
- import { DEFAULT_OLLAMA_API_KEY_REF, DEFAULT_OLLAMA_BASE_URL } from './ollama.js';
4
- export const EXTERNAL_USAGE_SOURCES = ['opencode-go', 'kimi-code', 'minimax', 'minimax-cn', 'ollama-cloud'];
2
+ import { fetchKimiCodeUsage, fetchOllamaUsage, fetchOpenCodeGoUsage, } from './external-usage.js';
3
+ import { DEFAULT_OLLAMA_API_KEY_REF, DEFAULT_OLLAMA_BASE_URL, } from './ollama.js';
4
+ export const EXTERNAL_USAGE_SOURCES = [
5
+ 'opencode-go',
6
+ 'kimi-code',
7
+ 'minimax',
8
+ 'minimax-cn',
9
+ 'ollama-cloud',
10
+ ];
5
11
  function valueOf(value) {
6
12
  return typeof value === 'function' ? value() : value;
7
13
  }
8
14
  const DEFAULT_REFS = {
9
15
  'opencode-go': 'OPENCODE_GO_API_KEY',
10
16
  'kimi-code': 'KIMI_CODING_API_KEY',
11
- 'minimax': 'MINIMAX_API_KEY',
17
+ minimax: 'MINIMAX_API_KEY',
12
18
  'minimax-cn': 'MINIMAX_CN_API_KEY',
13
19
  'ollama-cloud': DEFAULT_OLLAMA_API_KEY_REF,
14
20
  };
@@ -28,7 +34,10 @@ export class ExternalUsageController {
28
34
  const value = await this.resolveCredential(valueOf(this.refs[source]));
29
35
  return typeof value?.value === 'string' && value.value.length > 0;
30
36
  }));
31
- return Object.fromEntries(EXTERNAL_USAGE_SOURCES.map((source, i) => [source, { configured: values[i] ?? false }]));
37
+ return Object.fromEntries(EXTERNAL_USAGE_SOURCES.map((source, i) => [
38
+ source,
39
+ { configured: values[i] ?? false },
40
+ ]));
32
41
  }
33
42
  async usage(source, signal) {
34
43
  // Snapshot the endpoint facts before the first await: a save that
@@ -24,7 +24,10 @@ function record(value) {
24
24
  : undefined;
25
25
  }
26
26
  function percent(value) {
27
- return typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 100
27
+ return typeof value === 'number' &&
28
+ Number.isFinite(value) &&
29
+ value >= 0 &&
30
+ value <= 100
28
31
  ? value
29
32
  : undefined;
30
33
  }
@@ -71,8 +74,11 @@ export async function fetchOpenCodeGoUsage(apiKey, http = fetch, signal) {
71
74
  ...(scope === undefined ? {} : { scope }),
72
75
  usedPercent,
73
76
  // Go's rolling counter is first-use anchored, not per-request sliding.
74
- ...(field === 'rolling' ? { fixedWindow: true, windowDurationMs: 18_000_000 }
75
- : field === 'weekly' ? { fixedWindow: true, windowDurationMs: 604_800_000 } : {}),
77
+ ...(field === 'rolling'
78
+ ? { fixedWindow: true, windowDurationMs: 18_000_000 }
79
+ : field === 'weekly'
80
+ ? { fixedWindow: true, windowDurationMs: 604_800_000 }
81
+ : {}),
76
82
  ...(resetsAt === undefined ? {} : { resetsAt }),
77
83
  ...monthlyDisplay,
78
84
  });
@@ -90,8 +96,12 @@ export async function fetchKimiCodeUsage(apiKey, http = fetch, signal) {
90
96
  // Only the top-level `usage` object is the summary the official CLI labels
91
97
  // "Weekly limit"; an anonymous `limits[]` entry is "Limit #n" there.
92
98
  const rows = [
93
- ...(record(body.usage) ? [{ raw: { detail: body.usage }, summary: true }] : []),
94
- ...(Array.isArray(body.limits) ? body.limits.map(raw => ({ raw, summary: false })) : []),
99
+ ...(record(body.usage)
100
+ ? [{ raw: { detail: body.usage }, summary: true }]
101
+ : []),
102
+ ...(Array.isArray(body.limits)
103
+ ? body.limits.map((raw) => ({ raw, summary: false }))
104
+ : []),
95
105
  ];
96
106
  for (const { raw, summary } of rows) {
97
107
  const item = record(raw);
@@ -99,25 +109,39 @@ export async function fetchKimiCodeUsage(apiKey, http = fetch, signal) {
99
109
  continue;
100
110
  const detail = record(item.detail) ?? item;
101
111
  const limit = numeric(detail.limit);
102
- const used = numeric(detail.used) ?? (limit !== undefined && numeric(detail.remaining) !== undefined
103
- ? limit - numeric(detail.remaining) : undefined);
112
+ const used = numeric(detail.used) ??
113
+ (limit !== undefined && numeric(detail.remaining) !== undefined
114
+ ? limit - numeric(detail.remaining)
115
+ : undefined);
104
116
  if (limit === undefined || limit <= 0 || used === undefined)
105
117
  continue;
106
- const usedPercent = percent(used / limit * 100);
118
+ const usedPercent = percent((used / limit) * 100);
107
119
  if (usedPercent === undefined)
108
120
  continue;
109
121
  const window = record(item.window) ?? item;
110
122
  const duration = numeric(window.duration ?? item.duration ?? detail.duration);
111
123
  const unit = String(window.timeUnit ?? item.timeUnit ?? detail.timeUnit ?? '').toUpperCase();
112
- const multiplier = unit.includes('MINUTE') ? 60_000 : unit.includes('HOUR') ? 3_600_000
113
- : unit.includes('DAY') ? 86_400_000 : unit.includes('SECOND') ? 1000 : undefined;
124
+ const multiplier = unit.includes('MINUTE')
125
+ ? 60_000
126
+ : unit.includes('HOUR')
127
+ ? 3_600_000
128
+ : unit.includes('DAY')
129
+ ? 86_400_000
130
+ : unit.includes('SECOND')
131
+ ? 1000
132
+ : undefined;
114
133
  const durationMs = duration === undefined || duration <= 0 || multiplier === undefined
115
- ? undefined : duration * multiplier;
116
- const kind = durationMs === 18_000_000 ? 'session'
117
- : durationMs === 604_800_000 ? 'weekly'
118
- // No period fields at all: the official CLI labels the top-level
119
- // summary the weekly limit, so use that kind rather than 'other'.
120
- : durationMs === undefined && unit === '' ? 'weekly' : 'other';
134
+ ? undefined
135
+ : duration * multiplier;
136
+ const kind = durationMs === 18_000_000
137
+ ? 'session'
138
+ : durationMs === 604_800_000
139
+ ? 'weekly'
140
+ : // No period fields at all: the official CLI labels the top-level
141
+ // summary the weekly limit, so use that kind rather than 'other'.
142
+ durationMs === undefined && unit === ''
143
+ ? 'weekly'
144
+ : 'other';
121
145
  const resetsAt = kimiReset(detail);
122
146
  const scope = detail.name ?? detail.title ?? item.name ?? item.title;
123
147
  // A vendor-declared span anchors the display marker at
@@ -133,7 +157,9 @@ export async function fetchKimiCodeUsage(apiKey, http = fetch, signal) {
133
157
  const displayStart = resetsAt !== undefined && markerSpan !== undefined
134
158
  ? { displayStartsAt: Math.min(resetsAt - markerSpan, Date.now()) }
135
159
  : {};
136
- modern.push({ kind, usedPercent,
160
+ modern.push({
161
+ kind,
162
+ usedPercent,
137
163
  ...(typeof scope === 'string' ? { scope } : {}),
138
164
  ...(resetsAt === undefined ? {} : { resetsAt }),
139
165
  ...(durationMs === undefined ? {} : { windowDurationMs: durationMs }),
@@ -156,7 +182,10 @@ export async function fetchKimiCodeUsage(apiKey, http = fetch, signal) {
156
182
  ]) {
157
183
  const value = record(usages[field]);
158
184
  const ratio = value?.used_ratio ?? value?.usedRatio;
159
- if (typeof ratio !== 'number' || !Number.isFinite(ratio) || ratio < 0 || ratio > 1)
185
+ if (typeof ratio !== 'number' ||
186
+ !Number.isFinite(ratio) ||
187
+ ratio < 0 ||
188
+ ratio > 1)
160
189
  continue;
161
190
  const resetsAt = resetTime(value?.reset_time ?? value?.resetAt);
162
191
  // The monthly total resets on the subscription's cycle, not the
@@ -169,7 +198,9 @@ export async function fetchKimiCodeUsage(apiKey, http = fetch, signal) {
169
198
  kind,
170
199
  ...(scope === undefined ? {} : { scope }),
171
200
  usedPercent: ratio * 100,
172
- ...(field === 'limit_7d' ? { fixedWindow: true, windowDurationMs: 604_800_000 } : {}),
201
+ ...(field === 'limit_7d'
202
+ ? { fixedWindow: true, windowDurationMs: 604_800_000 }
203
+ : {}),
173
204
  ...(resetsAt === undefined ? {} : { resetsAt }),
174
205
  ...monthlyDisplay,
175
206
  });
@@ -206,7 +237,10 @@ export async function fetchOllamaUsage(apiKey, baseURL, http = fetch, signal) {
206
237
  const included = record(body?.included);
207
238
  const allowance = numeric(included?.allowance_usd);
208
239
  const balance = numeric(included?.balance_usd);
209
- if (included === undefined || allowance === undefined || allowance <= 0 || balance === undefined) {
240
+ if (included === undefined ||
241
+ allowance === undefined ||
242
+ allowance <= 0 ||
243
+ balance === undefined) {
210
244
  throw new Error('Ollama balance response has no usable included allowance');
211
245
  }
212
246
  const period = record(included.period);
@@ -214,14 +248,16 @@ export async function fetchOllamaUsage(apiKey, baseURL, http = fetch, signal) {
214
248
  const startsAt = period === undefined ? undefined : resetTime(period.from);
215
249
  return {
216
250
  supported: true,
217
- windows: [{
251
+ windows: [
252
+ {
218
253
  kind: 'other',
219
254
  scope: 'Credits',
220
255
  // An overdrawn balance stays visible instead of dropping the window.
221
- usedPercent: (allowance - balance) / allowance * 100,
256
+ usedPercent: ((allowance - balance) / allowance) * 100,
222
257
  ...(resetsAt === undefined ? {} : { resetsAt }),
223
258
  ...(startsAt === undefined ? {} : { startsAt }),
224
- }],
259
+ },
260
+ ],
225
261
  plan: 'Ollama Cloud',
226
262
  };
227
263
  }