dsh-subscriptions 0.1.5 → 0.2.2

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 (110) hide show
  1. package/README.md +212 -154
  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 +73 -2
  6. package/lib/auth/rpc.js +124 -23
  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 +13 -0
  16. package/lib/client/OllamaCard.js +94 -0
  17. package/lib/client/OllamaSettingsManager.d.ts +24 -0
  18. package/lib/client/OllamaSettingsManager.js +329 -0
  19. package/lib/client/ProviderAccountManager.d.ts +1 -1
  20. package/lib/client/ProviderAccountManager.js +164 -49
  21. package/lib/client/ProviderModelEditor.js +109 -26
  22. package/lib/client/SpeedSelect.d.ts +62 -14
  23. package/lib/client/SpeedSelect.js +257 -46
  24. package/lib/client/SubscriptionUsageBadge.d.ts +5 -11
  25. package/lib/client/SubscriptionUsageBadge.js +301 -112
  26. package/lib/client/SubscriptionsSection.d.ts +2 -0
  27. package/lib/client/SubscriptionsSection.js +259 -85
  28. package/lib/client/UsageBadgeDisplaySetting.js +35 -12
  29. package/lib/client/UsageMeter.d.ts +1 -1
  30. package/lib/client/UsageMeter.js +69 -27
  31. package/lib/client/VideoGenerateToolview.js +69 -17
  32. package/lib/client/account-preferences.d.ts +8 -0
  33. package/lib/client/account-preferences.js +20 -5
  34. package/lib/client/host-contract.d.ts +11 -2
  35. package/lib/client/host-contract.js +20 -3
  36. package/lib/client/index.d.ts +5 -5
  37. package/lib/client/index.js +34 -12
  38. package/lib/client/locales.d.ts +78 -0
  39. package/lib/client/locales.js +121 -34
  40. package/lib/client/ollama-draft.d.ts +229 -0
  41. package/lib/client/ollama-draft.js +255 -0
  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.d.ts +7 -0
  48. package/lib/client/usage-pace.js +43 -10
  49. package/lib/client.js +1647 -266
  50. package/lib/client.js.map +1 -1
  51. package/lib/http.js +1 -1
  52. package/lib/index.d.ts +28 -3
  53. package/lib/index.js +9329 -8217
  54. package/lib/model-defaults.js +4 -2
  55. package/lib/provider-settings.d.ts +14 -1
  56. package/lib/provider-settings.js +64 -20
  57. package/lib/providers/account-preferences.js +51 -22
  58. package/lib/providers/accounts.js +15 -9
  59. package/lib/providers/antigravity-oauth-client.js +2 -2
  60. package/lib/providers/antigravity.js +141 -68
  61. package/lib/providers/catalog-store.js +120 -41
  62. package/lib/providers/claude-cli-version.js +3 -3
  63. package/lib/providers/claude.d.ts +23 -3
  64. package/lib/providers/claude.js +167 -92
  65. package/lib/providers/codex-client-version.js +3 -3
  66. package/lib/providers/codex-search.js +40 -13
  67. package/lib/providers/codex.js +196 -112
  68. package/lib/providers/common.d.ts +14 -0
  69. package/lib/providers/common.js +118 -38
  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.d.ts +9 -2
  77. package/lib/providers/external-usage-controller.js +30 -7
  78. package/lib/providers/external-usage.d.ts +8 -0
  79. package/lib/providers/external-usage.js +140 -20
  80. package/lib/providers/grok.js +152 -89
  81. package/lib/providers/image-pool.js +29 -11
  82. package/lib/providers/minimax-usage.js +31 -11
  83. package/lib/providers/npm-cli-version.js +10 -3
  84. package/lib/providers/ollama-settings.d.ts +90 -0
  85. package/lib/providers/ollama-settings.js +213 -0
  86. package/lib/providers/ollama-web.d.ts +46 -0
  87. package/lib/providers/ollama-web.js +231 -0
  88. package/lib/providers/ollama.d.ts +107 -0
  89. package/lib/providers/ollama.js +608 -0
  90. package/lib/providers/pool-family.js +11 -3
  91. package/lib/providers/pool-health.js +28 -8
  92. package/lib/providers/pool-usage.js +16 -9
  93. package/lib/providers/pool.d.ts +8 -0
  94. package/lib/providers/pool.js +81 -42
  95. package/lib/providers/rate-limit.js +8 -2
  96. package/lib/providers/replay.js +40 -17
  97. package/lib/providers/reset-redemption.js +18 -6
  98. package/lib/tools/image-generate.js +123 -65
  99. package/lib/tools/registration.js +5 -2
  100. package/lib/tools/video-generate.js +51 -31
  101. package/lib/tools/x-search.js +50 -22
  102. package/lib/translate/anthropic.js +85 -37
  103. package/lib/translate/antigravity-schema.js +71 -28
  104. package/lib/translate/antigravity-thinking.js +22 -9
  105. package/lib/translate/antigravity.js +141 -50
  106. package/lib/translate/chat-completions.js +67 -23
  107. package/lib/translate/resolved.js +55 -21
  108. package/lib/translate/responses.js +69 -29
  109. package/lib/translate/sse.js +4 -1
  110. package/package.json +26 -4
