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
@@ -9,9 +9,9 @@ import { createServer } from 'node:http';
9
9
  import { createPkce, randomHex, randomToken } from './pkce.js';
10
10
  /** Default attempt lifetime: three minutes for the user to complete login. */
11
11
  export const DEFAULT_FLOW_TIMEOUT_MS = 180_000;
12
- const SUCCESS_PAGE = '<!doctype html><html><head><meta charset="utf-8"><title>Login successful</title></head>'
13
- + '<body style="font-family:sans-serif"><h1>Login successful</h1>'
14
- + '<p>You can close this tab and return to DeepSeek Harness.</p></body></html>';
12
+ const SUCCESS_PAGE = '<!doctype html><html><head><meta charset="utf-8"><title>Login successful</title></head>' +
13
+ '<body style="font-family:sans-serif"><h1>Login successful</h1>' +
14
+ '<p>You can close this tab and return to DeepSeek Harness.</p></body></html>';
15
15
  function authorizationFailure(error) {
16
16
  // Safe means this failure path carries no provider-controlled free text,
17
17
  // not that no secret can exist elsewhere in the system.
@@ -40,13 +40,13 @@ function authorizationFailure(error) {
40
40
  category = 'temporarily_unavailable';
41
41
  break;
42
42
  }
43
- return 'authorization failed'
44
- + (category === undefined ? '' : ` (${category})`)
45
- + ': [provider message omitted]';
43
+ return ('authorization failed' +
44
+ (category === undefined ? '' : ` (${category})`) +
45
+ ': [provider message omitted]');
46
46
  }
47
47
  function failurePage(detail) {
48
- return '<!doctype html><html><head><meta charset="utf-8"><title>Login failed</title></head>'
49
- + `<body style="font-family:sans-serif"><h1>Login failed</h1><p>${detail.replace(/[<>&]/g, '')}</p></body></html>`;
48
+ return ('<!doctype html><html><head><meta charset="utf-8"><title>Login failed</title></head>' +
49
+ `<body style="font-family:sans-serif"><h1>Login failed</h1><p>${detail.replace(/[<>&]/g, '')}</p></body></html>`);
50
50
  }
51
51
  /**
52
52
  * Loopback addresses one listen host covers. `localhost` resolves to ::1 or
@@ -69,7 +69,7 @@ function familyUnavailable(error) {
69
69
  */
