@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.
@@ -765,13 +765,13 @@ var CodeAssistHttpError = class extends Error {
765
765
  }
766
766
  };
767
767
  async function loginGemini(callbacks) {
768
- const { clientId, clientSecret } = getGeminiOAuthClientCredentials();
768
+ const { clientId: clientId2, clientSecret } = getGeminiOAuthClientCredentials();
769
769
  const { verifier, challenge } = await generatePKCE();
770
770
  const state = crypto4.randomBytes(32).toString("hex");
771
771
  const redirectUri = await getLoopbackRedirectUri();
772
772
  const url = new URL(AUTHORIZE_URL3);
773
773
  url.searchParams.set("response_type", "code");
774
- url.searchParams.set("client_id", clientId);
774
+ url.searchParams.set("client_id", clientId2);
775
775
  url.searchParams.set("redirect_uri", redirectUri);
776
776
  url.searchParams.set("scope", SCOPE2);
777
777
  url.searchParams.set("access_type", "offline");
@@ -796,7 +796,7 @@ async function loginGemini(callbacks) {
796
796
  }
797
797
  code = parsed.code;
798
798
  }
799
- const creds = await exchangeGeminiCode(code, verifier, redirectUri, clientId, clientSecret);
799
+ const creds = await exchangeGeminiCode(code, verifier, redirectUri, clientId2, clientSecret);
800
800
  callbacks.onStatus("Setting up Gemini Code Assist access...");
801
801
  const projectId = await setupCodeAssistProject(creds.accessToken, callbacks);
802
802
  return {
@@ -805,11 +805,11 @@ async function loginGemini(callbacks) {
805
805
  };
806
806
  }
807
807
  async function refreshGeminiToken(refreshToken) {
808
- const { clientId, clientSecret } = getGeminiOAuthClientCredentials();
808
+ const { clientId: clientId2, clientSecret } = getGeminiOAuthClientCredentials();
809
809
  const data = await postTokenRequest2({
810
810
  grant_type: "refresh_token",
811
811
  refresh_token: refreshToken,
812
- client_id: clientId,
812
+ client_id: clientId2,
813
813
  client_secret: clientSecret
814
814
  });
815
815
  return {
@@ -819,9 +819,9 @@ async function refreshGeminiToken(refreshToken) {
819
819
  };
820
820
  }
821
821
  function getGeminiOAuthClientCredentials() {
822
- const clientId = process.env[CLIENT_ID_ENV]?.trim() || DEFAULT_CLIENT_ID;
822
+ const clientId2 = process.env[CLIENT_ID_ENV]?.trim() || DEFAULT_CLIENT_ID;
823
823
  const clientSecret = process.env[CLIENT_SECRET_ENV]?.trim() || DEFAULT_CLIENT_SECRET;
824
- return { clientId, clientSecret };
824
+ return { clientId: clientId2, clientSecret };
825
825
  }
826
826
  async function getLoopbackRedirectUri() {
827
827
  return new Promise((resolve, reject) => {
@@ -900,10 +900,10 @@ async function loginWithServer2(authUrl, redirectUri, expectedState, callbacks)
900
900
  });
901
901
  });
902
902
  }