@@ -46,6 +46,13 @@ export declare function validateModels(models: readonly ModelEntry[], label: str
46
46
  * @throws SyntaxError with the local origin and a fixed omission marker.
47
47
  */
48
48
  export declare function parseProviderJson<T = unknown>(input: Response | string, origin: string): Promise<T>;
49
+ /**
50
+ * Read a bounded JSON body; oversized or malformed payloads fail loudly.
51
+ * Bytes accumulate per chunk and the stream is cancelled past the cap, so
52
+ * the bound limits memory even when the body has no end; a declared
53
+ * over-cap `content-length` fails before the first read.
54
+ */
55
+ export declare function readBoundedJson(response: Response, label: string, maxBytes: number): Promise<unknown>;
49
56
  /** Optional per-call hooks {@link httpLlmError} uses to read a rate-limit window. */
50
57
  export interface HttpLlmErrorOptions {
51
58
  /**
@@ -212,6 +219,13 @@ export interface UsageWindow {
212
219
  windowDurationMs?: number;
213
220
  /** Provider-verified fixed interval; permits deriving start from end and duration. */
214
221
  fixedWindow?: boolean;
222
+ /**
223
+ * Display-only start the reader assumed (kind or label based, clamped
224
+ * to now). The time marker may ride it; pace and color must never —
225
+ * it is not evidence of when the window opened. Mutually exclusive
226
+ * with `startsAt`.
227
+ */
228
+ displayStartsAt?: number;
215
229
  }
216
230
  /** One banked reset credit (Codex), or one grant containing resets (Claude). */
217
231
  export interface ResetCredit {
@@ -22,19 +22,22 @@ export function validateModels(models, label) {
22
22
  if (model.name !== undefined && model.name.length === 0) {
23
23
  throw new Error(`${label}: catalog model "${model.id}" has an empty name`);
24
24
  }
25
- if (model.contextWindow !== undefined
26
- && (!Number.isInteger(model.contextWindow) || model.contextWindow <= 0)) {
25
+ if (model.contextWindow !== undefined &&
26
+ (!Number.isInteger(model.contextWindow) || model.contextWindow <= 0)) {
27
27
  throw new Error(`${label}: catalog model "${model.id}" contextWindow must be a positive integer`);
28
28
  }
29
- if (model.maxTokens !== undefined && (!Number.isInteger(model.maxTokens) || model.maxTokens <= 0)) {
29
+ if (model.maxTokens !== undefined &&
30
+ (!Number.isInteger(model.maxTokens) || model.maxTokens <= 0)) {
30
31
  throw new Error(`${label}: catalog model "${model.id}" maxTokens must be a positive integer`);
31
32
  }
32
- if (model.inputModalities !== undefined
33
- && (model.inputModalities.length === 0
34
- || model.inputModalities.some(modality => modality !== 'text' && modality !== 'image'))) {
33
+ if (model.inputModalities !== undefined &&
34
+ (model.inputModalities.length === 0 ||
35
+ model.inputModalities.some((modality) => modality !== 'text' && modality !== 'image'))) {
35
36
  throw new Error(`${label}: catalog model "${model.id}" inputModalities must be a non-empty list of "text"/"image"`);
36
37
  }
37
- if (model.wire !== undefined && model.wire !== 'chat-completions' && model.wire !== 'responses') {
38
+ if (model.wire !== undefined &&
39
+ model.wire !== 'chat-completions' &&
40
+ model.wire !== 'responses') {
38
41
  throw new Error(`${label}: catalog model "${model.id}" wire must be "chat-completions" or "responses"`);
39
42
  }
40
43
  if (seen.has(model.id))
@@ -42,11 +45,15 @@ export function validateModels(models, label) {
42
45
  seen.add(model.id);
43
46
  return {
44
47
  id: model.id,
45
- ...model.name === undefined ? {} : { name: model.name },
46
- ...model.contextWindow === undefined ? {} : { contextWindow: model.contextWindow },
47
- ...model.maxTokens === undefined ? {} : { maxTokens: model.maxTokens },
48
- ...model.inputModalities === undefined ? {} : { inputModalities: [...model.inputModalities] },
49
- ...model.wire === undefined ? {} : { wire: model.wire },
48
+ ...(model.name === undefined ? {} : { name: model.name }),
49
+ ...(model.contextWindow === undefined
50
+ ? {}
51
+ : { contextWindow: model.contextWindow }),
52
+ ...(model.maxTokens === undefined ? {} : { maxTokens: model.maxTokens }),
53
+ ...(model.inputModalities === undefined
54
+ ? {}
55
+ : { inputModalities: [...model.inputModalities] }),
56
+ ...(model.wire === undefined ? {} : { wire: model.wire }),
50
57
  };
51
58
  });
52
59
  }
@@ -80,9 +87,61 @@ export async function parseProviderJson(input, origin) {
80
87
  catch (error) {
81
88
  if (!(error instanceof SyntaxError))
82
89
  throw error;
90
+ // Do not attach the original SyntaxError: Node's message quotes the input, and the host renders cause.
91
+ // eslint-disable-next-line preserve-caught-error -- the replacement omits the provider excerpt on purpose
83
92
  throw new SyntaxError(providerDiagnostic(`${origin}: invalid JSON`, true));
84
93
  }
85
94
  }
95
+ /**
96
+ * Read a bounded JSON body; oversized or malformed payloads fail loudly.
97
+ * Bytes accumulate per chunk and the stream is cancelled past the cap, so
98
+ * the bound limits memory even when the body has no end; a declared
99
+ * over-cap `content-length` fails before the first read.
100
+ */
101
+ export async function readBoundedJson(response, label, maxBytes) {
102
+ const declared = Number(response.headers.get('content-length'));
103
+ if (Number.isFinite(declared) && declared > maxBytes) {
104
+ // Stop the socket too: the headers arrived, but the body must never flow.
105
+ await response.body?.cancel().catch(() => undefined);
106
+ throw new Error(`${label} response exceeds ${String(maxBytes)} bytes`);
107
+ }
108
+ if (response.body === null)
109
+ throw new Error(`${label} response has no body`);
110
+ const reader = response.body.getReader();
111
+ const decoder = new TextDecoder();
112
+ let bytes = 0;
113
+ let text = '';
114
+ try {
115
+ for (;;) {
116
+ const chunk = await reader.read();
117
+ if (chunk.done)
118
+ break;
119
+ bytes += chunk.value.byteLength;
120
+ if (bytes > maxBytes) {
121
+ // Cancel before throwing so the live stream does not linger behind
122
+ // a rejection the caller already stopped listening to.
123
+ await reader.cancel().catch(() => undefined);
124
+ throw new Error(`${label} response exceeds ${String(maxBytes)} bytes`);
125
+ }
126
+ text += decoder.decode(chunk.value, { stream: true });
127
+ }
128
+ text += decoder.decode();
129
+ }
130
+ finally {
131
+ try {
132
+ reader.releaseLock();
133
+ }
134
+ catch {
135
+ /* best effort */
136
+ }
137
+ }
138
+ try {
139
+ return JSON.parse(text);
140
+ }
141
+ catch {
142
+ throw new Error(`${label} response is not JSON`);
143
+ }
144
+ }
86
145
  /**
87
146
  * Build an LlmError from a non-2xx provider response, mapping the status to a
88
147
  * stable code and, for a rate-limited request, the disclosed reset instant to
@@ -139,7 +198,9 @@ export async function httpLlmError(response, label, options = {}) {
139
198
  // re-hitting the same closed window.
140
199
  const rateLimited = response.status === 429;
141
200
  const reset = rateLimited
142
- ? options.rateLimitReset?.(response, body, now) ?? googleQuotaReset(body, now) ?? retryAfterInstant(response, now)
201
+ ? (options.rateLimitReset?.(response, body, now) ??
202
+ googleQuotaReset(body, now) ??
203
+ retryAfterInstant(response, now))
143
204
  : retryAfterInstant(response, now);
144
205
  if (reset === undefined && rateLimited) {
145
206
  // Header names and values are provider-controlled too; none are displayed.
@@ -147,7 +208,9 @@ export async function httpLlmError(response, label, options = {}) {
147
208
  }
148
209
  return new LlmError(message, code, {
149
210
  status: response.status,
150
- ...reset === undefined ? {} : { providerRetryAfterMs: waitFromReset(reset, now) },
211
+ ...(reset === undefined
212
+ ? {}
213
+ : { providerRetryAfterMs: waitFromReset(reset, now) }),
151
214
  });
152
215
  }
153
216
  /**
@@ -236,9 +299,13 @@ export function idleWatchdog(caller, timeoutMs) {
236
299
  */
237
300
  export function mapFetchFailure(label, error, watchdog, caller) {
238
301
  if (watchdog.timedOut())
239
- return new LlmError(`${label} stream idle timeout`, 'TIMEOUT', { cause: error });
302
+ return new LlmError(`${label} stream idle timeout`, 'TIMEOUT', {
303
+ cause: error,
304
+ });
240
305
  if (caller?.aborted === true)
241
- return new LlmError(`${label} request aborted by caller`, 'ABORTED', { cause: error });
306
+ return new LlmError(`${label} request aborted by caller`, 'ABORTED', {
307
+ cause: error,
308
+ });
242
309
  if (error instanceof LlmError)
243
310
  return error;
244
311
  return new LlmError(`${label} request failed`, 'TRANSPORT', { cause: error });
@@ -339,10 +406,11 @@ export class TokenManager {
339
406
  async session(forceRefresh = false) {
340
407
  const session = await this.options.load();
341
408
  if (session === undefined) {
342
- throw new LlmError(`dsh-subscriptions: not logged in to ${this.options.displayName}; `
343
- + 'log in via Settings → Subscriptions in the dsh web app', 'MISSING_CREDENTIAL');
409
+ throw new LlmError(`dsh-subscriptions: not logged in to ${this.options.displayName}; ` +
410
+ 'log in via Settings → Subscriptions in the dsh web app', 'MISSING_CREDENTIAL');
344
411
  }
345
- if (!forceRefresh && session.expiresAt - Date.now() > this.options.preemptMs) {
412
+ if (!forceRefresh &&
413
+ session.expiresAt - Date.now() > this.options.preemptMs) {
346
414
  return session;
347
415
  }
348
416
  this.inflight ??= this.doRefresh(session).finally(() => {
@@ -358,7 +426,9 @@ export class TokenManager {
358
426
  // log out an account that just signed in. Serve it instead.
359
427
  const attempted = this.attempted;
360
428
  const stored = await this.options.load();
361
- if (stored !== undefined && attempted !== undefined && stored.refreshToken !== attempted.refreshToken) {
429
+ if (stored !== undefined &&
430
+ attempted !== undefined &&
431
+ stored.refreshToken !== attempted.refreshToken) {
362
432
  return stored;
363
433
  }
364
434
  await this.options.remove();
@@ -378,9 +448,9 @@ export class TokenManager {
378
448
  // A concurrent caller may have refreshed while this one waited: re-read
379
449
  // the store and skip the round trip when the stored session is fresh.
380
450
  const current = await this.options.load();
381
- if (current !== undefined
382
- && current.accessToken !== session.accessToken
383
- && current.expiresAt - Date.now() > this.options.preemptMs) {
451
+ if (current !== undefined &&
452
+ current.accessToken !== session.accessToken &&
453
+ current.expiresAt - Date.now() > this.options.preemptMs) {
384
454
  return current;
385
455
  }
386
456
  const attempted = current ?? session;
@@ -398,14 +468,14 @@ export const DISCOVERY_TIMEOUT_MS = 10_000;
398
468
  */
399
469
  export function withTimeout(work, timeoutMs) {
400
470
  const signal = AbortSignal.timeout(timeoutMs);
401
- const aborted = new Promise(resolve => {
471
+ const aborted = new Promise((resolve) => {
402
472
  if (signal.aborted)
403
473
  resolve(undefined);
404
474
  else
405
475
  signal.addEventListener('abort', () => resolve(undefined), { once: true });
406
476
  });
407
477
  return Promise.race([
408
- work(signal).then(value => (signal.aborted ? undefined : value), (error) => {
478
+ work(signal).then((value) => (signal.aborted ? undefined : value), (error) => {
409
479
  if (signal.aborted)
410
480
  return undefined;
411
481
  throw error;
@@ -415,7 +485,9 @@ export function withTimeout(work, timeoutMs) {
415
485
  }
416
486
  /** Display name for a wire reasoning-effort identifier. */
417
487
  export function effortDisplayName(effort) {
418
- return effort === 'xhigh' ? 'Extra High' : effort.charAt(0).toUpperCase() + effort.slice(1);
488
+ return effort === 'xhigh'
489
+ ? 'Extra High'
490
+ : effort.charAt(0).toUpperCase() + effort.slice(1);
419
491
  }
420
492
  /**
421
493
  * Fold a configured per-model default effort into a reasoning block, keeping
@@ -444,7 +516,9 @@ export function mergeReasoning(configuredDefault, base, options) {
444
516
  ? undefined
445
517
  : {
446
518
  efforts: [...base.efforts],
447
- ...(base.defaultEffort === undefined ? {} : { defaultEffort: base.defaultEffort }),
519
+ ...(base.defaultEffort === undefined
520
+ ? {}
521
+ : { defaultEffort: base.defaultEffort }),
448
522
  };
449
523
  if (configuredDefault === undefined)
450
524
  return detached;
@@ -454,10 +528,13 @@ export function mergeReasoning(configuredDefault, base, options) {
454
528
  // catalog does not cover). Inventing a reasoning block here would claim a
455
529
  // capability nobody advertised; only a fallback-based provider may.
456
530
  return options?.extendable === true
457
- ? { efforts: [{ id: effort, name: effortDisplayName(effort) }], defaultEffort: effort }
531
+ ? {
532
+ efforts: [{ id: effort, name: effortDisplayName(effort) }],
533
+ defaultEffort: effort,
534
+ }
458
535
  : undefined;
459
536
  }
460
- if (base.efforts.some(entry => entry.id === effort)) {
537
+ if (base.efforts.some((entry) => entry.id === effort)) {
461
538
  return { efforts: [...base.efforts], defaultEffort: effort };
462
539
  }
463
540
  if (options?.extendable !== true)
@@ -536,7 +613,9 @@ export class ModelCatalogCache {
536
613
  if (this.persistence === undefined)
537
614
  return Promise.resolve();
538
615
  this.seeded ??= this.persistence.load().then((snapshot) => {
539
- if (snapshot !== undefined && this.entry === undefined && !this.seedDisabled) {
616
+ if (snapshot !== undefined &&
617
+ this.entry === undefined &&
618
+ !this.seedDisabled) {
540
619
  this.entry = snapshot;
541
620
  }
542
621
  }, () => undefined);
@@ -612,8 +691,8 @@ export class ModelCatalogCache {
612
691
  }
613
692
  /** Whether discovery failed because the stored login is gone. */
614
693
  export function isMissingOrInvalidCredential(error) {
615
- return error instanceof LlmError
616
- && (error.code === 'MISSING_CREDENTIAL' || error.code === 'INVALID_CREDENTIAL');
694
+ return (error instanceof LlmError &&
695
+ (error.code === 'MISSING_CREDENTIAL' || error.code === 'INVALID_CREDENTIAL'));
617
696
  }
618
697
  /** Whether discovery stopped because the caller cancelled or the timeout fired. */
619
698
  export function isDiscoveryAborted(error, signal) {
@@ -621,9 +700,9 @@ export function isDiscoveryAborted(error, signal) {
621
700
  return true;
622
701
  // Only treat abort-shaped errors as cancellation when this call had a signal;
623
702
  // a refresh TimeoutError must not fail the whole picker union.
624
- return signal !== undefined
625
- && error instanceof Error
626
- && (error.name === 'AbortError' || error.name === 'TimeoutError');
703
+ return (signal !== undefined &&
704
+ error instanceof Error &&
705
+ (error.name === 'AbortError' || error.name === 'TimeoutError'));
627
706
  }
628
707
  /**
629
708
  * Whether discovery failed because the access token was rejected. After
@@ -632,8 +711,8 @@ export function isDiscoveryAborted(error, signal) {
632
711
  * the refresh grant still answers, so the store keeps the session.
633
712
  */
634
713
  export function isDiscoveryAuthFailure(error) {
635
- return (error instanceof OAuthEndpointError && error.status === 401)
636
- || (error instanceof LlmError && error.code === 'AUTH');
714
+ return ((error instanceof OAuthEndpointError && error.status === 401) ||
715
+ (error instanceof LlmError && error.code === 'AUTH'));
637
716
  }
638
717
  /**
639
718
  * Run a catalog fetch, retrying once after a forced token refresh when the
@@ -653,7 +732,8 @@ export async function discoverOrRetryAuth(session, catalog, run) {
653
732
  return await run();
654
733
  }
655
734
  catch (retryError) {
656
- if (!isMissingOrInvalidCredential(retryError) && isDiscoveryAuthFailure(retryError)) {
735
+ if (!isMissingOrInvalidCredential(retryError) &&
736
+ isDiscoveryAuthFailure(retryError)) {
657
737
  catalog.invalidate();
658
738
  }
659
739
  throw retryError;