@kenkaiiii/gg-core 5.36.0 → 5.37.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.cjs CHANGED
@@ -41,6 +41,7 @@ __export(index_exports, {
41
41
  NotLoggedInError: () => NotLoggedInError,
42
42
  SubscriptionUsageError: () => SubscriptionUsageError,
43
43
  TelegramBot: () => TelegramBot,
44
+ XAI_OAUTH_KEY: () => XAI_OAUTH_KEY,
44
45
  XIAOMI_CREDITS_KEY: () => XIAOMI_CREDITS_KEY,
45
46
  clearLocalDiscoveryCache: () => clearLocalDiscoveryCache,
46
47
  clearRuntimeModels: () => clearRuntimeModels,
@@ -49,6 +50,8 @@ __export(index_exports, {
49
50
  decodeOggOpus: () => decodeOggOpus,
50
51
  discoverLocalModels: () => discoverLocalModels,
51
52
  downmixToMono: () => downmixToMono,
53
+ dualAuthProvider: () => dualAuthProvider,
54
+ dualAuthProviderByOAuthKey: () => dualAuthProviderByOAuthKey,
52
55
  endpointRoot: () => endpointRoot,
53
56
  fetchSubscriptionUsage: () => fetchSubscriptionUsage,
54
57
  findProbedModel: () => findProbedModel,
@@ -73,6 +76,9 @@ __export(index_exports, {
73
76
  getSupportedThinkingLevels: () => getSupportedThinkingLevels,
74
77
  getToolResultCharLimit: () => getToolResultCharLimit,
75
78
  getVideoByteLimit: () => getVideoByteLimit,
79
+ grokCliBaseUrl: () => grokCliBaseUrl,
80
+ grokCliHeaders: () => grokCliHeaders,
81
+ isGrokCliEndpoint: () => isGrokCliEndpoint,
76
82
  isKimiCodingEndpoint: () => isKimiCodingEndpoint,
77
83
  isLocalModelId: () => isLocalModelId,
78
84
  isLoggerOpen: () => isLoggerOpen,
@@ -86,14 +92,18 @@ __export(index_exports, {
86
92
  loginGemini: () => loginGemini,
87
93
  loginKimi: () => loginKimi,
88
94
  loginOpenAI: () => loginOpenAI,
95
+ loginXai: () => loginXai,
96
+ oauthStorageKey: () => oauthStorageKey,
89
97
  openLog: () => openLog,
90
98
  parseLocalModelId: () => parseLocalModelId,
91
99
  probeEndpoint: () => probeEndpoint,
100
+ providerStorageKeys: () => providerStorageKeys,
92
101
  readStoredBaseUrlSync: () => readStoredBaseUrlSync,
93
102
  refreshAnthropicToken: () => refreshAnthropicToken,
94
103
  refreshGeminiToken: () => refreshGeminiToken,
95
104
  refreshKimiToken: () => refreshKimiToken,
96
105
  refreshOpenAIToken: () => refreshOpenAIToken,
106
+ refreshXaiToken: () => refreshXaiToken,
97
107
  registerLogCleanup: () => registerLogCleanup,
98
108
  registerRuntimeModels: () => registerRuntimeModels,
99
109
  resample: () => resample,
@@ -896,13 +906,13 @@ var CodeAssistHttpError = class extends Error {
896
906
  }
897
907
  };
898
908
  async function loginGemini(callbacks) {
899
- const { clientId, clientSecret } = getGeminiOAuthClientCredentials();
909
+ const { clientId: clientId2, clientSecret } = getGeminiOAuthClientCredentials();
900
910
  const { verifier, challenge } = await generatePKCE();
901
911
  const state = import_node_crypto5.default.randomBytes(32).toString("hex");
902
912
  const redirectUri = await getLoopbackRedirectUri();
903
913
  const url = new URL(AUTHORIZE_URL3);
904
914
  url.searchParams.set("response_type", "code");
905
- url.searchParams.set("client_id", clientId);
915
+ url.searchParams.set("client_id", clientId2);
906
916
  url.searchParams.set("redirect_uri", redirectUri);
907
917
  url.searchParams.set("scope", SCOPE2);
908
918
  url.searchParams.set("access_type", "offline");
@@ -927,7 +937,7 @@ async function loginGemini(callbacks) {
927
937
  }
928
938
  code = parsed.code;
929
939
  }
930
- const creds = await exchangeGeminiCode(code, verifier, redirectUri, clientId, clientSecret);
940
+ const creds = await exchangeGeminiCode(code, verifier, redirectUri, clientId2, clientSecret);
931
941
  callbacks.onStatus("Setting up Gemini Code Assist access...");
932
942
  const projectId = await setupCodeAssistProject(creds.accessToken, callbacks);
933
943
  return {
@@ -936,11 +946,11 @@ async function loginGemini(callbacks) {
936
946
  };
937
947
  }
938
948
  async function refreshGeminiToken(refreshToken) {
939
- const { clientId, clientSecret } = getGeminiOAuthClientCredentials();
949
+ const { clientId: clientId2, clientSecret } = getGeminiOAuthClientCredentials();
940
950
  const data = await postTokenRequest2({
941
951
  grant_type: "refresh_token",
942
952
  refresh_token: refreshToken,
943
- client_id: clientId,
953
+ client_id: clientId2,
944
954
  client_secret: clientSecret
945
955
  });
946
956
  return {
@@ -950,9 +960,9 @@ async function refreshGeminiToken(refreshToken) {
950
960
  };
951
961
  }
952
962
  function getGeminiOAuthClientCredentials() {
953
- const clientId = process.env[CLIENT_ID_ENV]?.trim() || DEFAULT_CLIENT_ID;
963
+ const clientId2 = process.env[CLIENT_ID_ENV]?.trim() || DEFAULT_CLIENT_ID;
954
964
  const clientSecret = process.env[CLIENT_SECRET_ENV]?.trim() || DEFAULT_CLIENT_SECRET;
955
- return { clientId, clientSecret };
965
+ return { clientId: clientId2, clientSecret };
956
966
  }
957
967
  async function getLoopbackRedirectUri() {
958
968
  return new Promise((resolve, reject) => {
@@ -1031,10 +1041,10 @@ async function loginWithServer2(authUrl, redirectUri, expectedState, callbacks)
1031
1041
  });
1032
1042
  });
1033
1043
  }
1034
- async function exchangeGeminiCode(code, verifier, redirectUri, clientId, clientSecret) {
1044
+ async function exchangeGeminiCode(code, verifier, redirectUri, clientId2, clientSecret) {
1035
1045
  const data = await postTokenRequest2({
1036
1046
  grant_type: "authorization_code",
1037
- client_id: clientId,
1047
+ client_id: clientId2,
1038
1048
  client_secret: clientSecret,
1039
1049
  code,
1040
1050
  redirect_uri: redirectUri,
@@ -1225,6 +1235,204 @@ function codeAssistHeaders(accessToken) {
1225
1235
  };
1226
1236
  }
1227
1237
 
1238
+ // src/oauth/xai.ts
1239
+ var DEFAULT_CLIENT_ID2 = "b1a00492-073a-47ea-816f-4c329264a828";
1240
+ var DEFAULT_ISSUER = "https://auth.x.ai";
1241
+ var DEFAULT_CLI_BASE_URL = "https://cli-chat-proxy.grok.com/v1";
1242
+ var OAUTH_SCOPE = "openid profile email offline_access grok-cli:access api:access";
1243
+ var DEFAULT_GROK_CLI_VERSION = "0.2.101";
1244
+ var DEVICE_TIMEOUT_FALLBACK_MS = 10 * 60 * 1e3;
1245
+ var DEFAULT_TOKEN_LIFETIME_SECONDS = 3600;
1246
+ function issuer() {
1247
+ return (process.env.XAI_OAUTH_ISSUER ?? DEFAULT_ISSUER).replace(/\/+$/, "");
1248
+ }
1249
+ function clientId() {
1250
+ return process.env.XAI_OAUTH_CLIENT_ID ?? DEFAULT_CLIENT_ID2;
1251
+ }
1252
+ function grokCliBaseUrl() {
1253
+ return (process.env.XAI_CLI_BASE_URL ?? DEFAULT_CLI_BASE_URL).replace(/\/+$/, "");
1254
+ }
1255
+ function grokCliVersion() {
1256
+ const raw = process.env.GROK_CLI_VERSION ?? DEFAULT_GROK_CLI_VERSION;
1257
+ const cleaned = raw.replace(/[^\u0020-\u007E]/g, "").trim();
1258
+ return cleaned.length > 0 ? cleaned : DEFAULT_GROK_CLI_VERSION;
1259
+ }
1260
+ function grokCliHeaders(modelId) {
1261
+ return {
1262
+ "X-XAI-Token-Auth": "xai-grok-cli",
1263
+ "x-grok-client-version": grokCliVersion(),
1264
+ "x-grok-client-identifier": "ggcoder",
1265
+ ...modelId ? { "x-grok-model-override": modelId } : {}
1266
+ };
1267
+ }
1268
+ function isGrokCliEndpoint(baseUrl) {
1269
+ if (typeof baseUrl !== "string" || baseUrl.length === 0) return false;
1270
+ const normalized = baseUrl.replace(/\/+$/, "");
1271
+ return normalized === grokCliBaseUrl() || /(^|\.)grok\.com/i.test(normalized);
1272
+ }
1273
+ async function postForm2(endpoint, params) {
1274
+ const response = await fetch(`${issuer()}${endpoint}`, {
1275
+ method: "POST",
1276
+ headers: {
1277
+ "Content-Type": "application/x-www-form-urlencoded",
1278
+ Accept: "application/json",
1279
+ "User-Agent": `grok-cli/${grokCliVersion()}`
1280
+ },
1281
+ body: new URLSearchParams(params).toString()
1282
+ });
1283
+ let data = {};
1284
+ try {
1285
+ const parsed = await response.json();
1286
+ if (parsed && typeof parsed === "object") data = parsed;
1287
+ } catch {
1288
+ }
1289
+ return { status: response.status, data };
1290
+ }
1291
+ function errorDetail2(data) {
1292
+ const desc = data.error_description ?? data.message ?? data.error;
1293
+ return typeof desc === "string" && desc.length > 0 ? desc : "unknown error";
1294
+ }
1295
+ function jwtExpirySeconds(token) {
1296
+ const payload = token.split(".")[1];
1297
+ if (!payload) return void 0;
1298
+ try {
1299
+ const json = Buffer.from(payload.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString(
1300
+ "utf-8"
1301
+ );
1302
+ const claims = JSON.parse(json);
1303
+ if (!claims || typeof claims !== "object") return void 0;
1304
+ const exp = claims.exp;
1305
+ return typeof exp === "number" && Number.isFinite(exp) && exp > 0 ? exp : void 0;
1306
+ } catch {
1307
+ return void 0;
1308
+ }
1309
+ }
1310
+ function credsFromTokenResponse2(data, opts) {
1311
+ const accessToken = data.access_token;
1312
+ const responseRefreshToken = data.refresh_token;
1313
+ if (typeof accessToken !== "string" || accessToken.length === 0) {
1314
+ throw new Error("Grok OAuth response missing access_token.");
1315
+ }
1316
+ const refreshToken = typeof responseRefreshToken === "string" && responseRefreshToken.length > 0 ? responseRefreshToken : opts?.fallbackRefreshToken ?? "";
1317
+ if (refreshToken.length === 0) {
1318
+ throw new Error(
1319
+ "Grok OAuth response missing refresh_token \u2014 the offline_access scope was not granted."
1320
+ );
1321
+ }
1322
+ const responseExpiresIn = Number(data.expires_in);
1323
+ let expiresIn;
1324
+ if (Number.isFinite(responseExpiresIn) && responseExpiresIn > 0) {
1325
+ expiresIn = responseExpiresIn;
1326
+ } else {
1327
+ const exp = jwtExpirySeconds(accessToken);
1328
+ const fromJwt = exp !== void 0 ? exp - Math.floor(Date.now() / 1e3) : 0;
1329
+ expiresIn = fromJwt > 0 ? fromJwt : DEFAULT_TOKEN_LIFETIME_SECONDS;
1330
+ }
1331
+ return {
1332
+ accessToken,
1333
+ refreshToken,
1334
+ expiresAt: Date.now() + expiresIn * 1e3,
1335
+ expiresIn,
1336
+ baseUrl: grokCliBaseUrl()
1337
+ };
1338
+ }
1339
+ async function requestDeviceAuthorization2() {
1340
+ const { status, data } = await postForm2("/oauth2/device/code", {
1341
+ client_id: clientId(),
1342
+ scope: OAUTH_SCOPE
1343
+ });
1344
+ if (status !== 200) {
1345
+ throw new Error(`Grok device authorization failed (${status}): ${errorDetail2(data)}`);
1346
+ }
1347
+ const userCode = data.user_code;
1348
+ const deviceCode = data.device_code;
1349
+ const verificationUriComplete = data.verification_uri_complete;
1350
+ if (typeof userCode !== "string" || typeof deviceCode !== "string") {
1351
+ throw new Error("Grok device authorization response missing user_code/device_code.");
1352
+ }
1353
+ return {
1354
+ userCode,
1355
+ deviceCode,
1356
+ verificationUri: typeof data.verification_uri === "string" ? data.verification_uri : "",
1357
+ verificationUriComplete: typeof verificationUriComplete === "string" ? verificationUriComplete : "",
1358
+ interval: Number(data.interval ?? 5) || 5,
1359
+ // RFC 8628 §3.2: the server states how long the device code lives. Honor it
1360
+ // rather than imposing our own budget — polling a code we know is dead only
1361
+ // burns requests, and a longer local window would keep a user waiting past
1362
+ // the point the code can ever succeed.
1363
+ expiresInMs: (Number(data.expires_in) || 0) > 0 ? Number(data.expires_in) * 1e3 : DEVICE_TIMEOUT_FALLBACK_MS
1364
+ };
1365
+ }
1366
+ async function pollDeviceToken2(deviceCode) {
1367
+ const { status, data } = await postForm2("/oauth2/token", {
1368
+ client_id: clientId(),
1369
+ device_code: deviceCode,
1370
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
1371
+ });
1372
+ if (status === 200 && typeof data.access_token === "string") {
1373
+ return { kind: "success", creds: credsFromTokenResponse2(data) };
1374
+ }
1375
+ if (status >= 500) {
1376
+ throw new Error(`Grok token polling server error (${status}): ${errorDetail2(data)}`);
1377
+ }
1378
+ const errorCode = typeof data.error === "string" ? data.error : "unknown_error";
1379
+ switch (errorCode) {
1380
+ case "authorization_pending":
1381
+ return { kind: "pending" };
1382
+ case "slow_down":
1383
+ return { kind: "slow_down" };
1384
+ case "expired_token":
1385
+ return { kind: "expired" };
1386
+ case "access_denied":
1387
+ return { kind: "denied" };
1388
+ default:
1389
+ throw new Error(`Grok token polling failed (${status}): ${errorDetail2(data)}`);
1390
+ }
1391
+ }
1392
+ function sleep2(ms) {
1393
+ return new Promise((resolve) => {
1394
+ setTimeout(resolve, ms);
1395
+ });
1396
+ }
1397
+ async function loginXai(callbacks) {
1398
+ const auth = await requestDeviceAuthorization2();
1399
+ callbacks.onStatus(
1400
+ `Visit ${auth.verificationUri || auth.verificationUriComplete} and enter code: ${auth.userCode}`
1401
+ );
1402
+ callbacks.onOpenUrl(auth.verificationUriComplete || auth.verificationUri);
1403
+ callbacks.onStatus("Waiting for you to authorize in the browser...");
1404
+ const deadline = Date.now() + auth.expiresInMs;
1405
+ let interval = Math.max(auth.interval, 1);
1406
+ while (Date.now() < deadline) {
1407
+ const remaining = deadline - Date.now();
1408
+ await sleep2(Math.min(interval * 1e3, remaining));
1409
+ const result = await pollDeviceToken2(auth.deviceCode);
1410
+ if (result.kind === "success") return result.creds;
1411
+ if (result.kind === "denied") {
1412
+ throw new Error("Grok authorization was denied.");
1413
+ }
1414
+ if (result.kind === "expired") {
1415
+ throw new Error("Grok device code expired. Please run login again.");
1416
+ }
1417
+ if (result.kind === "slow_down") {
1418
+ interval += 5;
1419
+ }
1420
+ }
1421
+ throw new Error("Grok login timed out. Please run login again.");
1422
+ }
1423
+ async function refreshXaiToken(refreshToken) {
1424
+ const { status, data } = await postForm2("/oauth2/token", {
1425
+ client_id: clientId(),
1426
+ grant_type: "refresh_token",
1427
+ refresh_token: refreshToken
1428
+ });
1429
+ if (status === 200 && typeof data.access_token === "string") {
1430
+ return credsFromTokenResponse2(data, { fallbackRefreshToken: refreshToken });
1431
+ }
1432
+ const errorCode = typeof data.error === "string" ? data.error : "";
1433
+ throw new Error(`Grok token refresh failed (${status}): ${errorCode || errorDetail2(data)}`);
1434
+ }
1435
+
1228
1436
  // src/file-lock.ts
1229
1437
  var import_promises2 = __toESM(require("fs/promises"), 1);
1230
1438
  var import_promises3 = require("timers/promises");
@@ -1289,18 +1497,49 @@ function isAlive(pid) {
1289
1497
 
1290
1498
  // src/auth-storage.ts
1291
1499
  var MOONSHOT_OAUTH_KEY = "moonshot-oauth";
1500
+ var XAI_OAUTH_KEY = "xai-oauth";
1501
+ var DUAL_AUTH_PROVIDERS = [
1502
+ {
1503
+ provider: "moonshot",
1504
+ oauthKey: MOONSHOT_OAUTH_KEY,
1505
+ oauthLabel: "Kimi OAuth",
1506
+ apiKeyLabel: "Moonshot API key",
1507
+ restoreHint: 'Run "ggcoder login" and choose Kimi OAuth to restore OAuth auth.'
1508
+ },
1509
+ {
1510
+ provider: "xai",
1511
+ oauthKey: XAI_OAUTH_KEY,
1512
+ oauthLabel: "Grok OAuth",
1513
+ apiKeyLabel: "xAI API key",
1514
+ restoreHint: 'Run "ggcoder login" and choose Grok OAuth to restore OAuth auth.'
1515
+ }
1516
+ ];
1517
+ function dualAuthProvider(provider) {
1518
+ return DUAL_AUTH_PROVIDERS.find((entry) => entry.provider === provider);
1519
+ }
1520
+ function dualAuthProviderByOAuthKey(storageKey) {
1521
+ return DUAL_AUTH_PROVIDERS.find((entry) => entry.oauthKey === storageKey);
1522
+ }
1523
+ function oauthStorageKey(provider) {
1524
+ return dualAuthProvider(provider)?.oauthKey;
1525
+ }
1526
+ function providerStorageKeys(provider) {
1527
+ const dual = dualAuthProvider(provider);
1528
+ return dual ? [dual.oauthKey, dual.provider] : [provider];
1529
+ }
1292
1530
  var XIAOMI_CREDITS_KEY = "xiaomi-credits";
1293
1531
  var LOCAL_AUTH_KEY_PREFIX = "local:";
1294
1532
  var LOCAL_CREDENTIAL_LIFETIME_MS = 100 * 365 * 24 * 60 * 60 * 1e3;
1295
1533
  function activeBaseUrlEntry(data, provider) {
1296
- if (provider === "moonshot") {
1297
- const oauth = data[MOONSHOT_OAUTH_KEY];
1534
+ const dual = dualAuthProvider(provider);
1535
+ if (dual) {
1536
+ const oauth = data[dual.oauthKey];
1298
1537
  if (oauth) {
1299
1538
  const exhaustedUntil = oauth.usageExhaustedUntil ?? 0;
1300
- if (Date.now() < exhaustedUntil && data["moonshot"]) return data["moonshot"];
1539
+ if (Date.now() < exhaustedUntil && data[dual.provider]) return data[dual.provider];
1301
1540
  return oauth;
1302
1541
  }
1303
- return data["moonshot"];
1542
+ return data[dual.provider];
1304
1543
  }
1305
1544
  return data[provider];
1306
1545
  }
@@ -1335,6 +1574,16 @@ var AuthStorage = class {
1335
1574
  data = {};
1336
1575
  filePath;
1337
1576
  loaded = false;
1577
+ /**
1578
+ * mtime+size of the file as of the cached snapshot (`size: -1` = no file).
1579
+ * auth.json is shared: the desktop app writes API keys and disconnects
1580
+ * NATIVELY (so they work with no daemon running), and every window/process has
1581
+ * its own AuthStorage. A load-once cache therefore goes stale — the sidecar
1582
+ * would keep listing models for a provider just disconnected, and hide the
1583
+ * ones just connected, until the daemon restarted.
1584
+ */
1585
+ snapshotMtimeMs = 0;
1586
+ snapshotSize = -1;
1338
1587
  /** Per-provider lock to serialize concurrent refresh calls. */
1339
1588
  refreshLocks = /* @__PURE__ */ new Map();
1340
1589
  constructor(filePath) {
@@ -1346,12 +1595,12 @@ var AuthStorage = class {
1346
1595
  }
1347
1596
  /** List provider keys with stored credentials. */
1348
1597
  async listProviders() {
1349
- await this.ensureLoaded();
1598
+ await this.ensureFresh();
1350
1599
  return Object.keys(this.data);
1351
1600
  }
1352
1601
  /** True if credentials exist for `provider`. */
1353
1602
  async hasCredentials(provider) {
1354
- await this.ensureLoaded();
1603
+ await this.ensureFresh();
1355
1604
  return Boolean(this.data[provider]);
1356
1605
  }
1357
1606
  /**
@@ -1362,18 +1611,19 @@ var AuthStorage = class {
1362
1611
  * instead of re-deriving the same order.
1363
1612
  */
1364
1613
  async pickStorageKey(keys) {
1365
- await this.ensureLoaded();
1614
+ await this.ensureFresh();
1366
1615
  return keys.find((key) => Boolean(this.data[key]));
1367
1616
  }
1368
1617
  /**
1369
- * True if the user has any usable auth for the logical provider. For
1370
- * `moonshot` this is satisfied by either the Kimi OAuth credential or the
1371
- * Moonshot API key.
1618
+ * True if the user has any usable auth for the logical provider. For a
1619
+ * dual-auth provider (Kimi/Grok) either the OAuth credential or the API key
1620
+ * satisfies it.
1372
1621
  */
1373
1622
  async hasProviderAuth(provider) {
1374
- await this.ensureLoaded();
1375
- if (provider === "moonshot") {
1376
- return Boolean(this.data[MOONSHOT_OAUTH_KEY] || this.data["moonshot"]);
1623
+ await this.ensureFresh();
1624
+ const dual = dualAuthProvider(provider);
1625
+ if (dual) {
1626
+ return Boolean(this.data[dual.oauthKey] || this.data[dual.provider]);
1377
1627
  }
1378
1628
  if (provider === "xiaomi") {
1379
1629
  return Boolean(this.data["xiaomi"] || this.data[XIAOMI_CREDITS_KEY]);
@@ -1407,14 +1657,16 @@ var AuthStorage = class {
1407
1657
  }
1408
1658
  /**
1409
1659
  * True if the active credential for `provider` is a static API key with no
1410
- * refresh mechanism. For `moonshot` this is only true when the Kimi OAuth
1411
- * credential is absent (a present OAuth credential is refreshable).
1660
+ * refresh mechanism. For a dual-auth provider this is only true when its OAuth
1661
+ * credential is absent or sidelined (a live OAuth credential is refreshable).
1412
1662
  */
1413
1663
  async isStaticApiKey(provider) {
1414
- await this.ensureLoaded();
1415
- if (provider === "moonshot" && this.data[MOONSHOT_OAUTH_KEY]) {
1416
- const exhaustedUntil = this.data[MOONSHOT_OAUTH_KEY].usageExhaustedUntil ?? 0;
1417
- const apiKeyActive = Date.now() < exhaustedUntil && Boolean(this.data["moonshot"]);
1664
+ await this.ensureFresh();
1665
+ const dual = dualAuthProvider(provider);
1666
+ const oauthCreds = dual ? this.data[dual.oauthKey] : void 0;
1667
+ if (dual && oauthCreds) {
1668
+ const exhaustedUntil = oauthCreds.usageExhaustedUntil ?? 0;
1669
+ const apiKeyActive = Date.now() < exhaustedUntil && Boolean(this.data[dual.provider]);
1418
1670
  if (!apiKeyActive) return false;
1419
1671
  }
1420
1672
  return STATIC_API_KEY_PROVIDERS.has(provider);
@@ -1422,26 +1674,30 @@ var AuthStorage = class {
1422
1674
  /**
1423
1675
  * The base URL on the credential that is active right now, if any.
1424
1676
  * Synchronous — call only after load()/resolveCredentials() populated the
1425
- * snapshot. For `moonshot` this is the Kimi For Coding URL whenever the
1426
- * OAuth entry is the one resolveCredentials would serve (i.e. not currently
1427
- * usage-exhausted with an API key configured).
1677
+ * snapshot. For a dual-auth provider this is the subscription endpoint (Kimi
1678
+ * For Coding, the Grok CLI proxy) whenever the OAuth entry is the one
1679
+ * resolveCredentials would serve (i.e. not currently usage-exhausted with an
1680
+ * API key configured).
1428
1681
  */
1429
1682
  getStoredBaseUrl(provider) {
1430
1683
  return activeBaseUrlEntry(this.data, provider)?.baseUrl;
1431
1684
  }
1432
1685
  async load() {
1686
+ const first = !this.loaded;
1433
1687
  await withFileLock(this.filePath, async () => {
1434
1688
  try {
1435
1689
  const content = await import_promises4.default.readFile(this.filePath, "utf-8");
1436
1690
  this.data = JSON.parse(content);
1437
- log("INFO", "auth", `Loaded credentials from ${this.filePath}`, {
1438
- providers: Object.keys(this.data).join(",") || "(none)"
1439
- });
1691
+ if (first) {
1692
+ log("INFO", "auth", `Loaded credentials from ${this.filePath}`, {
1693
+ providers: Object.keys(this.data).join(",") || "(none)"
1694
+ });
1695
+ }
1440
1696
  } catch (err) {
1441
1697
  this.data = {};
1442
1698
  const code = err.code;
1443
1699
  if (code === "ENOENT") {
1444
- log("INFO", "auth", `No auth file found at ${this.filePath} (first run)`);
1700
+ if (first) log("INFO", "auth", `No auth file found at ${this.filePath} (first run)`);
1445
1701
  } else {
1446
1702
  log(
1447
1703
  "ERROR",
@@ -1453,10 +1709,50 @@ var AuthStorage = class {
1453
1709
  }
1454
1710
  });
1455
1711
  this.loaded = true;
1712
+ await this.rememberSnapshot();
1456
1713
  }
1457
1714
  async ensureLoaded() {
1458
1715
  if (!this.loaded) await this.load();
1459
1716
  }
1717
+ /**
1718
+ * Like {@link ensureLoaded}, but re-reads when the file changed since this
1719
+ * snapshot — a cheap stat, not a re-parse. Used by the "what is connected?"
1720
+ * readers, which must reflect writes made by another window, the CLI, or the
1721
+ * desktop app's native (daemon-free) API-key and disconnect paths.
1722
+ *
1723
+ * Deliberately NOT used by {@link resolveCredentials}: that path compares the
1724
+ * caller's snapshot against the latest file to detect a concurrent re-login,
1725
+ * and silently refreshing this instance's view first would destroy the
1726
+ * evidence that the token it just had rejected has already been replaced.
1727
+ */
1728
+ async ensureFresh() {
1729
+ if (!this.loaded) {
1730
+ await this.load();
1731
+ return;
1732
+ }
1733
+ let changed;
1734
+ try {
1735
+ const stat = await import_promises4.default.stat(this.filePath);
1736
+ changed = stat.mtimeMs !== this.snapshotMtimeMs || stat.size !== this.snapshotSize;
1737
+ } catch {
1738
+ changed = this.snapshotSize !== -1;
1739
+ }
1740
+ if (changed) await this.load();
1741
+ }
1742
+ /**
1743
+ * Record the file identity behind the current snapshot, so {@link ensureLoaded}
1744
+ * can tell "someone else wrote" from "this is our own write".
1745
+ */
1746
+ async rememberSnapshot() {
1747
+ try {
1748
+ const stat = await import_promises4.default.stat(this.filePath);
1749
+ this.snapshotMtimeMs = stat.mtimeMs;
1750
+ this.snapshotSize = stat.size;
1751
+ } catch {
1752
+ this.snapshotMtimeMs = 0;
1753
+ this.snapshotSize = -1;
1754
+ }
1755
+ }
1460
1756
  /**
1461
1757
  * Apply one provider-scoped mutation to the latest on-disk snapshot.
1462
1758
  * AuthStorage instances live in every app session/process, so writing this
@@ -1472,14 +1768,16 @@ var AuthStorage = class {
1472
1768
  await atomicWriteFile(this.filePath, JSON.stringify(latest, null, 2));
1473
1769
  this.data = latest;
1474
1770
  });
1771
+ await this.rememberSnapshot();
1475
1772
  }
1476
1773
  async reloadLatest() {
1477
1774
  await withFileLock(this.filePath, async () => {
1478
1775
  this.data = await readAuthData(this.filePath);
1479
1776
  });
1777
+ await this.rememberSnapshot();
1480
1778
  }
1481
1779
  async getCredentials(provider) {
1482
- await this.ensureLoaded();
1780
+ await this.ensureFresh();
1483
1781
  return this.data[provider];
1484
1782
  }
1485
1783
  async setCredentials(provider, creds) {
@@ -1497,7 +1795,7 @@ var AuthStorage = class {
1497
1795
  * `resetsAt` (unix SECONDS, from the provider's rate-limit response) or a
1498
1796
  * 15-minute default when no reset time is known. While the mark is in the
1499
1797
  * future, `resolveCredentials("moonshot")` serves the Moonshot API key
1500
- * instead of the Kimi OAuth credential (when both are configured) — OAuth
1798
+ * instead of the subscription OAuth credential (when both are configured) — OAuth
1501
1799
  * stays the preferred credential and is retried automatically once the mark
1502
1800
  * lapses. Persisted to auth.json so a restart (or another gg-app window)
1503
1801
  * doesn't burn a request rediscovering the same exhausted window. No-op if
@@ -1534,7 +1832,7 @@ var AuthStorage = class {
1534
1832
  */
1535
1833
  async resolveCredentials(provider, opts) {
1536
1834
  await this.ensureLoaded();
1537
- const directStorageKeys = opts?.storageKeys && !(opts.storageKeys.length === 1 && opts.storageKeys[0] === provider) ? opts.storageKeys : provider === "moonshot" ? [MOONSHOT_OAUTH_KEY, "moonshot"] : [provider];
1835
+ const directStorageKeys = opts?.storageKeys && !(opts.storageKeys.length === 1 && opts.storageKeys[0] === provider) ? opts.storageKeys : providerStorageKeys(provider);
1538
1836
  if (!directStorageKeys.some((key) => Boolean(this.data[key]))) {
1539
1837
  await this.reloadLatest();
1540
1838
  }
@@ -1545,28 +1843,30 @@ var AuthStorage = class {
1545
1843
  }
1546
1844
  throw new NotLoggedInError(provider);
1547
1845
  }
1548
- if (provider === "moonshot" && this.data[MOONSHOT_OAUTH_KEY]) {
1549
- const exhaustedUntil = this.data[MOONSHOT_OAUTH_KEY].usageExhaustedUntil ?? 0;
1550
- if (Date.now() < exhaustedUntil && this.data["moonshot"]) {
1846
+ const dual = dualAuthProvider(provider);
1847
+ const dualOAuthCreds = dual ? this.data[dual.oauthKey] : void 0;
1848
+ if (dual && dualOAuthCreds) {
1849
+ const exhaustedUntil = dualOAuthCreds.usageExhaustedUntil ?? 0;
1850
+ if (Date.now() < exhaustedUntil && this.data[dual.provider]) {
1551
1851
  log(
1552
1852
  "WARN",
1553
1853
  "auth",
1554
- `Kimi OAuth usage window is exhausted \u2014 using the Moonshot API key until ${new Date(exhaustedUntil).toISOString()} (OAuth resumes automatically).`
1854
+ `${dual.oauthLabel} usage window is exhausted \u2014 using the ${dual.apiKeyLabel} until ${new Date(exhaustedUntil).toISOString()} (OAuth resumes automatically).`
1555
1855
  );
1556
- return this.data["moonshot"];
1856
+ return this.data[dual.provider];
1557
1857
  }
1558
1858
  try {
1559
- return await this.resolveCredentials(MOONSHOT_OAUTH_KEY, {
1859
+ return await this.resolveCredentials(dual.oauthKey, {
1560
1860
  ...opts?.forceRefresh ? { forceRefresh: true } : {}
1561
1861
  });
1562
1862
  } catch (err) {
1563
- if (err instanceof NotLoggedInError && this.data["moonshot"]) {
1863
+ if (err instanceof NotLoggedInError && this.data[dual.provider]) {
1564
1864
  log(
1565
1865
  "WARN",
1566
1866
  "auth",
1567
- 'Kimi OAuth credential is no longer valid \u2014 falling back to the Moonshot API key. Run "ggcoder login" and choose Kimi OAuth to restore OAuth auth.'
1867
+ `${dual.oauthLabel} credential is no longer valid \u2014 falling back to the ${dual.apiKeyLabel}. ${dual.restoreHint}`
1568
1868
  );
1569
- return this.data["moonshot"];
1869
+ return this.data[dual.provider];
1570
1870
  }
1571
1871
  throw err;
1572
1872
  }
@@ -1595,7 +1895,7 @@ var AuthStorage = class {
1595
1895
  this.data = latest;
1596
1896
  return latestCreds;
1597
1897
  }
1598
- const refreshFn = provider === "anthropic" ? refreshAnthropicToken : provider === "gemini" ? refreshGeminiToken : provider === MOONSHOT_OAUTH_KEY ? refreshKimiToken : refreshOpenAIToken;
1898
+ const refreshFn = provider === "anthropic" ? refreshAnthropicToken : provider === "gemini" ? refreshGeminiToken : provider === MOONSHOT_OAUTH_KEY ? refreshKimiToken : provider === XAI_OAUTH_KEY ? refreshXaiToken : refreshOpenAIToken;
1599
1899
  let refreshed;
1600
1900
  try {
1601
1901
  refreshed = await refreshFn(latestCreds.refreshToken);
@@ -2828,7 +3128,7 @@ var TelegramBot = class {
2828
3128
  } catch (err) {
2829
3129
  if (!this.running) break;
2830
3130
  console.error(`[telegram] Poll error: ${err instanceof Error ? err.message : err}`);
2831
- await sleep2(3e3);
3131
+ await sleep3(3e3);
2832
3132
  }
2833
3133
  }
2834
3134
  }
@@ -2998,7 +3298,7 @@ function splitMessage(text) {
2998
3298
  }
2999
3299
  return chunks;
3000
3300
  }
3001
- function sleep2(ms) {
3301
+ function sleep3(ms) {
3002
3302
  return new Promise((r) => setTimeout(r, ms));
3003
3303
  }
3004
3304
 
@@ -3263,6 +3563,7 @@ function createAutoUpdater(config) {
3263
3563
  NotLoggedInError,
3264
3564
  SubscriptionUsageError,
3265
3565
  TelegramBot,
3566
+ XAI_OAUTH_KEY,
3266
3567
  XIAOMI_CREDITS_KEY,
3267
3568
  clearLocalDiscoveryCache,
3268
3569
  clearRuntimeModels,
@@ -3271,6 +3572,8 @@ function createAutoUpdater(config) {
3271
3572
  decodeOggOpus,
3272
3573
  discoverLocalModels,
3273
3574
  downmixToMono,
3575
+ dualAuthProvider,
3576
+ dualAuthProviderByOAuthKey,
3274
3577
  endpointRoot,
3275
3578
  fetchSubscriptionUsage,
3276
3579
  findProbedModel,
@@ -3295,6 +3598,9 @@ function createAutoUpdater(config) {
3295
3598
  getSupportedThinkingLevels,
3296
3599
  getToolResultCharLimit,
3297
3600
  getVideoByteLimit,
3601
+ grokCliBaseUrl,
3602
+ grokCliHeaders,
3603
+ isGrokCliEndpoint,
3298
3604
  isKimiCodingEndpoint,
3299
3605
  isLocalModelId,
3300
3606
  isLoggerOpen,
@@ -3308,14 +3614,18 @@ function createAutoUpdater(config) {
3308
3614
  loginGemini,
3309
3615
  loginKimi,
3310
3616
  loginOpenAI,
3617
+ loginXai,
3618
+ oauthStorageKey,
3311
3619
  openLog,
3312
3620
  parseLocalModelId,
3313
3621
  probeEndpoint,
3622
+ providerStorageKeys,
3314
3623
  readStoredBaseUrlSync,
3315
3624
  refreshAnthropicToken,
3316
3625
  refreshGeminiToken,
3317
3626
  refreshKimiToken,
3318
3627
  refreshOpenAIToken,
3628
+ refreshXaiToken,
3319
3629
  registerLogCleanup,
3320
3630
  registerRuntimeModels,
3321
3631
  resample,