903
- async function exchangeGeminiCode(code, verifier, redirectUri, clientId, clientSecret) {
903
+ async function exchangeGeminiCode(code, verifier, redirectUri, clientId2, clientSecret) {
904
904
  const data = await postTokenRequest2({
905
905
  grant_type: "authorization_code",
906
- client_id: clientId,
906
+ client_id: clientId2,
907
907
  client_secret: clientSecret,
908
908
  code,
909
909
  redirect_uri: redirectUri,
@@ -1094,6 +1094,204 @@ function codeAssistHeaders(accessToken) {
1094
1094
  };
1095
1095
  }
1096
1096
 
1097
+ // src/oauth/xai.ts
1098
+ var DEFAULT_CLIENT_ID2 = "b1a00492-073a-47ea-816f-4c329264a828";
1099
+ var DEFAULT_ISSUER = "https://auth.x.ai";
1100
+ var DEFAULT_CLI_BASE_URL = "https://cli-chat-proxy.grok.com/v1";
1101
+ var OAUTH_SCOPE = "openid profile email offline_access grok-cli:access api:access";
1102
+ var DEFAULT_GROK_CLI_VERSION = "0.2.101";
1103
+ var DEVICE_TIMEOUT_FALLBACK_MS = 10 * 60 * 1e3;
1104
+ var DEFAULT_TOKEN_LIFETIME_SECONDS = 3600;
1105
+ function issuer() {
1106
+ return (process.env.XAI_OAUTH_ISSUER ?? DEFAULT_ISSUER).replace(/\/+$/, "");
1107
+ }
1108
+ function clientId() {
1109
+ return process.env.XAI_OAUTH_CLIENT_ID ?? DEFAULT_CLIENT_ID2;
1110
+ }
1111
+ function grokCliBaseUrl() {
1112
+ return (process.env.XAI_CLI_BASE_URL ?? DEFAULT_CLI_BASE_URL).replace(/\/+$/, "");
1113
+ }
1114
+ function grokCliVersion() {
1115
+ const raw = process.env.GROK_CLI_VERSION ?? DEFAULT_GROK_CLI_VERSION;
1116
+ const cleaned = raw.replace(/[^\u0020-\u007E]/g, "").trim();
1117
+ return cleaned.length > 0 ? cleaned : DEFAULT_GROK_CLI_VERSION;
1118
+ }
1119
+ function grokCliHeaders(modelId) {
1120
+ return {
1121
+ "X-XAI-Token-Auth": "xai-grok-cli",
1122
+ "x-grok-client-version": grokCliVersion(),
1123
+ "x-grok-client-identifier": "ggcoder",
1124
+ ...modelId ? { "x-grok-model-override": modelId } : {}
1125
+ };
1126
+ }
1127
+ function isGrokCliEndpoint(baseUrl) {
1128
+ if (typeof baseUrl !== "string" || baseUrl.length === 0) return false;
1129
+ const normalized = baseUrl.replace(/\/+$/, "");
1130
+ return normalized === grokCliBaseUrl() || /(^|\.)grok\.com/i.test(normalized);
1131
+ }
1132
+ async function postForm2(endpoint, params) {
1133
+ const response = await fetch(`${issuer()}${endpoint}`, {
1134
+ method: "POST",
1135
+ headers: {
1136
+ "Content-Type": "application/x-www-form-urlencoded",
1137
+ Accept: "application/json",
1138
+ "User-Agent": `grok-cli/${grokCliVersion()}`
1139
+ },
1140
+ body: new URLSearchParams(params).toString()
1141
+ });
1142
+ let data = {};
1143
+ try {
1144
+ const parsed = await response.json();
1145
+ if (parsed && typeof parsed === "object") data = parsed;
1146
+ } catch {
1147
+ }
1148
+ return { status: response.status, data };
1149
+ }
1150
+ function errorDetail2(data) {
1151
+ const desc = data.error_description ?? data.message ?? data.error;
1152
+ return typeof desc === "string" && desc.length > 0 ? desc : "unknown error";
1153
+ }
1154
+ function jwtExpirySeconds(token) {
1155
+ const payload = token.split(".")[1];
1156
+ if (!payload) return void 0;
1157
+ try {
1158
+ const json = Buffer.from(payload.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString(
1159
+ "utf-8"
1160
+ );
1161
+ const claims = JSON.parse(json);
1162
+ if (!claims || typeof claims !== "object") return void 0;
1163
+ const exp = claims.exp;
1164
+ return typeof exp === "number" && Number.isFinite(exp) && exp > 0 ? exp : void 0;
1165
+ } catch {
1166
+ return void 0;
1167
+ }
1168
+ }
1169
+ function credsFromTokenResponse2(data, opts) {
1170
+ const accessToken = data.access_token;
1171
+ const responseRefreshToken = data.refresh_token;
1172
+ if (typeof accessToken !== "string" || accessToken.length === 0) {
1173
+ throw new Error("Grok OAuth response missing access_token.");
1174
+ }
1175
+ const refreshToken = typeof responseRefreshToken === "string" && responseRefreshToken.length > 0 ? responseRefreshToken : opts?.fallbackRefreshToken ?? "";
1176
+ if (refreshToken.length === 0) {
1177
+ throw new Error(
1178
+ "Grok OAuth response missing refresh_token \u2014 the offline_access scope was not granted."
1179
+ );
1180
+ }
1181
+ const responseExpiresIn = Number(data.expires_in);
1182
+ let expiresIn;
1183
+ if (Number.isFinite(responseExpiresIn) && responseExpiresIn > 0) {
1184
+ expiresIn = responseExpiresIn;
1185
+ } else {
1186
+ const exp = jwtExpirySeconds(accessToken);
1187
+ const fromJwt = exp !== void 0 ? exp - Math.floor(Date.now() / 1e3) : 0;
1188
+ expiresIn = fromJwt > 0 ? fromJwt : DEFAULT_TOKEN_LIFETIME_SECONDS;
1189
+ }
1190
+ return {
1191
+ accessToken,
1192
+ refreshToken,
1193
+ expiresAt: Date.now() + expiresIn * 1e3,
1194
+ expiresIn,
1195
+ baseUrl: grokCliBaseUrl()
1196
+ };
1197
+ }
1198
+ async function requestDeviceAuthorization2() {
1199
+ const { status, data } = await postForm2("/oauth2/device/code", {
1200
+ client_id: clientId(),
1201
+ scope: OAUTH_SCOPE
1202
+ });
1203
+ if (status !== 200) {
1204
+ throw new Error(`Grok device authorization failed (${status}): ${errorDetail2(data)}`);
1205
+ }
1206
+ const userCode = data.user_code;
1207
+ const deviceCode = data.device_code;
1208
+ const verificationUriComplete = data.verification_uri_complete;
1209
+ if (typeof userCode !== "string" || typeof deviceCode !== "string") {
1210
+ throw new Error("Grok device authorization response missing user_code/device_code.");
1211
+ }
1212
+ return {
1213
+ userCode,
1214
+ deviceCode,
1215
+ verificationUri: typeof data.verification_uri === "string" ? data.verification_uri : "",
1216
+ verificationUriComplete: typeof verificationUriComplete === "string" ? verificationUriComplete : "",
1217
+ interval: Number(data.interval ?? 5) || 5,
1218
+ // RFC 8628 §3.2: the server states how long the device code lives. Honor it
1219
+ // rather than imposing our own budget — polling a code we know is dead only
1220
+ // burns requests, and a longer local window would keep a user waiting past
1221
+ // the point the code can ever succeed.
1222
+ expiresInMs: (Number(data.expires_in) || 0) > 0 ? Number(data.expires_in) * 1e3 : DEVICE_TIMEOUT_FALLBACK_MS
1223
+ };
1224
+ }
1225
+ async function pollDeviceToken2(deviceCode) {
1226
+ const { status, data } = await postForm2("/oauth2/token", {
1227
+ client_id: clientId(),
1228
+ device_code: deviceCode,
1229
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
1230
+ });
1231
+ if (status === 200 && typeof data.access_token === "string") {
1232
+ return { kind: "success", creds: credsFromTokenResponse2(data) };
1233
+ }
1234
+ if (status >= 500) {
1235
+ throw new Error(`Grok token polling server error (${status}): ${errorDetail2(data)}`);
1236
+ }
1237
+ const errorCode = typeof data.error === "string" ? data.error : "unknown_error";
1238
+ switch (errorCode) {
1239
+ case "authorization_pending":
1240
+ return { kind: "pending" };
1241
+ case "slow_down":
1242
+ return { kind: "slow_down" };
1243
+ case "expired_token":
1244
+ return { kind: "expired" };
1245
+ case "access_denied":
1246
+ return { kind: "denied" };
1247
+ default:
1248
+ throw new Error(`Grok token polling failed (${status}): ${errorDetail2(data)}`);
1249
+ }
1250
+ }
1251
+ function sleep2(ms) {
1252
+ return new Promise((resolve) => {
1253
+ setTimeout(resolve, ms);
1254
+ });
1255
+ }
1256
+ async function loginXai(callbacks) {
1257
+ const auth = await requestDeviceAuthorization2();
1258
+ callbacks.onStatus(
1259
+ `Visit ${auth.verificationUri || auth.verificationUriComplete} and enter code: ${auth.userCode}`
1260
+ );
1261
+ callbacks.onOpenUrl(auth.verificationUriComplete || auth.verificationUri);
1262
+ callbacks.onStatus("Waiting for you to authorize in the browser...");
1263
+ const deadline = Date.now() + auth.expiresInMs;
1264
+ let interval = Math.max(auth.interval, 1);
1265
+ while (Date.now() < deadline) {
1266
+ const remaining = deadline - Date.now();
1267
+ await sleep2(Math.min(interval * 1e3, remaining));
1268
+ const result = await pollDeviceToken2(auth.deviceCode);
1269
+ if (result.kind === "success") return result.creds;
1270
+ if (result.kind === "denied") {
1271
+ throw new Error("Grok authorization was denied.");
1272
+ }
1273
+ if (result.kind === "expired") {
1274
+ throw new Error("Grok device code expired. Please run login again.");
1275
+ }
1276
+ if (result.kind === "slow_down") {
1277
+ interval += 5;
1278
+ }
1279
+ }
1280
+ throw new Error("Grok login timed out. Please run login again.");
1281
+ }
1282
+ async function refreshXaiToken(refreshToken) {
1283
+ const { status, data } = await postForm2("/oauth2/token", {
1284
+ client_id: clientId(),
1285
+ grant_type: "refresh_token",
1286
+ refresh_token: refreshToken
1287
+ });
1288
+ if (status === 200 && typeof data.access_token === "string") {
1289
+ return credsFromTokenResponse2(data, { fallbackRefreshToken: refreshToken });
1290
+ }
1291
+ const errorCode = typeof data.error === "string" ? data.error : "";
1292
+ throw new Error(`Grok token refresh failed (${status}): ${errorCode || errorDetail2(data)}`);
1293
+ }
1294
+
1097
1295
  // src/file-lock.ts
1098
1296
  import fs3 from "fs/promises";
1099
1297
  import { setTimeout as setTimeout2 } from "timers/promises";
@@ -1158,18 +1356,49 @@ function isAlive(pid) {
1158
1356
 
1159
1357
  // src/auth-storage.ts
1160
1358
  var MOONSHOT_OAUTH_KEY = "moonshot-oauth";
1359
+ var XAI_OAUTH_KEY = "xai-oauth";
1360
+ var DUAL_AUTH_PROVIDERS = [
1361
+ {
1362
+ provider: "moonshot",
1363
+ oauthKey: MOONSHOT_OAUTH_KEY,
1364
+ oauthLabel: "Kimi OAuth",
1365
+ apiKeyLabel: "Moonshot API key",
1366
+ restoreHint: 'Run "ggcoder login" and choose Kimi OAuth to restore OAuth auth.'
1367
+ },
1368
+ {
1369
+ provider: "xai",
1370
+ oauthKey: XAI_OAUTH_KEY,
1371
+ oauthLabel: "Grok OAuth",
1372
+ apiKeyLabel: "xAI API key",
1373
+ restoreHint: 'Run "ggcoder login" and choose Grok OAuth to restore OAuth auth.'
1374
+ }
1375
+ ];
1376
+ function dualAuthProvider(provider) {
1377
+ return DUAL_AUTH_PROVIDERS.find((entry) => entry.provider === provider);
1378
+ }
1379
+ function dualAuthProviderByOAuthKey(storageKey) {
1380
+ return DUAL_AUTH_PROVIDERS.find((entry) => entry.oauthKey === storageKey);
1381
+ }
1382
+ function oauthStorageKey(provider) {
1383
+ return dualAuthProvider(provider)?.oauthKey;
1384
+ }
1385
+ function providerStorageKeys(provider) {
1386
+ const dual = dualAuthProvider(provider);
1387
+ return dual ? [dual.oauthKey, dual.provider] : [provider];
1388
+ }
1161
1389
  var XIAOMI_CREDITS_KEY = "xiaomi-credits";
1162
1390
  var LOCAL_AUTH_KEY_PREFIX = "local:";
1163
1391
  var LOCAL_CREDENTIAL_LIFETIME_MS = 100 * 365 * 24 * 60 * 60 * 1e3;
1164
1392
  function activeBaseUrlEntry(data, provider) {
1165
- if (provider === "moonshot") {
1166
- const oauth = data[MOONSHOT_OAUTH_KEY];
1393
+ const dual = dualAuthProvider(provider);
1394
+ if (dual) {
1395
+ const oauth = data[dual.oauthKey];
1167
1396
  if (oauth) {
1168
1397
  const exhaustedUntil = oauth.usageExhaustedUntil ?? 0;
1169
- if (Date.now() < exhaustedUntil && data["moonshot"]) return data["moonshot"];
1398
+ if (Date.now() < exhaustedUntil && data[dual.provider]) return data[dual.provider];
1170
1399
  return oauth;
1171
1400
  }
1172
- return data["moonshot"];
1401
+ return data[dual.provider];
1173
1402
  }
1174
1403
  return data[provider];
1175
1404
  }
@@ -1204,6 +1433,16 @@ var AuthStorage = class {
1204
1433
  data = {};
1205
1434
  filePath;
1206
1435
  loaded = false;
1436
+ /**
1437
+ * mtime+size of the file as of the cached snapshot (`size: -1` = no file).
1438
+ * auth.json is shared: the desktop app writes API keys and disconnects
1439
+ * NATIVELY (so they work with no daemon running), and every window/process has
1440
+ * its own AuthStorage. A load-once cache therefore goes stale — the sidecar
1441
+ * would keep listing models for a provider just disconnected, and hide the
1442
+ * ones just connected, until the daemon restarted.
1443
+ */
1444
+ snapshotMtimeMs = 0;
1445
+ snapshotSize = -1;
1207
1446
  /** Per-provider lock to serialize concurrent refresh calls. */
1208
1447
  refreshLocks = /* @__PURE__ */ new Map();
1209
1448
  constructor(filePath) {
@@ -1215,12 +1454,12 @@ var AuthStorage = class {
1215
1454
  }
1216
1455
  /** List provider keys with stored credentials. */
1217
1456
  async listProviders() {
1218
- await this.ensureLoaded();
1457
+ await this.ensureFresh();
1219
1458
  return Object.keys(this.data);
1220
1459
  }
1221
1460
  /** True if credentials exist for `provider`. */
1222
1461
  async hasCredentials(provider) {
1223
- await this.ensureLoaded();
1462
+ await this.ensureFresh();
1224
1463
  return Boolean(this.data[provider]);
1225
1464
  }
1226
1465
  /**
@@ -1231,18 +1470,19 @@ var AuthStorage = class {
1231
1470
  * instead of re-deriving the same order.
1232
1471
  */
1233
1472
  async pickStorageKey(keys) {
1234
- await this.ensureLoaded();
1473
+ await this.ensureFresh();
1235
1474
  return keys.find((key) => Boolean(this.data[key]));
1236
1475
  }
1237
1476
  /**
1238
- * True if the user has any usable auth for the logical provider. For
1239
- * `moonshot` this is satisfied by either the Kimi OAuth credential or the
1240
- * Moonshot API key.
1477
+ * True if the user has any usable auth for the logical provider. For a
1478
+ * dual-auth provider (Kimi/Grok) either the OAuth credential or the API key
1479
+ * satisfies it.
1241
1480
  */
1242
1481
  async hasProviderAuth(provider) {
1243
- await this.ensureLoaded();
1244
- if (provider === "moonshot") {
1245
- return Boolean(this.data[MOONSHOT_OAUTH_KEY] || this.data["moonshot"]);
1482
+ await this.ensureFresh();
1483
+ const dual = dualAuthProvider(provider);
1484
+ if (dual) {
1485
+ return Boolean(this.data[dual.oauthKey] || this.data[dual.provider]);
1246
1486
  }
1247
1487
  if (provider === "xiaomi") {
1248
1488
  return Boolean(this.data["xiaomi"] || this.data[XIAOMI_CREDITS_KEY]);
@@ -1276,14 +1516,16 @@ var AuthStorage = class {
1276
1516
  }
1277
1517
  /**
1278
1518
  * True if the active credential for `provider` is a static API key with no
1279
- * refresh mechanism. For `moonshot` this is only true when the Kimi OAuth
1280
- * credential is absent (a present OAuth credential is refreshable).
1519
+ * refresh mechanism. For a dual-auth provider this is only true when its OAuth
1520
+ * credential is absent or sidelined (a live OAuth credential is refreshable).
1281
1521
  */
1282
1522
  async isStaticApiKey(provider) {
1283
- await this.ensureLoaded();
1284
- if (provider === "moonshot" && this.data[MOONSHOT_OAUTH_KEY]) {
1285
- const exhaustedUntil = this.data[MOONSHOT_OAUTH_KEY].usageExhaustedUntil ?? 0;
1286
- const apiKeyActive = Date.now() < exhaustedUntil && Boolean(this.data["moonshot"]);
1523
+ await this.ensureFresh();
1524
+ const dual = dualAuthProvider(provider);
1525
+ const oauthCreds = dual ? this.data[dual.oauthKey] : void 0;
1526
+ if (dual && oauthCreds) {
1527
+ const exhaustedUntil = oauthCreds.usageExhaustedUntil ?? 0;
1528
+ const apiKeyActive = Date.now() < exhaustedUntil && Boolean(this.data[dual.provider]);
1287
1529
  if (!apiKeyActive) return false;
1288
1530
  }
1289
1531
  return STATIC_API_KEY_PROVIDERS.has(provider);
@@ -1291,26 +1533,30 @@ var AuthStorage = class {
1291
1533
  /**
1292
1534
  * The base URL on the credential that is active right now, if any.
1293
1535
  * Synchronous — call only after load()/resolveCredentials() populated the
1294
- * snapshot. For `moonshot` this is the Kimi For Coding URL whenever the
1295
- * OAuth entry is the one resolveCredentials would serve (i.e. not currently
1296
- * usage-exhausted with an API key configured).
1536
+ * snapshot. For a dual-auth provider this is the subscription endpoint (Kimi
1537
+ * For Coding, the Grok CLI proxy) whenever the OAuth entry is the one
1538
+ * resolveCredentials would serve (i.e. not currently usage-exhausted with an
1539
+ * API key configured).
1297
1540
  */
1298
1541
  getStoredBaseUrl(provider) {
1299
1542
  return activeBaseUrlEntry(this.data, provider)?.baseUrl;
1300
1543
  }
1301
1544
  async load() {
1545
+ const first = !this.loaded;
1302
1546
  await withFileLock(this.filePath, async () => {
1303
1547
  try {
1304
1548
  const content = await fs4.readFile(this.filePath, "utf-8");
1305
1549
  this.data = JSON.parse(content);
1306
- log("INFO", "auth", `Loaded credentials from ${this.filePath}`, {
1307
- providers: Object.keys(this.data).join(",") || "(none)"
1308
- });
1550
+ if (first) {
1551
+ log("INFO", "auth", `Loaded credentials from ${this.filePath}`, {
1552
+ providers: Object.keys(this.data).join(",") || "(none)"
1553
+ });
1554
+ }
1309
1555
  } catch (err) {
1310
1556
  this.data = {};
1311
1557
  const code = err.code;
1312
1558
  if (code === "ENOENT") {
1313
- log("INFO", "auth", `No auth file found at ${this.filePath} (first run)`);
1559
+ if (first) log("INFO", "auth", `No auth file found at ${this.filePath} (first run)`);
1314
1560
  } else {
1315
1561
  log(
1316
1562
  "ERROR",
@@ -1322,10 +1568,50 @@ var AuthStorage = class {
1322
1568
  }
1323
1569
  });
1324
1570
  this.loaded = true;
1571
+ await this.rememberSnapshot();
1325
1572
  }
1326
1573
  async ensureLoaded() {
1327
1574
  if (!this.loaded) await this.load();
1328
1575
  }
1576
+ /**
1577
+ * Like {@link ensureLoaded}, but re-reads when the file changed since this
1578
+ * snapshot — a cheap stat, not a re-parse. Used by the "what is connected?"
1579
+ * readers, which must reflect writes made by another window, the CLI, or the
1580
+ * desktop app's native (daemon-free) API-key and disconnect paths.
1581
+ *
1582
+ * Deliberately NOT used by {@link resolveCredentials}: that path compares the
1583
+ * caller's snapshot against the latest file to detect a concurrent re-login,
1584
+ * and silently refreshing this instance's view first would destroy the
1585
+ * evidence that the token it just had rejected has already been replaced.
1586
+ */
1587
+ async ensureFresh() {
1588
+ if (!this.loaded) {
1589
+ await this.load();
1590
+ return;
1591
+ }
1592
+ let changed;
1593
+ try {
1594
+ const stat = await fs4.stat(this.filePath);
1595
+ changed = stat.mtimeMs !== this.snapshotMtimeMs || stat.size !== this.snapshotSize;
1596
+ } catch {
1597
+ changed = this.snapshotSize !== -1;
1598
+ }
1599
+ if (changed) await this.load();
1600
+ }
1601
+ /**
1602
+ * Record the file identity behind the current snapshot, so {@link ensureLoaded}
1603
+ * can tell "someone else wrote" from "this is our own write".
1604
+ */
1605
+ async rememberSnapshot() {
1606
+ try {
1607
+ const stat = await fs4.stat(this.filePath);
1608
+ this.snapshotMtimeMs = stat.mtimeMs;
1609
+ this.snapshotSize = stat.size;
1610
+ } catch {
1611
+ this.snapshotMtimeMs = 0;
1612
+ this.snapshotSize = -1;
1613
+ }
1614
+ }
1329
1615
  /**
1330
1616
  * Apply one provider-scoped mutation to the latest on-disk snapshot.
1331
1617
  * AuthStorage instances live in every app session/process, so writing this
@@ -1341,14 +1627,16 @@ var AuthStorage = class {
1341
1627
  await atomicWriteFile(this.filePath, JSON.stringify(latest, null, 2));
1342
1628
  this.data = latest;
1343
1629
  });
1630
+ await this.rememberSnapshot();
1344
1631
  }
1345
1632
  async reloadLatest() {
1346
1633
  await withFileLock(this.filePath, async () => {
1347
1634
  this.data = await readAuthData(this.filePath);
1348
1635
  });
1636
+ await this.rememberSnapshot();
1349
1637
  }
1350
1638
  async getCredentials(provider) {
1351
- await this.ensureLoaded();
1639
+ await this.ensureFresh();
1352
1640
  return this.data[provider];
1353
1641
  }
1354
1642
  async setCredentials(provider, creds) {
@@ -1366,7 +1654,7 @@ var AuthStorage = class {
1366
1654
  * `resetsAt` (unix SECONDS, from the provider's rate-limit response) or a
1367
1655
  * 15-minute default when no reset time is known. While the mark is in the
1368
1656
  * future, `resolveCredentials("moonshot")` serves the Moonshot API key
1369
- * instead of the Kimi OAuth credential (when both are configured) — OAuth
1657
+ * instead of the subscription OAuth credential (when both are configured) — OAuth
1370
1658
  * stays the preferred credential and is retried automatically once the mark
1371
1659
  * lapses. Persisted to auth.json so a restart (or another gg-app window)
1372
1660
  * doesn't burn a request rediscovering the same exhausted window. No-op if
@@ -1403,7 +1691,7 @@ var AuthStorage = class {
1403
1691
  */
1404
1692
  async resolveCredentials(provider, opts) {
1405
1693
  await this.ensureLoaded();
1406
- const directStorageKeys = opts?.storageKeys && !(opts.storageKeys.length === 1 && opts.storageKeys[0] === provider) ? opts.storageKeys : provider === "moonshot" ? [MOONSHOT_OAUTH_KEY, "moonshot"] : [provider];
1694
+ const directStorageKeys = opts?.storageKeys && !(opts.storageKeys.length === 1 && opts.storageKeys[0] === provider) ? opts.storageKeys : providerStorageKeys(provider);
1407
1695
  if (!directStorageKeys.some((key) => Boolean(this.data[key]))) {
1408
1696
  await this.reloadLatest();
1409
1697
  }
@@ -1414,28 +1702,30 @@ var AuthStorage = class {
1414
1702
  }
1415
1703
  throw new NotLoggedInError(provider);
1416
1704
  }
1417
- if (provider === "moonshot" && this.data[MOONSHOT_OAUTH_KEY]) {
1418
- const exhaustedUntil = this.data[MOONSHOT_OAUTH_KEY].usageExhaustedUntil ?? 0;
1419
- if (Date.now() < exhaustedUntil && this.data["moonshot"]) {
1705
+ const dual = dualAuthProvider(provider);
1706
+ const dualOAuthCreds = dual ? this.data[dual.oauthKey] : void 0;
1707
+ if (dual && dualOAuthCreds) {
1708
+ const exhaustedUntil = dualOAuthCreds.usageExhaustedUntil ?? 0;
1709
+ if (Date.now() < exhaustedUntil && this.data[dual.provider]) {
1420
1710
  log(
1421
1711
  "WARN",
1422
1712
  "auth",
1423
- `Kimi OAuth usage window is exhausted \u2014 using the Moonshot API key until ${new Date(exhaustedUntil).toISOString()} (OAuth resumes automatically).`
1713
+ `${dual.oauthLabel} usage window is exhausted \u2014 using the ${dual.apiKeyLabel} until ${new Date(exhaustedUntil).toISOString()} (OAuth resumes automatically).`
1424
1714
  );
1425
- return this.data["moonshot"];
1715
+ return this.data[dual.provider];
1426
1716
  }
1427
1717
  try {
1428
- return await this.resolveCredentials(MOONSHOT_OAUTH_KEY, {
1718
+ return await this.resolveCredentials(dual.oauthKey, {
1429
1719
  ...opts?.forceRefresh ? { forceRefresh: true } : {}
1430
1720
  });
1431
1721
  } catch (err) {
1432
- if (err instanceof NotLoggedInError && this.data["moonshot"]) {
1722
+ if (err instanceof NotLoggedInError && this.data[dual.provider]) {
1433
1723
  log(
1434
1724
  "WARN",
1435
1725
  "auth",
1436
- '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.'
1726
+ `${dual.oauthLabel} credential is no longer valid \u2014 falling back to the ${dual.apiKeyLabel}. ${dual.restoreHint}`
1437
1727
  );
1438
- return this.data["moonshot"];
1728
+ return this.data[dual.provider];
1439
1729
  }
1440
1730
  throw err;
1441
1731
  }
@@ -1464,7 +1754,7 @@ var AuthStorage = class {
1464
1754
  this.data = latest;
1465
1755
  return latestCreds;
1466
1756
  }
1467
- const refreshFn = provider === "anthropic" ? refreshAnthropicToken : provider === "gemini" ? refreshGeminiToken : provider === MOONSHOT_OAUTH_KEY ? refreshKimiToken : refreshOpenAIToken;
1757
+ const refreshFn = provider === "anthropic" ? refreshAnthropicToken : provider === "gemini" ? refreshGeminiToken : provider === MOONSHOT_OAUTH_KEY ? refreshKimiToken : provider === XAI_OAUTH_KEY ? refreshXaiToken : refreshOpenAIToken;
1468
1758
  let refreshed;
1469
1759
  try {
1470
1760
  refreshed = await refreshFn(latestCreds.refreshToken);
@@ -2084,8 +2374,18 @@ export {
2084
2374
  refreshOpenAIToken,
2085
2375
  loginGemini,
2086
2376
  refreshGeminiToken,
2377
+ grokCliBaseUrl,
2378
+ grokCliHeaders,
2379
+ isGrokCliEndpoint,
2380
+ loginXai,
2381
+ refreshXaiToken,
2087
2382
  withFileLock,
2088
2383
  MOONSHOT_OAUTH_KEY,
2384
+ XAI_OAUTH_KEY,
2385
+ dualAuthProvider,
2386
+ dualAuthProviderByOAuthKey,
2387
+ oauthStorageKey,
2388
+ providerStorageKeys,
2089
2389
  XIAOMI_CREDITS_KEY,
2090
2390
  LOCAL_AUTH_KEY_PREFIX,
2091
2391
  readStoredBaseUrlSync,
@@ -2110,4 +2410,4 @@ export {
2110
2410
  getSummaryModel,
2111
2411
  getFastModel
2112
2412
  };
2113
- //# sourceMappingURL=chunk-C6Q3GFWE.js.map
2413
+ //# sourceMappingURL=chunk-XDE6VUI4.js.map