70
70
  async function listen(handler, spec) {
71
71
  const hosts = listenHosts(spec.host);
72
- const candidates = spec.ports.flatMap(port => (port === 0 ? [0, 0, 0] : [port]));
72
+ const candidates = spec.ports.flatMap((port) => port === 0 ? [0, 0, 0] : [port]);
73
73
  let lastError;
74
74
  for (const candidate of candidates) {
75
75
  const servers = [];
@@ -161,6 +161,8 @@ export class OAuthFlowManager {
161
161
  rejectCode = reject;
162
162
  });
163
163
  let settled = false;
164
+ // Assigned after listen(); a callback that arrives first must see it absent.
165
+ // eslint-disable-next-line prefer-const -- the single write is after the listener exists
164
166
  let timer;
165
167
  let servers = [];
166
168
  const handler = (request, response) => {
@@ -177,7 +179,8 @@ export class OAuthFlowManager {
177
179
  response.end('state mismatch');
178
180
  return;
179
181
  }
180
- if (url.searchParams.has('error_description') || url.searchParams.has('error')) {
182
+ if (url.searchParams.has('error_description') ||
183
+ url.searchParams.has('error')) {
181
184
  const detail = authorizationFailure(url.searchParams.get('error'));
182
185
  response.writeHead(200, { 'content-type': 'text/html' });
183
186
  response.end(failurePage(detail));
package/lib/auth/rpc.d.ts CHANGED
@@ -23,7 +23,7 @@ export declare const SUBSCRIPTIONS_AUTH_PREFIX = "subscriptions-auth.";
23
23
  * Every endpoint {@link dispatch} answers; each gets one exact Fetch route.
24
24
  * Kept in one place so the route table and the switch cannot drift apart.
25
25
  */
26
- export declare const SUBSCRIPTIONS_AUTH_ENDPOINTS: readonly ["providerSettings", "setProviderSettings", "status", "login", "manual", "cancel", "logout", "setDefault", "usage", "externalStatus", "externalUsage", "cursorStatus", "cursorLogin", "cursorCancel", "cursorLogout", "cursorUsage", "cursorModels", "cursorSettings", "cursorSetSettings", "image", "video", "prepareReset", "consumeReset", "speed", "setSpeed", "modelDefaults", "setModelDefault"];
26
+ export declare const SUBSCRIPTIONS_AUTH_ENDPOINTS: readonly ["providerSettings", "setProviderSettings", "status", "login", "manual", "cancel", "logout", "setDefault", "usage", "externalStatus", "externalUsage", "cursorStatus", "cursorLogin", "cursorCancel", "cursorLogout", "cursorUsage", "cursorModels", "cursorSettings", "cursorSetSettings", "ollamaSettings", "ollamaSetSettings", "ollamaModels", "image", "video", "prepareReset", "consumeReset", "speed", "setSpeed", "modelDefaults", "setModelDefault"];
27
27
  /** Decoded image bytes returned by the `image` endpoint. */
28
28
  export interface ImageBytesResult {
29
29
  mediaType: string;
@@ -201,11 +201,82 @@ export interface CursorRpcController {
201
201
  settings(force: boolean, signal: AbortSignal): Promise<unknown>;
202
202
  setSettings(input: unknown): Promise<void>;
203
203
  }
204
+ /** One model in the Ollama Cloud picker catalog, with its capability flags. */
205
+ export interface OllamaModelView {
206
+ id: string;
207
+ name: string;
208
+ /** The endpoint advertised the `vision` capability. */
209
+ vision: boolean;
210
+ /** The endpoint advertised the `thinking` capability. */
211
+ thinking: boolean;
212
+ contextWindow?: number;
213
+ /** Advertised reasoning levels, when the model reasons. */
214
+ efforts?: {
215
+ id: string;
216
+ name: string;
217
+ }[];
218
+ /** The level preselected for the model, when one is known. */
219
+ defaultEffort?: string;
220
+ }
221
+ /** One candidate returned by the cloud listing; adopting it stores `id`. */
222
+ export interface OllamaCloudCandidate {
223
+ /** Normalized wire id, e.g. `glm-5.3:cloud`. */
224
+ id: string;
225
+ /** The id exactly as the listing published it, e.g. `glm-5.3`. */
226
+ rawId: string;
227
+ /** Display basename for the checkbox row. */
228
+ name: string;
229
+ vision: boolean;
230
+ thinking: boolean;
231
+ contextWindow?: number;
232
+ efforts: string[];
233
+ defaultEffort?: string;
234
+ }
235
+ /**
236
+ * What the Ollama card renders: the stored overrides, the effective values
237
+ * after config/default fallback, and the catalog the picker serves.
238
+ */
239
+ export interface OllamaSettingsView {
240
+ provider: 'ollama-cloud';
241
+ /** The stored document; absent fields mean "no override". */
242
+ settings: unknown;
243
+ effective: {
244
+ baseURL: string;
245
+ apiKeyRef: string;
246
+ web: boolean;
247
+ };
248
+ /** The effective catalog, in picker order (empty hides the picker entry). */
249
+ models: OllamaModelView[];
250
+ /** A non-empty `config.ollama.models` wins over the store, so card edits are inert. */
251
+ configOverridesModels: boolean;
252
+ }
253
+ /** Ollama Cloud catalog and connection settings; the API key stays in DSH credentials. */
254
+ export interface OllamaRpcController {
255
+ settings(): Promise<OllamaSettingsView>;
256
+ /** Validate and persist the store document, then re-announce the route. */
257
+ setSettings(input: unknown): Promise<void>;
258
+ /**
259
+ * List the endpoint's Cloud models for the adoption checklist.
260
+ * @param input - an optional one-time API key and base URL override; the
261
+ * key is used for this listing only and never persisted.
262
+ * @param signal - caller cancellation from the RPC transport.
263
+ */
264
+ models(input: {
265
+ apiKey?: string;
266
+ baseURL?: string;
267
+ }, signal: AbortSignal): Promise<{
268
+ models: OllamaCloudCandidate[];
269
+ }>;
270
+ }
204
271
  /**
205
272
  * Register the `/subscriptions-auth` RPC channel when a host connection exists.
206
273
  * @param ctx - the plugin context (headless profiles have no `connection`).
207
274
  * @param controller - the auth operations backing the endpoints.
208
275
  * @param speed - the per-session speed-tier state backing the Speed toggle.
209
276
  * @param modelDefaults - optional per-model default-effort state backing `modelDefaults`/`setModelDefault`.
277
+ * @param providerSettings - optional OAuth provider/account preferences.
278
+ * @param externalUsage - optional credential-backed quota readers.
279
+ * @param cursor - optional Cursor account and model-visibility actions.
280
+ * @param ollama - optional Ollama Cloud catalog and connection settings.
210
281
  */
211
- export declare function registerAuthRpc(ctx: Context, controller: AuthController, speed: SpeedController, modelDefaults?: ModelDefaultsController | undefined, providerSettings?: ProviderSettingsController | undefined, externalUsage?: ExternalUsageRpcController | undefined, cursor?: CursorRpcController | undefined): void;
282
+ export declare function registerAuthRpc(ctx: Context, controller: AuthController, speed: SpeedController, modelDefaults?: ModelDefaultsController | undefined, providerSettings?: ProviderSettingsController | undefined, externalUsage?: ExternalUsageRpcController | undefined, cursor?: CursorRpcController | undefined, ollama?: OllamaRpcController | undefined): void;
package/lib/auth/rpc.js CHANGED
@@ -9,6 +9,7 @@
9
9
  import { AttachmentId } from '@deepseek-ai/dsh-attachment';
10
10
  import { PROVIDER_IDS } from './store.js';
11
11
  import { EXTERNAL_USAGE_SOURCES } from '../providers/external-usage-controller.js';
12
+ import { validateOllamaBaseURL } from '../providers/ollama-settings.js';
12
13
  /**
13
14
  * Endpoint-name prefix under the shared `/api` channel: endpoint `status`
14
15
  * is served at `/api/subscriptions-auth.status`. The browser half calls
@@ -20,17 +21,44 @@ export const SUBSCRIPTIONS_AUTH_PREFIX = 'subscriptions-auth.';
20
21
  * Kept in one place so the route table and the switch cannot drift apart.
21
22
  */
22
23
  export const SUBSCRIPTIONS_AUTH_ENDPOINTS = [
23
- 'providerSettings', 'setProviderSettings',
24
- 'status', 'login', 'manual', 'cancel', 'logout', 'setDefault', 'usage',
25
- 'externalStatus', 'externalUsage',
26
- 'cursorStatus', 'cursorLogin', 'cursorCancel', 'cursorLogout', 'cursorUsage', 'cursorModels',
27
- 'cursorSettings', 'cursorSetSettings',
28
- 'image', 'video', 'prepareReset', 'consumeReset',
29
- 'speed', 'setSpeed',
30
- 'modelDefaults', 'setModelDefault',
24
+ 'providerSettings',
25
+ 'setProviderSettings',
26
+ 'status',
27
+ 'login',
28
+ 'manual',
29
+ 'cancel',
30
+ 'logout',
31
+ 'setDefault',
32
+ 'usage',
33
+ 'externalStatus',
34
+ 'externalUsage',
35
+ 'cursorStatus',
36
+ 'cursorLogin',
37
+ 'cursorCancel',
38
+ 'cursorLogout',
39
+ 'cursorUsage',
40
+ 'cursorModels',
41
+ 'cursorSettings',
42
+ 'cursorSetSettings',
43
+ 'ollamaSettings',
44
+ 'ollamaSetSettings',
45
+ 'ollamaModels',
46
+ 'image',
47
+ 'video',
48
+ 'prepareReset',
49
+ 'consumeReset',
50
+ 'speed',
51
+ 'setSpeed',
52
+ 'modelDefaults',
53
+ 'setModelDefault',
31
54
  ];
32
55
  /** Media types the attachment store accepts (ImageMediaType). */
33
- const IMAGE_MEDIA_TYPES = ['image/png', 'image/jpeg', 'image/webp', 'image/gif'];
56
+ const IMAGE_MEDIA_TYPES = [
57
+ 'image/png',
58
+ 'image/jpeg',
59
+ 'image/webp',
60
+ 'image/gif',
61
+ ];
34
62
  /** Bare MP4 file names the `video` endpoint accepts (no path separators). */
35
63
  const VIDEO_NAME_PATTERN = /^[\w.-]+\.mp4$/;
36
64
  /** Payload carried no usable provider id — an RPC client bug, not a server failure. */
@@ -40,9 +68,16 @@ function readEnvelope(body) {
40
68
  if (typeof body !== 'object' || body === null)
41
69
  return undefined;
42
70
  const record = body;
43
- if (record.type !== 'client-request' || typeof record.rpcId !== 'string' || typeof record.method !== 'string')
71
+ if (record.type !== 'client-request' ||
72
+ typeof record.rpcId !== 'string' ||
73
+ typeof record.method !== 'string')
44
74
  return undefined;
45
- return { type: 'client-request', rpcId: record.rpcId, method: record.method, payload: record.payload };
75
+ return {
76
+ type: 'client-request',
77
+ rpcId: record.rpcId,
78
+ method: record.method,
79
+ payload: record.payload,
80
+ };
46
81
  }
47
82
  function serverResponse(rpcId, result) {
48
83
  return Response.json({ type: 'server-response', rpcId, result });
@@ -62,8 +97,14 @@ function fetchRouteFor(endpoint, handler) {
62
97
  methods: ['POST'],
63
98
  requestBody: 'buffered',
64
99
  fetch: async (request) => {
65
- if (request.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase() !== 'application/json') {
66
- return new Response('content type must be application/json', { status: 415 });
100
+ if (request.headers
101
+ .get('content-type')
102
+ ?.split(';', 1)[0]
103
+ ?.trim()
104
+ .toLowerCase() !== 'application/json') {
105
+ return new Response('content type must be application/json', {
106
+ status: 415,
107
+ });
67
108
  }
68
109
  let body;
69
110
  try {
@@ -77,7 +118,11 @@ function fetchRouteFor(endpoint, handler) {
77
118
  const rawId = body?.rpcId;
78
119
  return serverResponse(typeof rawId === 'string' ? rawId : 'invalid-request', {
79
120
  ok: false,
80
- error: { code: 'gateway/bad-request', message: 'invalid client-request message', details: { issues: [] } },
121
+ error: {
122
+ code: 'gateway/bad-request',
123
+ message: 'invalid client-request message',
124
+ details: { issues: [] },
125
+ },
81
126
  });
82
127
  }
83
128
  if (envelope.method !== method) {
@@ -101,7 +146,10 @@ function failure(error) {
101
146
  const message = error instanceof Error ? error.message : String(error);
102
147
  if (error instanceof BadRequest) {
103
148
  // The issues array is zod-shaped upstream; this channel validates by hand.
104
- return { ok: false, error: { code: 'bad-request', message, details: { issues: [] } } };
149
+ return {
150
+ ok: false,
151
+ error: { code: 'bad-request', message, details: { issues: [] } },
152
+ };
105
153
  }
106
154
  return { ok: false, error: { code: 'internal', message, details: {} } };
107
155
  }
@@ -109,7 +157,8 @@ function readProvider(payload) {
109
157
  if (typeof payload !== 'object' || payload === null)
110
158
  throw new BadRequest('payload must be an object');
111
159
  const provider = payload.provider;
112
- if (typeof provider !== 'string' || !PROVIDER_IDS.includes(provider)) {
160
+ if (typeof provider !== 'string' ||
161
+ !PROVIDER_IDS.includes(provider)) {
113
162
  throw new BadRequest(`payload.provider must be one of ${PROVIDER_IDS.join(', ')}`);
114
163
  }
115
164
  return provider;
@@ -121,6 +170,18 @@ function readString(payload, field) {
121
170
  }
122
171
  return value;
123
172
  }
173
+ /** Read a field that may be omitted, rejecting an empty or non-string value. */
174
+ function readOptionalString(payload, field) {
175
+ if (typeof payload !== 'object' || payload === null)
176
+ throw new BadRequest('payload must be an object');
177
+ const value = payload[field];
178
+ if (value === undefined)
179
+ return undefined;
180
+ if (typeof value !== 'string' || value.trim().length === 0) {
181
+ throw new BadRequest(`payload.${field} must be a non-empty string when present`);
182
+ }
183
+ return value;
184
+ }
124
185
  /** Validate the `setModelDefault` endpoint's payload. */
125
186
  function readModelDefaultInput(payload) {
126
187
  const provider = readProvider(payload);
@@ -169,7 +230,8 @@ function readImageRef(payload) {
169
230
  throw new BadRequest('payload.attachmentId must be a non-empty string');
170
231
  }
171
232
  const mediaType = record.mediaType;
172
- if (typeof mediaType !== 'string' || !IMAGE_MEDIA_TYPES.includes(mediaType)) {
233
+ if (typeof mediaType !== 'string' ||
234
+ !IMAGE_MEDIA_TYPES.includes(mediaType)) {
173
235
  throw new BadRequest(`payload.mediaType must be one of ${IMAGE_MEDIA_TYPES.join(', ')}`);
174
236
  }
175
237
  for (const field of ['bytes', 'width', 'height']) {
@@ -188,7 +250,7 @@ function readImageRef(payload) {
188
250
  bytes: record.bytes,
189
251
  width: record.width,
190
252
  height: record.height,
191
- ...name === undefined ? {} : { name: name },
253
+ ...(name === undefined ? {} : { name: name }),
192
254
  };
193
255
  }
194
256
  /**
@@ -222,7 +284,7 @@ function readSessionId(payload) {
222
284
  throw new BadRequest('payload must be an object');
223
285
  return readString(payload, 'sessionId');
224
286
  }
225
- async function dispatch(controller, speed, modelDefaults, endpoint, payload, signal, providerSettings, externalUsage, cursor) {
287
+ async function dispatch(controller, speed, modelDefaults, endpoint, payload, signal, providerSettings, externalUsage, cursor, ollama) {
226
288
  switch (endpoint) {
227
289
  case 'providerSettings':
228
290
  if (!providerSettings)
@@ -239,11 +301,14 @@ async function dispatch(controller, speed, modelDefaults, endpoint, payload, sig
239
301
  // One provider's failure (a corrupt store entry, a broken flow) must not
240
302
  // blind the whole page: it degrades to an error detail on that provider
241
303
  // while the others still report their real status.
242
- const entries = await Promise.all(PROVIDER_IDS.map(async (provider) => [provider, await controller.status(provider).catch((error) => ({
304
+ const entries = await Promise.all(PROVIDER_IDS.map(async (provider) => [
305
+ provider,
306
+ await controller.status(provider).catch((error) => ({
243
307
  busy: false,
244
308
  accounts: [],
245
309
  detail: error instanceof Error ? error.message : String(error),
246
- }))]));
310
+ })),
311
+ ]));
247
312
  return ok({ providers: Object.fromEntries(entries) });
248
313
  }
249
314
  case 'login': {
@@ -322,6 +387,38 @@ async function dispatch(controller, speed, modelDefaults, endpoint, payload, sig
322
387
  throw new BadRequest('payload must be an object');
323
388
  await cursor.setSettings(payload.settings);
324
389
  return ok({ ok: true });
390
+ case 'ollamaSettings':
391
+ if (!ollama)
392
+ throw new BadRequest('Ollama settings are unavailable');
393
+ return ok(await ollama.settings());
394
+ case 'ollamaSetSettings':
395
+ if (!ollama)
396
+ throw new BadRequest('Ollama settings are unavailable');
397
+ if (payload === null || typeof payload !== 'object')
398
+ throw new BadRequest('payload must be an object');
399
+ await ollama.setSettings(payload.settings);
400
+ return ok({ ok: true });
401
+ case 'ollamaModels': {
402
+ if (!ollama)
403
+ throw new BadRequest('Ollama settings are unavailable');
404
+ const apiKey = readOptionalString(payload, 'apiKey');
405
+ const rawBaseURL = readOptionalString(payload, 'baseURL');
406
+ let baseURL;
407
+ if (rawBaseURL !== undefined) {
408
+ // The listing may run against a not-yet-saved draft address, so it
409
+ // passes the same URL check the store applies.
410
+ try {
411
+ baseURL = validateOllamaBaseURL(rawBaseURL);
412
+ }
413
+ catch (error) {
414
+ throw new BadRequest(error instanceof Error ? error.message : String(error));
415
+ }
416
+ }
417
+ return ok(await ollama.models({
418
+ ...(apiKey === undefined ? {} : { apiKey }),
419
+ ...(baseURL === undefined ? {} : { baseURL }),
420
+ }, signal));
421
+ }
325
422
  case 'prepareReset':
326
423
  if (!controller.prepareReset)
327
424
  throw new BadRequest('Reset redemption unavailable');
@@ -363,8 +460,12 @@ async function dispatch(controller, speed, modelDefaults, endpoint, payload, sig
363
460
  * @param controller - the auth operations backing the endpoints.
364
461
  * @param speed - the per-session speed-tier state backing the Speed toggle.
365
462
  * @param modelDefaults - optional per-model default-effort state backing `modelDefaults`/`setModelDefault`.
463
+ * @param providerSettings - optional OAuth provider/account preferences.
464
+ * @param externalUsage - optional credential-backed quota readers.
465
+ * @param cursor - optional Cursor account and model-visibility actions.
466
+ * @param ollama - optional Ollama Cloud catalog and connection settings.
366
467
  */
367
- export function registerAuthRpc(ctx, controller, speed, modelDefaults = undefined, providerSettings = undefined, externalUsage = undefined, cursor = undefined) {
468
+ export function registerAuthRpc(ctx, controller, speed, modelDefaults = undefined, providerSettings = undefined, externalUsage = undefined, cursor = undefined, ollama = undefined) {
368
469
  // `connection` is not in this plugin's inject list (headless compositions
369
470
  // lack it), so its startup order is unconstrained: defer registration until
370
471
  // the service exists instead of probing once at apply time.
@@ -381,7 +482,7 @@ export function registerAuthRpc(ctx, controller, speed, modelDefaults = undefine
381
482
  const connection = ctx.get('connection');
382
483
  const handler = async (endpoint, payload, signal) => {
383
484
  try {
384
- return await dispatch(controller, speed, modelDefaults, endpoint, payload, signal, providerSettings, externalUsage, cursor);
485
+ return await dispatch(controller, speed, modelDefaults, endpoint, payload, signal, providerSettings, externalUsage, cursor, ollama);
385
486
  }
386
487
  catch (error) {
387
488
  return failure(error);
package/lib/auth/store.js CHANGED
@@ -18,7 +18,13 @@ import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
18
18
  import { dirname } from 'node:path';
19
19
  import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
20
20
  /** Every provider route, in display order. */
21
- export const PROVIDER_IDS = ['codex', 'claude', 'grok', 'copilot', 'antigravity'];
21
+ export const PROVIDER_IDS = [
22
+ 'codex',
23
+ 'claude',
24
+ 'grok',
25
+ 'copilot',
26
+ 'antigravity',
27
+ ];
22
28
  /**
23
29
  * The stable identity of one session's account: Codex keys on workspace AND
24
30
  * user (email fallback, workspace-only for unidentified legacy sessions),
@@ -37,40 +43,53 @@ export function accountKeyOf(provider, session) {
37
43
  switch (provider) {
38
44
  case 'codex': {
39
45
  const codex = session;
40
- const payload = typeof codex.idToken === 'string' ? decodeJwtPayload(codex.idToken) : undefined;
46
+ const payload = typeof codex.idToken === 'string'
47
+ ? decodeJwtPayload(codex.idToken)
48
+ : undefined;
41
49
  const auth = claimObject(payload?.['https://api.openai.com/auth']);
42
50
  const user = nonEmpty(auth?.chatgpt_user_id) ?? nonEmpty(auth?.user_id);
43
51
  const profile = claimObject(payload?.['https://api.openai.com/profile']);
44
- const email = nonEmpty(codex.emailAddress) ?? nonEmpty(payload?.email) ?? nonEmpty(profile?.email);
52
+ const email = nonEmpty(codex.emailAddress) ??
53
+ nonEmpty(payload?.email) ??
54
+ nonEmpty(profile?.email);
45
55
  if (user === undefined && email === undefined)
46
56
  return codex.accountId;
47
57
  // JSON tuple encoding avoids separator collisions and distinguishes IDs from emails.
48
- return JSON.stringify([codex.accountId, user === undefined ? 'email' : 'user', user ?? email.toLowerCase()]);
58
+ return JSON.stringify([
59
+ codex.accountId,
60
+ user === undefined ? 'email' : 'user',
61
+ user ?? email.toLowerCase(),
62
+ ]);
49
63
  }
50
64
  case 'claude':
51
- return session.emailAddress ?? tokenHash(session.refreshToken);
65
+ return (session.emailAddress ??
66
+ tokenHash(session.refreshToken));
52
67
  case 'grok':
53
68
  return session.account ?? tokenHash(session.refreshToken);
54
69
  case 'antigravity':
55
- return session.account ?? tokenHash(session.refreshToken);
70
+ return (session.account ??
71
+ tokenHash(session.refreshToken));
56
72
  case 'copilot':
57
- return session.account ?? tokenHash(session.refreshToken);
73
+ return (session.account ?? tokenHash(session.refreshToken));
58
74
  }
59
75
  }
60
76
  function nonEmpty(value) {
61
- return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
77
+ return typeof value === 'string' && value.trim().length > 0
78
+ ? value.trim()
79
+ : undefined;
62
80
  }
63
81
  function claimObject(value) {
64
82
  return typeof value === 'object' && value !== null && !Array.isArray(value)
65
- ? value : undefined;
83
+ ? value
84
+ : undefined;
66
85
  }
67
86
  /** Resolve only recorded aliases, not an ambiguous workspace-wide match. */
68
87
  function resolveAccount(entry, key) {
69
88
  const seen = new Set();
70
- while (!Object.hasOwn(entry.accounts, key)
71
- && entry.aliases !== undefined
72
- && Object.hasOwn(entry.aliases, key)
73
- && !seen.has(key)) {
89
+ while (!Object.hasOwn(entry.accounts, key) &&
90
+ entry.aliases !== undefined &&
91
+ Object.hasOwn(entry.aliases, key) &&
92
+ !seen.has(key)) {
74
93
  seen.add(key);
75
94
  key = entry.aliases[key];
76
95
  }
@@ -107,9 +126,13 @@ export async function resolveAccountKey(provider, account, path = authFilePath()
107
126
  }
108
127
  /** Migrate workspace-only keys once; retain collisions rather than discard credentials. */
109
128
  function codexEmail(session) {
110
- const payload = typeof session.idToken === 'string' ? decodeJwtPayload(session.idToken) : undefined;
129
+ const payload = typeof session.idToken === 'string'
130
+ ? decodeJwtPayload(session.idToken)
131
+ : undefined;
111
132
  const profile = claimObject(payload?.['https://api.openai.com/profile']);
112
- return (nonEmpty(session.emailAddress) ?? nonEmpty(payload?.email) ?? nonEmpty(profile?.email))?.toLowerCase();
133
+ return (nonEmpty(session.emailAddress) ??
134
+ nonEmpty(payload?.email) ??
135
+ nonEmpty(profile?.email))?.toLowerCase();
113
136
  }
114
137
  function codexUserKey(session) {
115
138
  const key = accountKeyOf('codex', session);
@@ -179,13 +202,16 @@ function assertSessionShape(provider, account, value) {
179
202
  throw new Error(`subscriptions auth store: entry "${provider}/${account}" is not an object; fix or delete the store file`);
180
203
  }
181
204
  const entry = value;
182
- if (typeof entry.accessToken !== 'string' || entry.accessToken.length === 0
183
- || typeof entry.refreshToken !== 'string' || entry.refreshToken.length === 0
184
- || typeof entry.expiresAt !== 'number' || !Number.isFinite(entry.expiresAt)) {
205
+ if (typeof entry.accessToken !== 'string' ||
206
+ entry.accessToken.length === 0 ||
207
+ typeof entry.refreshToken !== 'string' ||
208
+ entry.refreshToken.length === 0 ||
209
+ typeof entry.expiresAt !== 'number' ||
210
+ !Number.isFinite(entry.expiresAt)) {
185
211
  throw new Error(`subscriptions auth store: entry "${provider}/${account}" is missing accessToken/refreshToken/expiresAt; fix or delete the store file`);
186
212
  }
187
- if (provider === 'antigravity'
188
- && (typeof entry.projectId !== 'string' || entry.projectId.length === 0)) {
213
+ if (provider === 'antigravity' &&
214
+ (typeof entry.projectId !== 'string' || entry.projectId.length === 0)) {
189
215
  throw new Error('subscriptions auth store: entry "antigravity" is missing projectId; log out and complete Antigravity login again');
190
216
  }
191
217
  }
@@ -261,12 +287,19 @@ function parseStore(text, path) {
261
287
  continue;
262
288
  }
263
289
  const session = record;
264
- const key = provider === 'codex' ? session.accountId : accountKeyOf(provider, session);
265
- store[provider] = { default: key, accounts: { [key]: session } };
290
+ const key = provider === 'codex'
291
+ ? session.accountId
292
+ : accountKeyOf(provider, session);
293
+ store[provider] = {
294
+ default: key,
295
+ accounts: { [key]: session },
296
+ };
266
297
  continue;
267
298
  }
268
299
  const accounts = record.accounts;
269
- if (typeof accounts !== 'object' || accounts === null || Array.isArray(accounts)) {
300
+ if (typeof accounts !== 'object' ||
301
+ accounts === null ||
302
+ Array.isArray(accounts)) {
270
303
  console.warn(`subscriptions auth store: entry "${provider}" has no accounts map; skipped`);
271
304
  continue;
272
305
  }
@@ -288,7 +321,11 @@ function parseStore(text, path) {
288
321
  const validDefault = record.default === undefined || record.default in kept
289
322
  ? record.default
290
323
  : Object.keys(kept)[0];
291
- store[provider] = { ...record, default: validDefault, accounts: kept };
324
+ store[provider] = {
325
+ ...record,
326
+ default: validDefault,
327
+ accounts: kept,
328
+ };
292
329
  }
293
330
  if (store.codex !== undefined)
294
331
  migrateCodex(store.codex);
@@ -299,9 +336,12 @@ function isValidSessionShape(value) {
299
336
  if (typeof value !== 'object' || value === null)
300
337
  return false;
301
338
  const entry = value;
302
- return typeof entry.accessToken === 'string' && entry.accessToken.length > 0
303
- && typeof entry.refreshToken === 'string' && entry.refreshToken.length > 0
304
- && typeof entry.expiresAt === 'number' && Number.isFinite(entry.expiresAt);
339
+ return (typeof entry.accessToken === 'string' &&
340
+ entry.accessToken.length > 0 &&
341
+ typeof entry.refreshToken === 'string' &&
342
+ entry.refreshToken.length > 0 &&
343
+ typeof entry.expiresAt === 'number' &&
344
+ Number.isFinite(entry.expiresAt));
305
345
  }
306
346
  /** Persist the whole store atomically with owner-only permissions. */
307
347
  async function writeStore(store, path) {
@@ -361,7 +401,10 @@ export async function listAccounts(provider, path = authFilePath()) {
361
401
  const entry = (await loadStore(path))[provider];
362
402
  if (entry === undefined)
363
403
  return [];
364
- const accounts = Object.entries(entry.accounts).map(([key, session]) => ({ key, session }));
404
+ const accounts = Object.entries(entry.accounts).map(([key, session]) => ({
405
+ key,
406
+ session,
407
+ }));
365
408
  accounts.sort((a, b) => Number(b.key === entry.default) - Number(a.key === entry.default));
366
409
  return accounts;
367
410
  }
@@ -402,7 +445,8 @@ export async function saveAccountSession(provider, account, session, path = auth
402
445
  if (entry !== undefined) {
403
446
  account = resolveAccount(entry, account);
404
447
  if (provider === 'codex') {
405
- account = reconcileCodexIdentity(entry, session) ?? accountKeyOf('codex', session);
448
+ account =
449
+ reconcileCodexIdentity(entry, session) ?? accountKeyOf('codex', session);
406
450
  }
407
451
  }
408
452
  ;
@@ -439,7 +483,9 @@ export async function deleteAccountSession(provider, account, path = authFilePat
439
483
  ;
440
484
  store[provider] = {
441
485
  ...entry,
442
- ...entry.default === account ? { default: Object.keys(accounts)[0] } : { default: entry.default },
486
+ ...(entry.default === account
487
+ ? { default: Object.keys(accounts)[0] }
488
+ : { default: entry.default }),
443
489
  accounts,
444
490
  };
445
491
  }
@@ -462,7 +508,10 @@ export async function setDefaultAccount(provider, account, path = authFilePath()
462
508
  throw new Error(`no ${provider} account "${account}" is logged in`);
463
509
  }
464
510
  ;
465
- store[provider] = { ...entry, default: account };
511
+ store[provider] = {
512
+ ...entry,
513
+ default: account,
514
+ };
466
515
  await writeStore(store, path);
467
516
  });
468
517
  }
@@ -2,7 +2,7 @@ import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client';
2
2
  import type { SubscriptionsSectionInjected } from './SubscriptionsSection.js';
3
3
  type Translate = SubscriptionsSectionInjected['t'];
4
4
  /** Cursor's single connected account, rendered with the same card and Manage dialog as the other providers. */
5
- export declare function CursorCard({ rpc, t }: {
5
+ export declare function CursorCard({ rpc, t, }: {
6
6
  rpc: ConnectionHandle['rpc'];
7
7
  t: Translate;
8
8
  }): import("react").JSX.Element;