@prestyj/core 5.11.0 → 5.13.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/dist/index.d.cts CHANGED
@@ -12,7 +12,7 @@ declare function getNextThinkingLevel(provider: Provider, model: string, current
12
12
  * and any other OpenAI-compatible server the user points us at.
13
13
  *
14
14
  * Everything rides the OpenAI-compatible `/v1` transport (see the `local`
15
- * provider in gg-ai's stream.ts); the only per-server difference is where the
15
+ * provider in @prestyj/ai's stream.ts); the only per-server difference is where the
16
16
  * *capabilities* come from, because `GET /v1/models` reports nothing useful:
17
17
  *
18
18
  * - Ollama → `POST /api/show` → `capabilities[]` + `model_info["<arch>.context_length"]`
@@ -185,7 +185,16 @@ interface OAuthCredentials {
185
185
  }
186
186
  interface OAuthLoginCallbacks {
187
187
  onOpenUrl: (url: string) => void;
188
- onPromptCode: (message: string) => Promise<string>;
188
+ /**
189
+ * Collect a pasted authorization code or callback URL.
190
+ *
191
+ * `signal` aborts when the code already arrived another way (the local
192
+ * callback listener won the race) and the prompt should be torn down.
193
+ * Implementations may ignore it — an abandoned promise is simply never
194
+ * awaited — but a terminal prompt should honour it so the line does not
195
+ * linger after login already succeeded.
196
+ */
197
+ onPromptCode: (message: string, signal?: AbortSignal) => Promise<string>;
189
198
  onStatus: (message: string) => void;
190
199
  }
191
200
 
@@ -195,6 +204,43 @@ interface OAuthLoginCallbacks {
195
204
  * prefer OAuth for the logical `moonshot` provider.
196
205
  */
197
206
  declare const MOONSHOT_OAUTH_KEY = "moonshot-oauth";
207
+ /**
208
+ * Storage key for Grok (xAI) subscription OAuth credentials. Kept distinct from
209
+ * the `xai` API-key entry for the same reason as Kimi's: a user may configure
210
+ * BOTH — a SuperGrok/X Premium subscription plus a metered console key — and we
211
+ * always prefer OAuth for the logical `xai` provider.
212
+ */
213
+ declare const XAI_OAUTH_KEY = "xai-oauth";
214
+ /**
215
+ * A provider that can hold two credentials at once: a refreshable subscription
216
+ * OAuth token and a static API key. One policy governs all of them — see
217
+ * {@link DUAL_AUTH_PROVIDERS} — so adding a provider here is enough to give it
218
+ * OAuth-first resolution, usage-exhaustion fallback, per-method logout and the
219
+ * matching UI affordances.
220
+ */
221
+ interface DualAuthProvider {
222
+ /** Logical provider id, which is also the API-key storage key. */
223
+ provider: string;
224
+ /** Storage key holding the OAuth credential. */
225
+ oauthKey: string;
226
+ /** Human label for the OAuth credential (log/UI wording). */
227
+ oauthLabel: string;
228
+ /** Human label for the API-key credential (log/UI wording). */
229
+ apiKeyLabel: string;
230
+ /** What the user should do to restore OAuth after it went invalid. */
231
+ restoreHint: string;
232
+ }
233
+ /** Dual-auth policy for a logical provider, or undefined if it has just one method. */
234
+ declare function dualAuthProvider(provider: string): DualAuthProvider | undefined;
235
+ /** Dual-auth policy keyed by the OAuth storage key (the reverse lookup). */
236
+ declare function dualAuthProviderByOAuthKey(storageKey: string): DualAuthProvider | undefined;
237
+ /** The OAuth storage key for a dual-auth provider, if it has one. */
238
+ declare function oauthStorageKey(provider: string): string | undefined;
239
+ /**
240
+ * Both storage keys a dual-auth provider may hold, in resolution order
241
+ * (OAuth first). Single-method providers yield just their own key.
242
+ */
243
+ declare function providerStorageKeys(provider: string): string[];
198
244
  /**
199
245
  * Storage key for the Xiaomi API Credits credential (`https://api.xiaomimimo.com/v1`).
200
246
  * Kept distinct from the `xiaomi` Token Plan entry (`token-plan-sgp.xiaomimimo.com`)
@@ -224,6 +270,16 @@ declare class AuthStorage {
224
270
  private data;
225
271
  private filePath;
226
272
  private loaded;
273
+ /**
274
+ * mtime+size of the file as of the cached snapshot (`size: -1` = no file).
275
+ * auth.json is shared: the desktop app writes API keys and disconnects
276
+ * NATIVELY (so they work with no daemon running), and every window/process has
277
+ * its own AuthStorage. A load-once cache therefore goes stale — the sidecar
278
+ * would keep listing models for a provider just disconnected, and hide the
279
+ * ones just connected, until the daemon restarted.
280
+ */
281
+ private snapshotMtimeMs;
282
+ private snapshotSize;
227
283
  /** Per-provider lock to serialize concurrent refresh calls. */
228
284
  private refreshLocks;
229
285
  constructor(filePath?: string);
@@ -242,9 +298,9 @@ declare class AuthStorage {
242
298
  */
243
299
  pickStorageKey(keys: string[]): Promise<string | undefined>;
244
300
  /**
245
- * True if the user has any usable auth for the logical provider. For
246
- * `moonshot` this is satisfied by either the Kimi OAuth credential or the
247
- * Moonshot API key.
301
+ * True if the user has any usable auth for the logical provider. For a
302
+ * dual-auth provider (Kimi/Grok) either the OAuth credential or the API key
303
+ * satisfies it.
248
304
  */
249
305
  hasProviderAuth(provider: string): Promise<boolean>;
250
306
  /** Endpoint ids that currently have a `local:<id>` credential stored. */
@@ -259,16 +315,17 @@ declare class AuthStorage {
259
315
  removeLocalEndpoint(endpointId: string): Promise<void>;
260
316
  /**
261
317
  * True if the active credential for `provider` is a static API key with no
262
- * refresh mechanism. For `moonshot` this is only true when the Kimi OAuth
263
- * credential is absent (a present OAuth credential is refreshable).
318
+ * refresh mechanism. For a dual-auth provider this is only true when its OAuth
319
+ * credential is absent or sidelined (a live OAuth credential is refreshable).
264
320
  */
265
321
  isStaticApiKey(provider: string): Promise<boolean>;
266
322
  /**
267
323
  * The base URL on the credential that is active right now, if any.
268
324
  * Synchronous — call only after load()/resolveCredentials() populated the
269
- * snapshot. For `moonshot` this is the Kimi For Coding URL whenever the
270
- * OAuth entry is the one resolveCredentials would serve (i.e. not currently
271
- * usage-exhausted with an API key configured).
325
+ * snapshot. For a dual-auth provider this is the subscription endpoint (Kimi
326
+ * For Coding, the Grok CLI proxy) whenever the OAuth entry is the one
327
+ * resolveCredentials would serve (i.e. not currently usage-exhausted with an
328
+ * API key configured).
272
329
  */
273
330
  getStoredBaseUrl(provider: string): string | undefined;
274
331
  load(): Promise<void>;
@@ -279,6 +336,20 @@ declare class AuthStorage {
279
336
  * writes API keys natively without going through this instance.
280
337
  */
281
338
  reload(): Promise<void>;
339
+ /**
340
+ * Like {@link ensureLoaded}, but re-reads when the file changed since this
341
+ * snapshot — a cheap stat, not a re-parse. Used by the "what is connected?"
342
+ * readers, which must reflect writes made by another window, the CLI, or the
343
+ * desktop app's native (daemon-free) API-key and disconnect paths.
344
+ *
345
+ * Deliberately NOT used by {@link resolveCredentials}: that path compares the
346
+ * caller's snapshot against the latest file to detect a concurrent re-login,
347
+ * and silently refreshing this instance's view first would destroy the
348
+ * evidence that the token it just had rejected has already been replaced.
349
+ */
350
+ private ensureFresh;
351
+ /** Record the file identity behind the current snapshot. */
352
+ private rememberSnapshot;
282
353
  /**
283
354
  * Apply one provider-scoped mutation to the latest on-disk snapshot.
284
355
  * AuthStorage instances live in every app session/process, so writing this
@@ -296,7 +367,7 @@ declare class AuthStorage {
296
367
  * `resetsAt` (unix SECONDS, from the provider's rate-limit response) or a
297
368
  * 15-minute default when no reset time is known. While the mark is in the
298
369
  * future, `resolveCredentials("moonshot")` serves the Moonshot API key
299
- * instead of the Kimi OAuth credential (when both are configured) — OAuth
370
+ * instead of the subscription OAuth credential (when both are configured) — OAuth
300
371
  * stays the preferred credential and is retried automatically once the mark
301
372
  * lapses. Persisted to auth.json so a restart (or another ezcoder-app window)
302
373
  * doesn't burn a request rediscovering the same exhausted window. No-op if
@@ -306,13 +377,27 @@ declare class AuthStorage {
306
377
  clearAll(): Promise<void>;
307
378
  /**
308
379
  * Returns valid credentials, auto-refreshing if expired.
380
+ *
309
381
  * If `forceRefresh` is true, refreshes even if the token hasn't expired
310
- * (useful when the provider rejects a token with 401 before its stored expiry).
382
+ * (useful when the provider rejects a token with 401 before its stored
383
+ * expiry). Callers recovering from a rejection should also pass
384
+ * `rejectedToken` — see the stampede guard below.
385
+ *
311
386
  * Throws if not logged in.
312
387
  */
313
388
  resolveCredentials(provider: string, opts?: {
314
389
  forceRefresh?: boolean;
315
390
  storageKeys?: string[];
391
+ /**
392
+ * The access token the provider just rejected. Refreshing an OAuth grant
393
+ * invalidates the previous access token, so N processes sharing auth.json
394
+ * (app windows, CLI sessions, the usage poller) can otherwise revoke each
395
+ * other in a loop: each one force-refreshes on 401, and every refresh kills
396
+ * the token the others still hold. Naming the rejected token lets the
397
+ * refresh path tell "this credential is genuinely dead" from "someone else
398
+ * already rotated it" and simply adopt the newer on-disk credential.
399
+ */
400
+ rejectedToken?: string;
316
401
  }): Promise<OAuthCredentials>;
317
402
  /**
318
403
  * Returns a valid access token, auto-refreshing if expired.
@@ -417,6 +502,69 @@ declare function loginKimi(callbacks: OAuthLoginCallbacks): Promise<OAuthCredent
417
502
  /** Exchange a refresh token for a fresh Kimi access token. */
418
503
  declare function refreshKimiToken(refreshToken: string): Promise<OAuthCredentials>;
419
504
 
505
+ /**
506
+ * Grok (xAI) subscription OAuth — Device Authorization Grant (RFC 8628).
507
+ *
508
+ * Two form-encoded POST endpoints against xAI's OIDC issuer (`https://auth.x.ai`,
509
+ * advertised by its `/.well-known/openid-configuration`):
510
+ *
511
+ * - `/oauth2/device/code` (client_id + scope) → device + user code
512
+ * - `/oauth2/token` (grant_type=device_code) → poll until authorized
513
+ * - `/oauth2/token` (grant_type=refresh_token) → refresh access token
514
+ *
515
+ * Like Kimi (and unlike Anthropic/OpenAI/Gemini's browser-redirect PKCE) this is
516
+ * a device-code/poll flow: show a URL + code, the user authorizes in a browser on
517
+ * any device, we poll for the token. Deliberately chosen over the loopback PKCE
518
+ * variant because xAI pins the Grok-CLI client's redirect to
519
+ * `http://127.0.0.1:56121/callback` — a fixed port we cannot rebind if it's busy,
520
+ * and unreachable from a container/SSH session. Device code has neither problem.
521
+ *
522
+ * The issued token is used against the Grok CLI's chat proxy
523
+ * (`https://cli-chat-proxy.grok.com/v1`, distinct from the `api.x.ai` API-key
524
+ * endpoint) — that is the surface the `grok-cli:access` scope grants, and it
525
+ * bills against the user's SuperGrok / X Premium subscription instead of metered
526
+ * API credits. We persist that base URL on the credential so the runtime routes
527
+ * there automatically; `grokCliHeaders()` supplies the client identity the proxy
528
+ * requires (attached centrally in @prestyj/ai's `xai` transport).
529
+ *
530
+ * Caveats worth knowing, both observed in the wild and surfaced to users rather
531
+ * than hidden here:
532
+ * - The client id below is xAI's public Grok-CLI desktop client (no secret).
533
+ * Every third-party implementation reuses it; xAI has not published a
534
+ * partner-client program, so subscription OAuth is a best-effort path.
535
+ * - xAI gates proxy access by subscription tier. A perfectly valid login can
536
+ * still be refused at inference time, which is exactly why `xai` keeps its
537
+ * API-key method as a fallback (see AuthStorage's dual-auth resolution).
538
+ */
539
+
540
+ /** Grok CLI chat-proxy base URL the issued OAuth token is used against. */
541
+ declare function grokCliBaseUrl(): string;
542
+ /**
543
+ * Headers the Grok CLI chat proxy requires on every model request. It serves
544
+ * only recognized Grok-CLI clients: without the token-auth marker and a client
545
+ * version it refuses the request. `modelId` populates the model-override header
546
+ * the proxy uses to route a request to the entitled model.
547
+ *
548
+ * Attach these ONLY to the proxy — the `api.x.ai` API-key path must not receive
549
+ * them (see {@link isGrokCliEndpoint}).
550
+ */
551
+ declare function grokCliHeaders(modelId?: string): Record<string, string>;
552
+ /**
553
+ * True if `baseUrl` targets the Grok CLI chat proxy (the URL persisted on Grok
554
+ * OAuth credentials). Callers use this to decide whether to attach
555
+ * {@link grokCliHeaders} and whether a usage/permission rejection should fall
556
+ * back to the xAI API key.
557
+ */
558
+ declare function isGrokCliEndpoint(baseUrl: string | undefined): boolean;
559
+ /**
560
+ * Drive the Grok device-code flow end-to-end. Shows the verification URL + user
561
+ * code via callbacks, opens the browser, and polls until the user authorizes or
562
+ * the device code expires (deadline set by the server).
563
+ */
564
+ declare function loginXai(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials>;
565
+ /** Exchange a refresh token for a fresh Grok access token. */
566
+ declare function refreshXaiToken(refreshToken: string): Promise<OAuthCredentials>;
567
+
420
568
  /**
421
569
  * Minimal Telegram Bot API client using raw fetch().
422
570
  * Supports long polling, markdown messages, inline keyboards, and message splitting.
@@ -590,4 +738,4 @@ interface AutoUpdater {
590
738
  }
591
739
  declare function createAutoUpdater(config: AutoUpdateConfig): AutoUpdater;
592
740
 
593
- export { AuthStorage, type AutoUpdateConfig, type AutoUpdater, DEFAULT_LOCAL_ENDPOINTS, type DiscoverOptions, type DiscoveryResult, FALLBACK_CONTEXT_WINDOW, type InlineButton, LOCAL_API_KEY_PLACEHOLDER, LOCAL_AUTH_KEY_PREFIX, type LocalEndpoint, type LocalEndpointKind, type LocalEndpointProbe, type LocalModel, type LogLevel, MOONSHOT_OAUTH_KEY, ModelInfo, NotLoggedInError, type OAuthCredentials, type OAuthLoginCallbacks, type ProbeOptions, type ProgressCallback, SubscriptionUsageError, type SubscriptionUsageProvider, type SubscriptionUsageSnapshot, type SubscriptionUsageWindow, TelegramBot, type TelegramConfig, type TelegramMessage, type TelegramUpdate, type TelegramVoiceMessage, XIAOMI_CREDITS_KEY, clearLocalDiscoveryCache, closeLogger, createAutoUpdater, decodeOggOpus, discoverLocalModels, downmixToMono, endpointRoot, fetchSubscriptionUsage, findProbedModel, formatLocalModelId, generatePKCE, getClaudeCliUserAgent, getClaudeCodeVersion, getNextThinkingLevel, getSessionId, getSupportedThinkingLevels, isKimiCodingEndpoint, isLocalModelId, isLoggerOpen, isModelLoaded, isThinkingLevelSupported, kimiCodeBaseUrl, kimiCodingHeaders, localAuthStorageKey, log, loginAnthropic, loginGemini, loginKimi, loginOpenAI, openLog, parseLocalModelId, probeEndpoint, readStoredBaseUrlSync, refreshAnthropicToken, refreshGeminiToken, refreshKimiToken, refreshOpenAIToken, registerLogCleanup, resample, setProgressCallback, toModelInfo, transcribeVoice, withFileLock };
741
+ export { AuthStorage, type AutoUpdateConfig, type AutoUpdater, DEFAULT_LOCAL_ENDPOINTS, type DiscoverOptions, type DiscoveryResult, type DualAuthProvider, FALLBACK_CONTEXT_WINDOW, type InlineButton, LOCAL_API_KEY_PLACEHOLDER, LOCAL_AUTH_KEY_PREFIX, type LocalEndpoint, type LocalEndpointKind, type LocalEndpointProbe, type LocalModel, type LogLevel, MOONSHOT_OAUTH_KEY, ModelInfo, NotLoggedInError, type OAuthCredentials, type OAuthLoginCallbacks, type ProbeOptions, type ProgressCallback, SubscriptionUsageError, type SubscriptionUsageProvider, type SubscriptionUsageSnapshot, type SubscriptionUsageWindow, TelegramBot, type TelegramConfig, type TelegramMessage, type TelegramUpdate, type TelegramVoiceMessage, XAI_OAUTH_KEY, XIAOMI_CREDITS_KEY, clearLocalDiscoveryCache, closeLogger, createAutoUpdater, decodeOggOpus, discoverLocalModels, downmixToMono, dualAuthProvider, dualAuthProviderByOAuthKey, endpointRoot, fetchSubscriptionUsage, findProbedModel, formatLocalModelId, generatePKCE, getClaudeCliUserAgent, getClaudeCodeVersion, getNextThinkingLevel, getSessionId, getSupportedThinkingLevels, grokCliBaseUrl, grokCliHeaders, isGrokCliEndpoint, isKimiCodingEndpoint, isLocalModelId, isLoggerOpen, isModelLoaded, isThinkingLevelSupported, kimiCodeBaseUrl, kimiCodingHeaders, localAuthStorageKey, log, loginAnthropic, loginGemini, loginKimi, loginOpenAI, loginXai, oauthStorageKey, openLog, parseLocalModelId, probeEndpoint, providerStorageKeys, readStoredBaseUrlSync, refreshAnthropicToken, refreshGeminiToken, refreshKimiToken, refreshOpenAIToken, refreshXaiToken, registerLogCleanup, resample, setProgressCallback, toModelInfo, transcribeVoice, withFileLock };
package/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@ declare function getNextThinkingLevel(provider: Provider, model: string, current
12
12
  * and any other OpenAI-compatible server the user points us at.
13
13
  *
14
14
  * Everything rides the OpenAI-compatible `/v1` transport (see the `local`
15
- * provider in gg-ai's stream.ts); the only per-server difference is where the
15
+ * provider in @prestyj/ai's stream.ts); the only per-server difference is where the
16
16
  * *capabilities* come from, because `GET /v1/models` reports nothing useful:
17
17
  *
18
18
  * - Ollama → `POST /api/show` → `capabilities[]` + `model_info["<arch>.context_length"]`
@@ -185,7 +185,16 @@ interface OAuthCredentials {
185
185
  }
186
186
  interface OAuthLoginCallbacks {
187
187
  onOpenUrl: (url: string) => void;
188
- onPromptCode: (message: string) => Promise<string>;
188
+ /**
189
+ * Collect a pasted authorization code or callback URL.
190
+ *
191
+ * `signal` aborts when the code already arrived another way (the local
192
+ * callback listener won the race) and the prompt should be torn down.
193
+ * Implementations may ignore it — an abandoned promise is simply never
194
+ * awaited — but a terminal prompt should honour it so the line does not
195
+ * linger after login already succeeded.
196
+ */
197
+ onPromptCode: (message: string, signal?: AbortSignal) => Promise<string>;
189
198
  onStatus: (message: string) => void;
190
199
  }
191
200
 
@@ -195,6 +204,43 @@ interface OAuthLoginCallbacks {
195
204
  * prefer OAuth for the logical `moonshot` provider.
196
205
  */
197
206
  declare const MOONSHOT_OAUTH_KEY = "moonshot-oauth";
207
+ /**
208
+ * Storage key for Grok (xAI) subscription OAuth credentials. Kept distinct from
209
+ * the `xai` API-key entry for the same reason as Kimi's: a user may configure
210
+ * BOTH — a SuperGrok/X Premium subscription plus a metered console key — and we
211
+ * always prefer OAuth for the logical `xai` provider.
212
+ */
213
+ declare const XAI_OAUTH_KEY = "xai-oauth";
214
+ /**
215
+ * A provider that can hold two credentials at once: a refreshable subscription
216
+ * OAuth token and a static API key. One policy governs all of them — see
217
+ * {@link DUAL_AUTH_PROVIDERS} — so adding a provider here is enough to give it
218
+ * OAuth-first resolution, usage-exhaustion fallback, per-method logout and the
219
+ * matching UI affordances.
220
+ */
221
+ interface DualAuthProvider {
222
+ /** Logical provider id, which is also the API-key storage key. */
223
+ provider: string;
224
+ /** Storage key holding the OAuth credential. */
225
+ oauthKey: string;
226
+ /** Human label for the OAuth credential (log/UI wording). */
227
+ oauthLabel: string;
228
+ /** Human label for the API-key credential (log/UI wording). */
229
+ apiKeyLabel: string;
230
+ /** What the user should do to restore OAuth after it went invalid. */
231
+ restoreHint: string;
232
+ }
233
+ /** Dual-auth policy for a logical provider, or undefined if it has just one method. */
234
+ declare function dualAuthProvider(provider: string): DualAuthProvider | undefined;
235
+ /** Dual-auth policy keyed by the OAuth storage key (the reverse lookup). */
236
+ declare function dualAuthProviderByOAuthKey(storageKey: string): DualAuthProvider | undefined;
237
+ /** The OAuth storage key for a dual-auth provider, if it has one. */
238
+ declare function oauthStorageKey(provider: string): string | undefined;
239
+ /**
240
+ * Both storage keys a dual-auth provider may hold, in resolution order
241
+ * (OAuth first). Single-method providers yield just their own key.
242
+ */
243
+ declare function providerStorageKeys(provider: string): string[];
198
244
  /**
199
245
  * Storage key for the Xiaomi API Credits credential (`https://api.xiaomimimo.com/v1`).
200
246
  * Kept distinct from the `xiaomi` Token Plan entry (`token-plan-sgp.xiaomimimo.com`)
@@ -224,6 +270,16 @@ declare class AuthStorage {
224
270
  private data;
225
271
  private filePath;
226
272
  private loaded;
273
+ /**
274
+ * mtime+size of the file as of the cached snapshot (`size: -1` = no file).
275
+ * auth.json is shared: the desktop app writes API keys and disconnects
276
+ * NATIVELY (so they work with no daemon running), and every window/process has
277
+ * its own AuthStorage. A load-once cache therefore goes stale — the sidecar
278
+ * would keep listing models for a provider just disconnected, and hide the
279
+ * ones just connected, until the daemon restarted.
280
+ */
281
+ private snapshotMtimeMs;
282
+ private snapshotSize;
227
283
  /** Per-provider lock to serialize concurrent refresh calls. */
228
284
  private refreshLocks;
229
285
  constructor(filePath?: string);
@@ -242,9 +298,9 @@ declare class AuthStorage {
242
298
  */
243
299
  pickStorageKey(keys: string[]): Promise<string | undefined>;
244
300
  /**
245
- * True if the user has any usable auth for the logical provider. For
246
- * `moonshot` this is satisfied by either the Kimi OAuth credential or the
247
- * Moonshot API key.
301
+ * True if the user has any usable auth for the logical provider. For a
302
+ * dual-auth provider (Kimi/Grok) either the OAuth credential or the API key
303
+ * satisfies it.
248
304
  */
249
305
  hasProviderAuth(provider: string): Promise<boolean>;
250
306
  /** Endpoint ids that currently have a `local:<id>` credential stored. */
@@ -259,16 +315,17 @@ declare class AuthStorage {
259
315
  removeLocalEndpoint(endpointId: string): Promise<void>;
260
316
  /**
261
317
  * True if the active credential for `provider` is a static API key with no
262
- * refresh mechanism. For `moonshot` this is only true when the Kimi OAuth
263
- * credential is absent (a present OAuth credential is refreshable).
318
+ * refresh mechanism. For a dual-auth provider this is only true when its OAuth
319
+ * credential is absent or sidelined (a live OAuth credential is refreshable).
264
320
  */
265
321
  isStaticApiKey(provider: string): Promise<boolean>;
266
322
  /**
267
323
  * The base URL on the credential that is active right now, if any.
268
324
  * Synchronous — call only after load()/resolveCredentials() populated the
269
- * snapshot. For `moonshot` this is the Kimi For Coding URL whenever the
270
- * OAuth entry is the one resolveCredentials would serve (i.e. not currently
271
- * usage-exhausted with an API key configured).
325
+ * snapshot. For a dual-auth provider this is the subscription endpoint (Kimi
326
+ * For Coding, the Grok CLI proxy) whenever the OAuth entry is the one
327
+ * resolveCredentials would serve (i.e. not currently usage-exhausted with an
328
+ * API key configured).
272
329
  */
273
330
  getStoredBaseUrl(provider: string): string | undefined;
274
331
  load(): Promise<void>;
@@ -279,6 +336,20 @@ declare class AuthStorage {
279
336
  * writes API keys natively without going through this instance.
280
337
  */
281
338
  reload(): Promise<void>;
339
+ /**
340
+ * Like {@link ensureLoaded}, but re-reads when the file changed since this
341
+ * snapshot — a cheap stat, not a re-parse. Used by the "what is connected?"
342
+ * readers, which must reflect writes made by another window, the CLI, or the
343
+ * desktop app's native (daemon-free) API-key and disconnect paths.
344
+ *
345
+ * Deliberately NOT used by {@link resolveCredentials}: that path compares the
346
+ * caller's snapshot against the latest file to detect a concurrent re-login,
347
+ * and silently refreshing this instance's view first would destroy the
348
+ * evidence that the token it just had rejected has already been replaced.
349
+ */
350
+ private ensureFresh;
351
+ /** Record the file identity behind the current snapshot. */
352
+ private rememberSnapshot;
282
353
  /**
283
354
  * Apply one provider-scoped mutation to the latest on-disk snapshot.
284
355
  * AuthStorage instances live in every app session/process, so writing this
@@ -296,7 +367,7 @@ declare class AuthStorage {
296
367
  * `resetsAt` (unix SECONDS, from the provider's rate-limit response) or a
297
368
  * 15-minute default when no reset time is known. While the mark is in the
298
369
  * future, `resolveCredentials("moonshot")` serves the Moonshot API key
299
- * instead of the Kimi OAuth credential (when both are configured) — OAuth
370
+ * instead of the subscription OAuth credential (when both are configured) — OAuth
300
371
  * stays the preferred credential and is retried automatically once the mark
301
372
  * lapses. Persisted to auth.json so a restart (or another ezcoder-app window)
302
373
  * doesn't burn a request rediscovering the same exhausted window. No-op if
@@ -306,13 +377,27 @@ declare class AuthStorage {
306
377
  clearAll(): Promise<void>;
307
378
  /**
308
379
  * Returns valid credentials, auto-refreshing if expired.
380
+ *
309
381
  * If `forceRefresh` is true, refreshes even if the token hasn't expired
310
- * (useful when the provider rejects a token with 401 before its stored expiry).
382
+ * (useful when the provider rejects a token with 401 before its stored
383
+ * expiry). Callers recovering from a rejection should also pass
384
+ * `rejectedToken` — see the stampede guard below.
385
+ *
311
386
  * Throws if not logged in.
312
387
  */
313
388
  resolveCredentials(provider: string, opts?: {
314
389
  forceRefresh?: boolean;
315
390
  storageKeys?: string[];
391
+ /**
392
+ * The access token the provider just rejected. Refreshing an OAuth grant
393
+ * invalidates the previous access token, so N processes sharing auth.json
394
+ * (app windows, CLI sessions, the usage poller) can otherwise revoke each
395
+ * other in a loop: each one force-refreshes on 401, and every refresh kills
396
+ * the token the others still hold. Naming the rejected token lets the
397
+ * refresh path tell "this credential is genuinely dead" from "someone else
398
+ * already rotated it" and simply adopt the newer on-disk credential.
399
+ */
400
+ rejectedToken?: string;
316
401
  }): Promise<OAuthCredentials>;
317
402
  /**
318
403
  * Returns a valid access token, auto-refreshing if expired.
@@ -417,6 +502,69 @@ declare function loginKimi(callbacks: OAuthLoginCallbacks): Promise<OAuthCredent
417
502
  /** Exchange a refresh token for a fresh Kimi access token. */
418
503
  declare function refreshKimiToken(refreshToken: string): Promise<OAuthCredentials>;
419
504
 
505
+ /**
506
+ * Grok (xAI) subscription OAuth — Device Authorization Grant (RFC 8628).
507
+ *
508
+ * Two form-encoded POST endpoints against xAI's OIDC issuer (`https://auth.x.ai`,
509
+ * advertised by its `/.well-known/openid-configuration`):
510
+ *
511
+ * - `/oauth2/device/code` (client_id + scope) → device + user code
512
+ * - `/oauth2/token` (grant_type=device_code) → poll until authorized
513
+ * - `/oauth2/token` (grant_type=refresh_token) → refresh access token
514
+ *
515
+ * Like Kimi (and unlike Anthropic/OpenAI/Gemini's browser-redirect PKCE) this is
516
+ * a device-code/poll flow: show a URL + code, the user authorizes in a browser on
517
+ * any device, we poll for the token. Deliberately chosen over the loopback PKCE
518
+ * variant because xAI pins the Grok-CLI client's redirect to
519
+ * `http://127.0.0.1:56121/callback` — a fixed port we cannot rebind if it's busy,
520
+ * and unreachable from a container/SSH session. Device code has neither problem.
521
+ *
522
+ * The issued token is used against the Grok CLI's chat proxy
523
+ * (`https://cli-chat-proxy.grok.com/v1`, distinct from the `api.x.ai` API-key
524
+ * endpoint) — that is the surface the `grok-cli:access` scope grants, and it
525
+ * bills against the user's SuperGrok / X Premium subscription instead of metered
526
+ * API credits. We persist that base URL on the credential so the runtime routes
527
+ * there automatically; `grokCliHeaders()` supplies the client identity the proxy
528
+ * requires (attached centrally in @prestyj/ai's `xai` transport).
529
+ *
530
+ * Caveats worth knowing, both observed in the wild and surfaced to users rather
531
+ * than hidden here:
532
+ * - The client id below is xAI's public Grok-CLI desktop client (no secret).
533
+ * Every third-party implementation reuses it; xAI has not published a
534
+ * partner-client program, so subscription OAuth is a best-effort path.
535
+ * - xAI gates proxy access by subscription tier. A perfectly valid login can
536
+ * still be refused at inference time, which is exactly why `xai` keeps its
537
+ * API-key method as a fallback (see AuthStorage's dual-auth resolution).
538
+ */
539
+
540
+ /** Grok CLI chat-proxy base URL the issued OAuth token is used against. */
541
+ declare function grokCliBaseUrl(): string;
542
+ /**
543
+ * Headers the Grok CLI chat proxy requires on every model request. It serves
544
+ * only recognized Grok-CLI clients: without the token-auth marker and a client
545
+ * version it refuses the request. `modelId` populates the model-override header
546
+ * the proxy uses to route a request to the entitled model.
547
+ *
548
+ * Attach these ONLY to the proxy — the `api.x.ai` API-key path must not receive
549
+ * them (see {@link isGrokCliEndpoint}).
550
+ */
551
+ declare function grokCliHeaders(modelId?: string): Record<string, string>;
552
+ /**
553
+ * True if `baseUrl` targets the Grok CLI chat proxy (the URL persisted on Grok
554
+ * OAuth credentials). Callers use this to decide whether to attach
555
+ * {@link grokCliHeaders} and whether a usage/permission rejection should fall
556
+ * back to the xAI API key.
557
+ */
558
+ declare function isGrokCliEndpoint(baseUrl: string | undefined): boolean;
559
+ /**
560
+ * Drive the Grok device-code flow end-to-end. Shows the verification URL + user
561
+ * code via callbacks, opens the browser, and polls until the user authorizes or
562
+ * the device code expires (deadline set by the server).
563
+ */
564
+ declare function loginXai(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials>;
565
+ /** Exchange a refresh token for a fresh Grok access token. */
566
+ declare function refreshXaiToken(refreshToken: string): Promise<OAuthCredentials>;
567
+
420
568
  /**
421
569
  * Minimal Telegram Bot API client using raw fetch().
422
570
  * Supports long polling, markdown messages, inline keyboards, and message splitting.
@@ -590,4 +738,4 @@ interface AutoUpdater {
590
738
  }
591
739
  declare function createAutoUpdater(config: AutoUpdateConfig): AutoUpdater;
592
740
 
593
- export { AuthStorage, type AutoUpdateConfig, type AutoUpdater, DEFAULT_LOCAL_ENDPOINTS, type DiscoverOptions, type DiscoveryResult, FALLBACK_CONTEXT_WINDOW, type InlineButton, LOCAL_API_KEY_PLACEHOLDER, LOCAL_AUTH_KEY_PREFIX, type LocalEndpoint, type LocalEndpointKind, type LocalEndpointProbe, type LocalModel, type LogLevel, MOONSHOT_OAUTH_KEY, ModelInfo, NotLoggedInError, type OAuthCredentials, type OAuthLoginCallbacks, type ProbeOptions, type ProgressCallback, SubscriptionUsageError, type SubscriptionUsageProvider, type SubscriptionUsageSnapshot, type SubscriptionUsageWindow, TelegramBot, type TelegramConfig, type TelegramMessage, type TelegramUpdate, type TelegramVoiceMessage, XIAOMI_CREDITS_KEY, clearLocalDiscoveryCache, closeLogger, createAutoUpdater, decodeOggOpus, discoverLocalModels, downmixToMono, endpointRoot, fetchSubscriptionUsage, findProbedModel, formatLocalModelId, generatePKCE, getClaudeCliUserAgent, getClaudeCodeVersion, getNextThinkingLevel, getSessionId, getSupportedThinkingLevels, isKimiCodingEndpoint, isLocalModelId, isLoggerOpen, isModelLoaded, isThinkingLevelSupported, kimiCodeBaseUrl, kimiCodingHeaders, localAuthStorageKey, log, loginAnthropic, loginGemini, loginKimi, loginOpenAI, openLog, parseLocalModelId, probeEndpoint, readStoredBaseUrlSync, refreshAnthropicToken, refreshGeminiToken, refreshKimiToken, refreshOpenAIToken, registerLogCleanup, resample, setProgressCallback, toModelInfo, transcribeVoice, withFileLock };
741
+ export { AuthStorage, type AutoUpdateConfig, type AutoUpdater, DEFAULT_LOCAL_ENDPOINTS, type DiscoverOptions, type DiscoveryResult, type DualAuthProvider, FALLBACK_CONTEXT_WINDOW, type InlineButton, LOCAL_API_KEY_PLACEHOLDER, LOCAL_AUTH_KEY_PREFIX, type LocalEndpoint, type LocalEndpointKind, type LocalEndpointProbe, type LocalModel, type LogLevel, MOONSHOT_OAUTH_KEY, ModelInfo, NotLoggedInError, type OAuthCredentials, type OAuthLoginCallbacks, type ProbeOptions, type ProgressCallback, SubscriptionUsageError, type SubscriptionUsageProvider, type SubscriptionUsageSnapshot, type SubscriptionUsageWindow, TelegramBot, type TelegramConfig, type TelegramMessage, type TelegramUpdate, type TelegramVoiceMessage, XAI_OAUTH_KEY, XIAOMI_CREDITS_KEY, clearLocalDiscoveryCache, closeLogger, createAutoUpdater, decodeOggOpus, discoverLocalModels, downmixToMono, dualAuthProvider, dualAuthProviderByOAuthKey, endpointRoot, fetchSubscriptionUsage, findProbedModel, formatLocalModelId, generatePKCE, getClaudeCliUserAgent, getClaudeCodeVersion, getNextThinkingLevel, getSessionId, getSupportedThinkingLevels, grokCliBaseUrl, grokCliHeaders, isGrokCliEndpoint, isKimiCodingEndpoint, isLocalModelId, isLoggerOpen, isModelLoaded, isThinkingLevelSupported, kimiCodeBaseUrl, kimiCodingHeaders, localAuthStorageKey, log, loginAnthropic, loginGemini, loginKimi, loginOpenAI, loginXai, oauthStorageKey, openLog, parseLocalModelId, probeEndpoint, providerStorageKeys, readStoredBaseUrlSync, refreshAnthropicToken, refreshGeminiToken, refreshKimiToken, refreshOpenAIToken, refreshXaiToken, registerLogCleanup, resample, setProgressCallback, toModelInfo, transcribeVoice, withFileLock };
package/dist/index.js CHANGED
@@ -5,9 +5,12 @@ import {
5
5
  MODELS,
6
6
  MOONSHOT_OAUTH_KEY,
7
7
  NotLoggedInError,
8
+ XAI_OAUTH_KEY,
8
9
  XIAOMI_CREDITS_KEY,
9
10
  clearRuntimeModels,
10
11
  closeLogger,
12
+ dualAuthProvider,
13
+ dualAuthProviderByOAuthKey,
11
14
  generatePKCE,
12
15
  getAllModels,
13
16
  getAuthStorageKey,
@@ -25,6 +28,9 @@ import {
25
28
  getSummaryModel,
26
29
  getToolResultCharLimit,
27
30
  getVideoByteLimit,
31
+ grokCliBaseUrl,
32
+ grokCliHeaders,
33
+ isGrokCliEndpoint,
28
34
  isKimiCodingEndpoint,
29
35
  isLoggerOpen,
30
36
  kimiCodeBaseUrl,
@@ -34,17 +40,21 @@ import {
34
40
  loginGemini,
35
41
  loginKimi,
36
42
  loginOpenAI,
43
+ loginXai,
44
+ oauthStorageKey,
37
45
  openLog,
46
+ providerStorageKeys,
38
47
  readStoredBaseUrlSync,
39
48
  refreshAnthropicToken,
40
49
  refreshGeminiToken,
41
50
  refreshKimiToken,
42
51
  refreshOpenAIToken,
52
+ refreshXaiToken,
43
53
  registerLogCleanup,
44
54
  registerRuntimeModels,
45
55
  usesOpenAICodexTransport,
46
56
  withFileLock
47
- } from "./chunk-WAG5K2MH.js";
57
+ } from "./chunk-S6QBRJ4D.js";
48
58
  import {
49
59
  getAppPaths
50
60
  } from "./chunk-CRU3SSNX.js";
@@ -75,6 +85,7 @@ var ANTHROPIC_ADAPTIVE_THINKING_LEVELS = [
75
85
  "max"
76
86
  ];
77
87
  var MOONSHOT_K3_THINKING_LEVELS = ["low", "high", "max"];
88
+ var GLM_THINKING_LEVELS = ["low", "medium", "high", "xhigh", "max"];
78
89
  var LOCAL_THINKING_LEVELS = ["low", "medium", "high", "max"];
79
90
  function isOpenAIGptModel(provider, model) {
80
91
  return provider === "openai" && model.startsWith("gpt-");
@@ -88,6 +99,9 @@ function isXaiModel(provider) {
88
99
  function isMoonshotK3Model(provider, model) {
89
100
  return provider === "moonshot" && model === "kimi-k3";
90
101
  }
102
+ function isGlmModel(provider) {
103
+ return provider === "glm";
104
+ }
91
105
  function isAnthropicXhighModel(provider, model) {
92
106
  return provider === "anthropic" && /opus-5|opus-4-8|opus-4-7/.test(model);
93
107
  }
@@ -119,6 +133,11 @@ function getSupportedThinkingLevels(provider, model) {
119
133
  return XAI_THINKING_LEVELS.slice(0, maxIndex2 + 1);
120
134
  }
121
135
  if (isMoonshotK3Model(provider, model)) return MOONSHOT_K3_THINKING_LEVELS;
136
+ if (isGlmModel(provider)) {
137
+ const maxIndex2 = GLM_THINKING_LEVELS.indexOf(maxLevel);
138
+ if (maxIndex2 === -1) return GLM_THINKING_LEVELS;
139
+ return GLM_THINKING_LEVELS.slice(0, maxIndex2 + 1);
140
+ }
122
141
  if (!isOpenAIGptModel(provider, model)) return [maxLevel];
123
142
  const levels = model.startsWith("gpt-5.6-") ? OPENAI_GPT_56_THINKING_LEVELS : OPENAI_GPT_THINKING_LEVELS;
124
143
  const maxIndex = levels.indexOf(maxLevel);
@@ -130,7 +149,7 @@ function isThinkingLevelSupported(provider, model, level) {
130
149
  }
131
150
  function getNextThinkingLevel(provider, model, current) {
132
151
  const supportedLevels = getSupportedThinkingLevels(provider, model);
133
- const shouldCycleLevels = isOpenAIGptModel(provider, model) || isAnthropicAdaptiveModel(provider, model) || isSakanaModel(provider) || isXaiModel(provider) || isMoonshotK3Model(provider, model) || // Local servers take a real effort level, not just on/off: Ollama accepts
152
+ const shouldCycleLevels = isOpenAIGptModel(provider, model) || isAnthropicAdaptiveModel(provider, model) || isSakanaModel(provider) || isXaiModel(provider) || isMoonshotK3Model(provider, model) || isGlmModel(provider) || // Local servers take a real effort level, not just on/off: Ollama accepts
134
153
  // low/medium/high on `reasoning_effort` (verified against 0.32) and the
135
154
  // other OpenAI-compatible servers use the same three. A model that can't
136
155
  // reason at all already has no supported levels, so it never gets here.
@@ -1117,6 +1136,7 @@ export {
1117
1136
  NotLoggedInError,
1118
1137
  SubscriptionUsageError,
1119
1138
  TelegramBot,
1139
+ XAI_OAUTH_KEY,
1120
1140
  XIAOMI_CREDITS_KEY,
1121
1141
  clearLocalDiscoveryCache,
1122
1142
  clearRuntimeModels,
@@ -1125,6 +1145,8 @@ export {
1125
1145
  decodeOggOpus,
1126
1146
  discoverLocalModels,
1127
1147
  downmixToMono,
1148
+ dualAuthProvider,
1149
+ dualAuthProviderByOAuthKey,
1128
1150
  endpointRoot,
1129
1151
  fetchSubscriptionUsage,
1130
1152
  findProbedModel,
@@ -1149,6 +1171,9 @@ export {
1149
1171
  getSupportedThinkingLevels,
1150
1172
  getToolResultCharLimit,
1151
1173
  getVideoByteLimit,
1174
+ grokCliBaseUrl,
1175
+ grokCliHeaders,
1176
+ isGrokCliEndpoint,
1152
1177
  isKimiCodingEndpoint,
1153
1178
  isLocalModelId,
1154
1179
  isLoggerOpen,
@@ -1162,14 +1187,18 @@ export {
1162
1187
  loginGemini,
1163
1188
  loginKimi,
1164
1189
  loginOpenAI,
1190
+ loginXai,
1191
+ oauthStorageKey,
1165
1192
  openLog,
1166
1193
  parseLocalModelId,
1167
1194
  probeEndpoint,
1195
+ providerStorageKeys,
1168
1196
  readStoredBaseUrlSync,
1169
1197
  refreshAnthropicToken,
1170
1198
  refreshGeminiToken,
1171
1199
  refreshKimiToken,
1172
1200
  refreshOpenAIToken,
1201
+ refreshXaiToken,
1173
1202
  registerLogCleanup,
1174
1203
  registerRuntimeModels,
1175
1204
  resample,