@kenkaiiii/gg-core 5.94.0 → 5.95.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
@@ -32,29 +32,38 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  AUTH_PROVIDERS: () => AUTH_PROVIDERS,
34
34
  AuthStorage: () => AuthStorage,
35
+ CHATGPT_PLAN_BASE_URL: () => CHATGPT_PLAN_BASE_URL,
36
+ CHATGPT_PLAN_NOT_ENABLED_MESSAGE: () => CHATGPT_PLAN_NOT_ENABLED_MESSAGE,
37
+ CHATGPT_PLAN_SCOPE: () => CHATGPT_PLAN_SCOPE,
38
+ CHATGPT_USAGE_URL: () => CHATGPT_USAGE_URL,
35
39
  CLAUDE_CODE_AUTH_KEY: () => CLAUDE_CODE_AUTH_KEY,
36
40
  CLAUDE_CODE_AUTH_MARKER: () => CLAUDE_CODE_AUTH_MARKER,
37
41
  CLAUDE_CODE_MANIFEST: () => CLAUDE_CODE_MANIFEST,
42
+ ChatGPTPlanNotEnabledError: () => ChatGPTPlanNotEnabledError,
38
43
  DEFAULT_LOCAL_ENDPOINTS: () => DEFAULT_LOCAL_ENDPOINTS,
39
44
  DEFAULT_MAX_VIDEO_BYTES: () => DEFAULT_MAX_VIDEO_BYTES,
40
45
  FALLBACK_CONTEXT_WINDOW: () => FALLBACK_CONTEXT_WINDOW,
41
46
  GEMINI_API_KEY: () => GEMINI_API_KEY,
42
47
  GEMINI_API_KEY_BASE_URL: () => GEMINI_API_KEY_BASE_URL,
43
48
  HIDDEN_PROVIDERS: () => HIDDEN_PROVIDERS,
49
+ KIMI_CODE_DEFAULT_BASE_URL: () => KIMI_CODE_DEFAULT_BASE_URL,
50
+ KIMI_CODE_KEY: () => KIMI_CODE_KEY,
51
+ KIMI_CODE_OVERSEAS_BASE_URL: () => KIMI_CODE_OVERSEAS_BASE_URL,
44
52
  LOCAL_API_KEY_PLACEHOLDER: () => LOCAL_API_KEY_PLACEHOLDER,
45
53
  LOCAL_AUTH_KEY_PREFIX: () => LOCAL_AUTH_KEY_PREFIX,
46
54
  MINIMAX_CN_BASE_URL: () => MINIMAX_CN_BASE_URL,
47
55
  MINIMAX_GLOBAL_BASE_URL: () => MINIMAX_GLOBAL_BASE_URL,
48
56
  MODELS: () => MODELS,
49
- MOONSHOT_OAUTH_KEY: () => MOONSHOT_OAUTH_KEY,
50
57
  NotLoggedInError: () => NotLoggedInError,
58
+ OPENAI_CHATGPT_KEY: () => OPENAI_CHATGPT_KEY,
51
59
  OPENROUTER_DEFAULT_BASE_URL: () => OPENROUTER_DEFAULT_BASE_URL,
52
60
  PLAN_MODE_THINKING_CAP: () => PLAN_MODE_THINKING_CAP,
61
+ RETIRED_SIGN_INS: () => RETIRED_SIGN_INS,
53
62
  SubscriptionUsageError: () => SubscriptionUsageError,
54
63
  TelegramBot: () => TelegramBot,
55
- XAI_OAUTH_KEY: () => XAI_OAUTH_KEY,
56
64
  XIAOMI_CREDITS_KEY: () => XIAOMI_CREDITS_KEY,
57
65
  XIAOMI_TOKEN_PLAN_BASE_URLS: () => XIAOMI_TOKEN_PLAN_BASE_URLS,
66
+ apiKeyStorageKeys: () => apiKeyStorageKeys,
58
67
  authPriorityNote: () => authPriorityNote,
59
68
  clampThinkingForPlanMode: () => clampThinkingForPlanMode,
60
69
  claudeCodeAuthStatus: () => claudeCodeAuthStatus,
@@ -71,6 +80,7 @@ __export(index_exports, {
71
80
  createAutoUpdater: () => createAutoUpdater,
72
81
  decodeOggOpus: () => decodeOggOpus,
73
82
  describeAuthMethods: () => describeAuthMethods,
83
+ detectKimiCodeBaseUrl: () => detectKimiCodeBaseUrl,
74
84
  detectMiniMaxBaseUrl: () => detectMiniMaxBaseUrl,
75
85
  detectXiaomiTokenPlanBaseUrl: () => detectXiaomiTokenPlanBaseUrl,
76
86
  discoverLocalModels: () => discoverLocalModels,
@@ -79,6 +89,7 @@ __export(index_exports, {
79
89
  dualAuthProviderByOAuthKey: () => dualAuthProviderByOAuthKey,
80
90
  endpointRoot: () => endpointRoot,
81
91
  ensureClaudeCodeBinary: () => ensureClaudeCodeBinary,
92
+ fetchChatGPTPlanModels: () => fetchChatGPTPlanModels,
82
93
  fetchSubscriptionUsage: () => fetchSubscriptionUsage,
83
94
  findProbedModel: () => findProbedModel,
84
95
  formatClaudeCodeInstallProgress: () => formatClaudeCodeInstallProgress,
@@ -89,6 +100,7 @@ __export(index_exports, {
89
100
  getAuthProvider: () => getAuthProvider,
90
101
  getAuthStorageKey: () => getAuthStorageKey,
91
102
  getAuthStorageKeys: () => getAuthStorageKeys,
103
+ getChatGPTHostId: () => getChatGPTHostId,
92
104
  getContextWindow: () => getContextWindow,
93
105
  getDefaultModel: () => getDefaultModel,
94
106
  getDefaultThinkingLevel: () => getDefaultThinkingLevel,
@@ -104,10 +116,7 @@ __export(index_exports, {
104
116
  getSupportedThinkingLevels: () => getSupportedThinkingLevels,
105
117
  getToolResultCharLimit: () => getToolResultCharLimit,
106
118
  getVideoByteLimit: () => getVideoByteLimit,
107
- grokCliBaseUrl: () => grokCliBaseUrl,
108
- grokCliHeaders: () => grokCliHeaders,
109
119
  isClaudeCodeAuthMarker: () => isClaudeCodeAuthMarker,
110
- isGrokCliEndpoint: () => isGrokCliEndpoint,
111
120
  isHiddenProvider: () => isHiddenProvider,
112
121
  isKimiCodingEndpoint: () => isKimiCodingEndpoint,
113
122
  isLegacyClaudeOAuth: () => isLegacyClaudeOAuth,
@@ -116,21 +125,17 @@ __export(index_exports, {
116
125
  isModelLoaded: () => isModelLoaded,
117
126
  isPaidModel: () => isPaidModel,
118
127
  isThinkingLevelSupported: () => isThinkingLevelSupported,
119
- kimiCodeBaseUrl: () => kimiCodeBaseUrl,
120
- kimiCodingHeaders: () => kimiCodingHeaders,
121
128
  localAuthStorageKey: () => localAuthStorageKey,
122
129
  localModelShortName: () => localModelShortName,
123
130
  log: () => log,
131
+ loginChatGPT: () => loginChatGPT,
124
132
  loginClaudeCode: () => loginClaudeCode,
125
- loginGemini: () => loginGemini,
126
- loginKimi: () => loginKimi,
127
- loginOpenAI: () => loginOpenAI,
128
- loginXai: () => loginXai,
129
133
  logoutClaudeCode: () => logoutClaudeCode,
130
134
  mergeClaudeCodeRateLimit: () => mergeClaudeCodeRateLimit,
131
135
  oauthStorageKey: () => oauthStorageKey,
132
136
  ollamaServedContext: () => ollamaServedContext,
133
137
  openLog: () => openLog,
138
+ parseCallbackInput: () => parseCallbackInput,
134
139
  parseLocalModelId: () => parseLocalModelId,
135
140
  parseOpenRouterCatalog: () => parseOpenRouterCatalog,
136
141
  probeEndpoint: () => probeEndpoint,
@@ -138,32 +143,63 @@ __export(index_exports, {
138
143
  readClaudeCodeUsage: () => readClaudeCodeUsage,
139
144
  readStoredBaseUrlSync: () => readStoredBaseUrlSync,
140
145
  recordClaudeCodeRateLimit: () => recordClaudeCodeRateLimit,
141
- refreshGeminiToken: () => refreshGeminiToken,
142
- refreshKimiToken: () => refreshKimiToken,
143
- refreshOpenAIToken: () => refreshOpenAIToken,
144
- refreshXaiToken: () => refreshXaiToken,
146
+ refreshChatGPTToken: () => refreshChatGPTToken,
145
147
  registerLogCleanup: () => registerLogCleanup,
146
148
  registerRuntimeModels: () => registerRuntimeModels,
149
+ requiredKeyLabel: () => requiredKeyLabel,
147
150
  resample: () => resample,
148
151
  resolveClaudeCodeBinary: () => resolveClaudeCodeBinary,
152
+ revokeChatGPTSignIn: () => revokeChatGPTSignIn,
149
153
  setProgressCallback: () => setProgressCallback,
150
154
  setProviderCatalog: () => setProviderCatalog,
151
155
  signInWithClaudeCode: () => signInWithClaudeCode,
152
156
  signOutOfClaudeCode: () => signOutOfClaudeCode,
157
+ storageKeyProvider: () => storageKeyProvider,
153
158
  toModelInfo: () => toModelInfo,
154
159
  transcribeVoice: () => transcribeVoice,
155
160
  usesOpenAICodexTransport: () => usesOpenAICodexTransport,
161
+ verifyIdToken: () => verifyIdToken,
156
162
  withFileLock: () => withFileLock,
157
163
  withoutPaidTier: () => withoutPaidTier
158
164
  });
159
165
  module.exports = __toCommonJS(index_exports);
160
166
 
161
- // src/oauth/kimi.ts
162
- var import_node_child_process = require("child_process");
163
- var import_node_crypto = require("crypto");
164
- var import_node_fs = require("fs");
165
- var import_node_os2 = require("os");
166
- var import_node_path2 = __toESM(require("path"), 1);
167
+ // src/kimi-region.ts
168
+ var import_gg_ai = require("@kenkaiiii/gg-ai");
169
+ var KIMI_CODE_DEFAULT_BASE_URL = "https://api.kimi.com/coding/v1";
170
+ var KIMI_CODE_OVERSEAS_BASE_URL = "https://api.kimi.ai/coding/v1";
171
+ var KIMI_CODE_BASE_URLS = [KIMI_CODE_DEFAULT_BASE_URL, KIMI_CODE_OVERSEAS_BASE_URL];
172
+ async function probeHost(baseUrl, apiKey, fetchImpl, timeoutMs) {
173
+ try {
174
+ const res = await fetchImpl(`${baseUrl}/models`, {
175
+ headers: { Authorization: `Bearer ${apiKey}`, "User-Agent": import_gg_ai.GG_CODER_USER_AGENT },
176
+ signal: AbortSignal.timeout(timeoutMs)
177
+ });
178
+ if (res.ok) return "authorized";
179
+ if (res.status === 401 || res.status === 403) return "unauthorized";
180
+ return "inconclusive";
181
+ } catch {
182
+ return "inconclusive";
183
+ }
184
+ }
185
+ async function detectKimiCodeBaseUrl(apiKey, options = {}) {
186
+ const fetchImpl = options.fetch ?? fetch;
187
+ const timeoutMs = options.timeoutMs ?? 5e3;
188
+ const results = await Promise.all(
189
+ KIMI_CODE_BASE_URLS.map((url) => probeHost(url, apiKey, fetchImpl, timeoutMs))
190
+ );
191
+ const index = results.indexOf("authorized");
192
+ return index === -1 ? void 0 : KIMI_CODE_BASE_URLS[index];
193
+ }
194
+ function isKimiCodingEndpoint(baseUrl) {
195
+ if (typeof baseUrl !== "string" || baseUrl.length === 0) return false;
196
+ return /^https:\/\/api\.kimi\.(com|ai)\/coding(\/|$)/i.test(baseUrl);
197
+ }
198
+
199
+ // src/auth-storage.ts
200
+ var import_promises6 = __toESM(require("fs/promises"), 1);
201
+ var import_node_fs4 = require("fs");
202
+ var import_node_crypto4 = __toESM(require("crypto"), 1);
167
203
 
168
204
  // src/paths.ts
169
205
  var import_node_path = __toESM(require("path"), 1);
@@ -194,240 +230,15 @@ function getAppPaths() {
194
230
  };
195
231
  }
196
232
 
197
- // src/oauth/request-timeout.ts
198
- var OAUTH_REQUEST_TIMEOUT_MS = 3e4;
199
- function oauthRequestSignal() {
200
- return AbortSignal.timeout(OAUTH_REQUEST_TIMEOUT_MS);
201
- }
202
-
203
- // src/oauth/kimi.ts
204
- var CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098";
205
- var DEFAULT_OAUTH_HOST = "https://auth.kimi.com";
206
- var DEFAULT_CODING_BASE_URL = "https://api.kimi.com/coding/v1";
207
- var KIMI_PLATFORM = "kimi_code_cli";
208
- var DEFAULT_KIMI_VERSION = "1.0.11";
209
- var DEVICE_TIMEOUT_MS = 15 * 60 * 1e3;
210
- function oauthHost() {
211
- const host = process.env.KIMI_CODE_OAUTH_HOST ?? process.env.KIMI_OAUTH_HOST ?? DEFAULT_OAUTH_HOST;
212
- return host.replace(/\/+$/, "");
213
- }
214
- function kimiCodeBaseUrl() {
215
- return (process.env.KIMI_CODE_BASE_URL ?? DEFAULT_CODING_BASE_URL).replace(/\/+$/, "");
216
- }
217
- function kimiVersion() {
218
- const v = process.env.KIMI_CODE_VERSION ?? DEFAULT_KIMI_VERSION;
219
- return asciiHeader(v, DEFAULT_KIMI_VERSION);
220
- }
221
- function asciiHeader(value, fallback = "unknown") {
222
- const cleaned = value.replace(/[^\u0020-\u007E]/g, "").trim();
223
- return cleaned.length > 0 ? cleaned : fallback;
224
- }
225
- function macOsProductVersion() {
226
- try {
227
- const version = (0, import_node_child_process.execFileSync)("/usr/bin/sw_vers", ["-productVersion"], {
228
- encoding: "utf-8",
229
- timeout: 1e3
230
- }).trim();
231
- return version.length > 0 ? version : void 0;
232
- } catch {
233
- return void 0;
234
- }
235
- }
236
- function deviceModel() {
237
- const os2 = (0, import_node_os2.type)();
238
- const version = (0, import_node_os2.release)();
239
- const osArch = (0, import_node_os2.arch)();
240
- if (os2 === "Darwin") return `macOS ${macOsProductVersion() ?? version} ${osArch}`;
241
- if (os2 === "Windows_NT") return `Windows ${version} ${osArch}`;
242
- return `${os2} ${version} ${osArch}`.trim();
243
- }
244
- function deviceId() {
245
- const idPath = import_node_path2.default.join(getAppPaths().agentDir, "kimi_device_id");
246
- if ((0, import_node_fs.existsSync)(idPath)) {
247
- try {
248
- const text = (0, import_node_fs.readFileSync)(idPath, "utf-8").trim();
249
- if (text.length > 0) return text;
250
- } catch {
251
- }
252
- }
253
- const id = (0, import_node_crypto.randomUUID)();
254
- try {
255
- (0, import_node_fs.mkdirSync)(getAppPaths().agentDir, { recursive: true, mode: 448 });
256
- (0, import_node_fs.writeFileSync)(idPath, id, { encoding: "utf-8", mode: 384 });
257
- } catch {
258
- }
259
- return id;
260
- }
261
- function deviceHeaders() {
262
- return {
263
- "X-Msh-Platform": KIMI_PLATFORM,
264
- "X-Msh-Version": kimiVersion(),
265
- "X-Msh-Device-Name": asciiHeader((0, import_node_os2.hostname)()),
266
- "X-Msh-Device-Model": asciiHeader(deviceModel()),
267
- "X-Msh-Os-Version": asciiHeader((0, import_node_os2.release)()),
268
- "X-Msh-Device-Id": deviceId()
269
- };
270
- }
271
- function kimiCodingHeaders() {
272
- return {
273
- "User-Agent": `kimi-code-cli/${kimiVersion()}`,
274
- ...deviceHeaders()
275
- };
276
- }
277
- function isKimiCodingEndpoint(baseUrl) {
278
- if (typeof baseUrl !== "string" || baseUrl.length === 0) return false;
279
- const normalized = baseUrl.replace(/\/+$/, "");
280
- return normalized === kimiCodeBaseUrl() || /(^|\.)kimi\.com/i.test(normalized);
281
- }
282
- async function postForm(endpoint, params) {
283
- const response = await fetch(`${oauthHost()}${endpoint}`, {
284
- method: "POST",
285
- headers: {
286
- ...deviceHeaders(),
287
- "Content-Type": "application/x-www-form-urlencoded",
288
- Accept: "application/json"
289
- },
290
- body: new URLSearchParams(params).toString(),
291
- signal: oauthRequestSignal()
292
- });
293
- let data = {};
294
- try {
295
- const parsed = await response.json();
296
- if (parsed && typeof parsed === "object") data = parsed;
297
- } catch {
298
- }
299
- return { status: response.status, data };
300
- }
301
- function errorDetail(data) {
302
- const desc = data.error_description ?? data.message ?? data.error;
303
- return typeof desc === "string" && desc.length > 0 ? desc : "unknown error";
304
- }
305
- function credsFromTokenResponse(data, opts) {
306
- const accessToken = data.access_token;
307
- const responseRefreshToken = data.refresh_token;
308
- const expiresIn = Number(data.expires_in);
309
- if (typeof accessToken !== "string" || accessToken.length === 0) {
310
- throw new Error("Kimi OAuth response missing access_token.");
311
- }
312
- const refreshToken = typeof responseRefreshToken === "string" && responseRefreshToken.length > 0 ? responseRefreshToken : opts?.fallbackRefreshToken ?? "";
313
- if (refreshToken.length === 0) {
314
- throw new Error("Kimi OAuth response missing refresh_token.");
315
- }
316
- if (!Number.isFinite(expiresIn) || expiresIn <= 0) {
317
- throw new Error("Kimi OAuth response missing or invalid expires_in.");
318
- }
319
- return {
320
- accessToken,
321
- refreshToken,
322
- expiresAt: Date.now() + expiresIn * 1e3,
323
- expiresIn,
324
- baseUrl: kimiCodeBaseUrl()
325
- };
326
- }
327
- async function requestDeviceAuthorization() {
328
- const { status, data } = await postForm("/api/oauth/device_authorization", {
329
- client_id: CLIENT_ID
330
- });
331
- if (status !== 200) {
332
- throw new Error(`Kimi device authorization failed (${status}): ${errorDetail(data)}`);
333
- }
334
- const userCode = data.user_code;
335
- const deviceCode = data.device_code;
336
- const verificationUriComplete = data.verification_uri_complete;
337
- if (typeof userCode !== "string" || typeof deviceCode !== "string") {
338
- throw new Error("Kimi device authorization response missing user_code/device_code.");
339
- }
340
- return {
341
- userCode,
342
- deviceCode,
343
- verificationUri: typeof data.verification_uri === "string" ? data.verification_uri : "",
344
- verificationUriComplete: typeof verificationUriComplete === "string" ? verificationUriComplete : "",
345
- interval: Number(data.interval ?? 5) || 5
346
- };
347
- }
348
- async function pollDeviceToken(deviceCode) {
349
- const { status, data } = await postForm("/api/oauth/token", {
350
- client_id: CLIENT_ID,
351
- device_code: deviceCode,
352
- grant_type: "urn:ietf:params:oauth:grant-type:device_code"
353
- });
354
- if (status === 200 && typeof data.access_token === "string") {
355
- return { kind: "success", creds: credsFromTokenResponse(data) };
356
- }
357
- if (status >= 500) {
358
- throw new Error(`Kimi token polling server error (${status}): ${errorDetail(data)}`);
359
- }
360
- const errorCode = typeof data.error === "string" ? data.error : "unknown_error";
361
- switch (errorCode) {
362
- case "authorization_pending":
363
- return { kind: "pending" };
364
- case "slow_down":
365
- return { kind: "slow_down" };
366
- case "expired_token":
367
- return { kind: "expired" };
368
- case "access_denied":
369
- return { kind: "denied" };
370
- default:
371
- throw new Error(`Kimi token polling failed (${status}): ${errorDetail(data)}`);
372
- }
373
- }
374
- function sleep(ms) {
375
- return new Promise((resolve) => {
376
- setTimeout(resolve, ms);
377
- });
378
- }
379
- async function loginKimi(callbacks) {
380
- const auth = await requestDeviceAuthorization();
381
- callbacks.onStatus(
382
- `Visit ${auth.verificationUri || auth.verificationUriComplete} and enter code: ${auth.userCode}`
383
- );
384
- callbacks.onOpenUrl(auth.verificationUriComplete || auth.verificationUri);
385
- callbacks.onStatus("Waiting for you to authorize in the browser...");
386
- const deadline = Date.now() + DEVICE_TIMEOUT_MS;
387
- let interval = Math.max(auth.interval, 1);
388
- while (Date.now() < deadline) {
389
- await sleep(interval * 1e3);
390
- const result = await pollDeviceToken(auth.deviceCode);
391
- if (result.kind === "success") return result.creds;
392
- if (result.kind === "denied") {
393
- throw new Error("Kimi authorization was denied.");
394
- }
395
- if (result.kind === "expired") {
396
- throw new Error("Kimi device code expired. Please run login again.");
397
- }
398
- if (result.kind === "slow_down") {
399
- interval += 5;
400
- }
401
- }
402
- throw new Error("Kimi login timed out. Please run login again.");
403
- }
404
- async function refreshKimiToken(refreshToken) {
405
- const { status, data } = await postForm("/api/oauth/token", {
406
- client_id: CLIENT_ID,
407
- grant_type: "refresh_token",
408
- refresh_token: refreshToken
409
- });
410
- if (status === 200 && typeof data.access_token === "string") {
411
- return credsFromTokenResponse(data, { fallbackRefreshToken: refreshToken });
412
- }
413
- const errorCode = typeof data.error === "string" ? data.error : "";
414
- throw new Error(`Kimi token refresh failed (${status}): ${errorCode || errorDetail(data)}`);
415
- }
416
-
417
- // src/auth-storage.ts
418
- var import_promises5 = __toESM(require("fs/promises"), 1);
419
- var import_node_fs5 = require("fs");
420
- var import_node_crypto6 = __toESM(require("crypto"), 1);
421
-
422
233
  // src/claude-code/runtime.ts
423
- var import_node_child_process2 = require("child_process");
424
- var import_node_crypto2 = __toESM(require("crypto"), 1);
425
- var import_node_fs3 = __toESM(require("fs"), 1);
234
+ var import_node_child_process = require("child_process");
235
+ var import_node_crypto = __toESM(require("crypto"), 1);
236
+ var import_node_fs2 = __toESM(require("fs"), 1);
426
237
  var import_promises2 = __toESM(require("fs/promises"), 1);
427
- var import_node_path3 = __toESM(require("path"), 1);
238
+ var import_node_path2 = __toESM(require("path"), 1);
428
239
 
429
240
  // src/claude-code/tar.ts
430
- var import_node_fs2 = __toESM(require("fs"), 1);
241
+ var import_node_fs = __toESM(require("fs"), 1);
431
242
  var import_promises = __toESM(require("fs/promises"), 1);
432
243
  var import_node_zlib = __toESM(require("zlib"), 1);
433
244
  var BLOCK = 512;
@@ -459,7 +270,7 @@ function paxPath(data) {
459
270
  return void 0;
460
271
  }
461
272
  async function extractFileFromTarGz(tgzPath, entryName, destPath) {
462
- const input = import_node_fs2.default.createReadStream(tgzPath).pipe(import_node_zlib.default.createGunzip());
273
+ const input = import_node_fs.default.createReadStream(tgzPath).pipe(import_node_zlib.default.createGunzip());
463
274
  let pending = Buffer.alloc(0);
464
275
  let remaining = 0;
465
276
  let padding = 0;
@@ -505,7 +316,7 @@ async function extractFileFromTarGz(tgzPath, entryName, destPath) {
505
316
  pos += BLOCK;
506
317
  if (header.every((b) => b === 0)) continue;
507
318
  const size = readSize(header, 124);
508
- const type2 = String.fromCharCode(header[156] || 48);
319
+ const type = String.fromCharCode(header[156] || 48);
509
320
  let name = readString(header, 0, 100);
510
321
  if (readString(header, 257, 6) === "ustar") {
511
322
  const prefix = readString(header, 345, 155);
@@ -514,12 +325,12 @@ async function extractFileFromTarGz(tgzPath, entryName, destPath) {
514
325
  remaining = size;
515
326
  padding = (BLOCK - size % BLOCK) % BLOCK;
516
327
  meta = [];
517
- if (type2 === "x") kind = "pax";
518
- else if (type2 === "L") kind = "longname";
328
+ if (type === "x") kind = "pax";
329
+ else if (type === "L") kind = "longname";
519
330
  else {
520
331
  const path7 = overridePath ?? name;
521
332
  overridePath = void 0;
522
- if ((type2 === "0" || type2 === "\0" || type2 === "7") && path7 === entryName) {
333
+ if ((type === "0" || type === "\0" || type === "7") && path7 === entryName) {
523
334
  kind = "target";
524
335
  out = await import_promises.default.open(destPath, "w", 493);
525
336
  if (size === 0) {
@@ -609,10 +420,10 @@ function isMusl() {
609
420
  return report != null && report.header?.glibcVersionRuntime === void 0;
610
421
  }
611
422
  function claudeCodePlatformKey(info = { platform: process.platform, arch: process.arch, musl: isMusl() }) {
612
- const arch2 = info.arch === "arm64" || info.arch === "x64" ? info.arch : null;
613
- if (!arch2) return null;
614
- if (info.platform === "darwin" || info.platform === "win32") return `${info.platform}-${arch2}`;
615
- if (info.platform === "linux") return `linux-${arch2}${info.musl ? "-musl" : ""}`;
423
+ const arch = info.arch === "arm64" || info.arch === "x64" ? info.arch : null;
424
+ if (!arch) return null;
425
+ if (info.platform === "darwin" || info.platform === "win32") return `${info.platform}-${arch}`;
426
+ if (info.platform === "linux") return `linux-${arch}${info.musl ? "-musl" : ""}`;
616
427
  return null;
617
428
  }
618
429
  function resolveAsset(opts) {
@@ -630,11 +441,11 @@ function claudeCodeHome(opts = {}) {
630
441
  return opts.home ?? getAppPaths().claudeCodeDir;
631
442
  }
632
443
  function claudeCodeConfigDir(opts = {}) {
633
- return opts.home ? import_node_path3.default.join(opts.home, "config") : getAppPaths().claudeCodeConfigDir;
444
+ return opts.home ? import_node_path2.default.join(opts.home, "config") : getAppPaths().claudeCodeConfigDir;
634
445
  }
635
446
  function claudeCodeBinaryPath(opts = {}) {
636
447
  const { manifest, asset } = resolveAsset(opts);
637
- return import_node_path3.default.join(claudeCodeHome(opts), manifest.claudeCodeVersion, asset.bin);
448
+ return import_node_path2.default.join(claudeCodeHome(opts), manifest.claudeCodeVersion, asset.bin);
638
449
  }
639
450
  function resolveClaudeCodeBinary(opts = {}) {
640
451
  let binary;
@@ -644,7 +455,7 @@ function resolveClaudeCodeBinary(opts = {}) {
644
455
  return null;
645
456
  }
646
457
  try {
647
- return import_node_fs3.default.statSync(binary).isFile() ? binary : null;
458
+ return import_node_fs2.default.statSync(binary).isFile() ? binary : null;
648
459
  } catch {
649
460
  return null;
650
461
  }
@@ -664,12 +475,12 @@ async function ensureClaudeCodeBinary(opts = {}) {
664
475
  async function installClaudeCode(opts) {
665
476
  const { manifest, asset } = resolveAsset(opts);
666
477
  const home = claudeCodeHome(opts);
667
- const versionDir = import_node_path3.default.join(home, manifest.claudeCodeVersion);
668
- const binary = import_node_path3.default.join(versionDir, asset.bin);
478
+ const versionDir = import_node_path2.default.join(home, manifest.claudeCodeVersion);
479
+ const binary = import_node_path2.default.join(versionDir, asset.bin);
669
480
  await import_promises2.default.mkdir(home, { recursive: true });
670
- const tag = `${process.pid}-${import_node_crypto2.default.randomBytes(4).toString("hex")}`;
671
- const tarball = import_node_path3.default.join(home, `.download-${tag}.tgz`);
672
- const staging = import_node_path3.default.join(home, `.staging-${tag}`);
481
+ const tag = `${process.pid}-${import_node_crypto.default.randomBytes(4).toString("hex")}`;
482
+ const tarball = import_node_path2.default.join(home, `.download-${tag}.tgz`);
483
+ const staging = import_node_path2.default.join(home, `.staging-${tag}`);
673
484
  try {
674
485
  await download(asset, tarball, opts);
675
486
  opts.onProgress?.({ phase: "extract" });
@@ -677,14 +488,14 @@ async function installClaudeCode(opts) {
677
488
  const found = await extractFileFromTarGz(
678
489
  tarball,
679
490
  `package/${asset.bin}`,
680
- import_node_path3.default.join(staging, asset.bin)
491
+ import_node_path2.default.join(staging, asset.bin)
681
492
  );
682
493
  if (!found) throw new Error(`The Claude Code download has no ${asset.bin} binary.`);
683
- await import_promises2.default.chmod(import_node_path3.default.join(staging, asset.bin), 493);
494
+ await import_promises2.default.chmod(import_node_path2.default.join(staging, asset.bin), 493);
684
495
  try {
685
496
  await import_promises2.default.rename(staging, versionDir);
686
497
  } catch (err) {
687
- if (!import_node_fs3.default.existsSync(binary)) throw err;
498
+ if (!import_node_fs2.default.existsSync(binary)) throw err;
688
499
  }
689
500
  await removeOldVersions(home, manifest.claudeCodeVersion);
690
501
  return binary;
@@ -702,7 +513,7 @@ async function download(asset, dest, opts) {
702
513
  throw new Error(`Claude Code download failed (HTTP ${res.status}).`);
703
514
  }
704
515
  const totalBytes = Number(res.headers.get("content-length")) || void 0;
705
- const hash = import_node_crypto2.default.createHash("sha512");
516
+ const hash = import_node_crypto.default.createHash("sha512");
706
517
  const out = await import_promises2.default.open(dest, "w");
707
518
  let receivedBytes = 0;
708
519
  let lastReported = -1;
@@ -740,7 +551,7 @@ async function removeOldVersions(home, keep) {
740
551
  for (const entry of entries) {
741
552
  if (!entry.isDirectory() || entry.name === keep || entry.name === "config") continue;
742
553
  if (!/^\d+\.\d+\.\d+/.test(entry.name)) continue;
743
- await import_promises2.default.rm(import_node_path3.default.join(home, entry.name), { recursive: true, force: true }).catch(() => {
554
+ await import_promises2.default.rm(import_node_path2.default.join(home, entry.name), { recursive: true, force: true }).catch(() => {
744
555
  });
745
556
  }
746
557
  }
@@ -781,7 +592,7 @@ function runCommand(opts) {
781
592
  function runClaude(args, opts) {
782
593
  const { command, prefix } = runCommand(opts);
783
594
  return new Promise((resolve, reject) => {
784
- const child = (0, import_node_child_process2.spawn)(command, [...prefix, ...args], {
595
+ const child = (0, import_node_child_process.spawn)(command, [...prefix, ...args], {
785
596
  env: claudeCodeEnv(opts),
786
597
  stdio: ["ignore", "pipe", "pipe"],
787
598
  windowsHide: true,
@@ -841,9 +652,9 @@ async function loginClaudeCode(callbacks, opts = {}) {
841
652
  const { command, prefix } = runCommand(opts);
842
653
  const home = claudeCodeHome(opts);
843
654
  await import_promises2.default.mkdir(claudeCodeConfigDir(opts), { recursive: true });
844
- const tag = `${process.pid}-${import_node_crypto2.default.randomBytes(4).toString("hex")}`;
845
- const hookFile = import_node_path3.default.join(home, `.open-url-${tag}.cjs`);
846
- const urlFile = import_node_path3.default.join(home, `.open-url-${tag}.txt`);
655
+ const tag = `${process.pid}-${import_node_crypto.default.randomBytes(4).toString("hex")}`;
656
+ const hookFile = import_node_path2.default.join(home, `.open-url-${tag}.cjs`);
657
+ const urlFile = import_node_path2.default.join(home, `.open-url-${tag}.txt`);
847
658
  await import_promises2.default.writeFile(hookFile, OPEN_URL_HOOK);
848
659
  const env = {
849
660
  ...claudeCodeEnv(opts),
@@ -855,7 +666,7 @@ async function loginClaudeCode(callbacks, opts = {}) {
855
666
  let timer;
856
667
  try {
857
668
  await new Promise((resolve, reject) => {
858
- const child = (0, import_node_child_process2.spawn)(command, [...prefix, "auth", "login", "--claudeai"], {
669
+ const child = (0, import_node_child_process.spawn)(command, [...prefix, "auth", "login", "--claudeai"], {
859
670
  env,
860
671
  stdio: ["pipe", "pipe", "pipe"],
861
672
  windowsHide: true
@@ -890,7 +701,7 @@ async function loginClaudeCode(callbacks, opts = {}) {
890
701
  timer = setInterval(() => {
891
702
  let captured = "";
892
703
  try {
893
- captured = import_node_fs3.default.readFileSync(urlFile, "utf8");
704
+ captured = import_node_fs2.default.readFileSync(urlFile, "utf8");
894
705
  } catch {
895
706
  }
896
707
  const callbackUrl = captured ? claudeCodeCallbackUrl(manualUrl, captured) : null;
@@ -923,7 +734,9 @@ async function loginClaudeCode(callbacks, opts = {}) {
923
734
 
924
735
  // src/oauth/openai.ts
925
736
  var import_node_http = __toESM(require("http"), 1);
926
- var import_node_crypto3 = __toESM(require("crypto"), 1);
737
+ var import_node_crypto2 = __toESM(require("crypto"), 1);
738
+ var import_promises3 = __toESM(require("fs/promises"), 1);
739
+ var import_node_path3 = __toESM(require("path"), 1);
927
740
 
928
741
  // src/oauth/pkce.ts
929
742
  function base64urlEncode(bytes) {
@@ -943,791 +756,396 @@ async function generatePKCE() {
943
756
  return { verifier, challenge };
944
757
  }
945
758
 
759
+ // src/oauth/request-timeout.ts
760
+ var OAUTH_REQUEST_TIMEOUT_MS = 3e4;
761
+ function oauthRequestSignal() {
762
+ return AbortSignal.timeout(OAUTH_REQUEST_TIMEOUT_MS);
763
+ }
764
+
946
765
  // src/oauth/openai.ts
947
- var CLIENT_ID2 = "app_EMoamEEZ73f0CkXaXp7hrann";
948
- var AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
949
- var TOKEN_URL = "https://auth.openai.com/oauth/token";
950
- var REDIRECT_URI = "http://localhost:1455/auth/callback";
951
- var SCOPE = "openid profile email offline_access api.connectors.read api.connectors.invoke";
952
- var JWT_CLAIM_PATH = "https://api.openai.com/auth";
766
+ var ISSUER = "https://auth.openai.com";
767
+ var AUTHORIZE_URL = `${ISSUER}/api/accounts/authorize`;
768
+ var TOKEN_URL = `${ISSUER}/api/accounts/oauth/token`;
769
+ var REVOKE_URL = `${ISSUER}/api/accounts/oauth/revoke`;
770
+ var JWKS_URL = `${ISSUER}/.well-known/jwks.json`;
771
+ var REGISTRATION_CLIENT_ID = "dynamic_agent_client";
772
+ var AGENT_NAME = "GG Coder";
773
+ var RESOURCE = "https://api.openai.com/v1";
774
+ var CHATGPT_PLAN_SCOPE = "chatgpt.tokens.use.direct";
775
+ var SCOPES = `openid profile email offline_access resource.invoke ${CHATGPT_PLAN_SCOPE}`;
776
+ var CHATGPT_PLAN_BASE_URL = "https://api.openai.com/v1";
777
+ var CHATGPT_USAGE_URL = "https://chatgpt.com/settings/usage";
778
+ var CALLBACK_PATH = "/auth/callback";
779
+ var PREFERRED_PORT = 1455;
953
780
  var CALLBACK_TIMEOUT_MS = 12e4;
954
781
  var MAX_PASTE_ATTEMPTS = 3;
955
- var MIN_RAW_CODE_LENGTH = 10;
956
- async function loginOpenAI(callbacks) {
957
- const { verifier, challenge } = await generatePKCE();
958
- const state = import_node_crypto3.default.randomBytes(16).toString("hex");
959
- const url = new URL(AUTHORIZE_URL);
960
- url.searchParams.set("response_type", "code");
961
- url.searchParams.set("client_id", CLIENT_ID2);
962
- url.searchParams.set("redirect_uri", REDIRECT_URI);
963
- url.searchParams.set("scope", SCOPE);
964
- url.searchParams.set("code_challenge", challenge);
965
- url.searchParams.set("code_challenge_method", "S256");
966
- url.searchParams.set("state", state);
967
- url.searchParams.set("prompt", "login");
968
- url.searchParams.set("id_token_add_organizations", "true");
969
- url.searchParams.set("codex_cli_simplified_flow", "true");
970
- url.searchParams.set("originator", "ggcoder");
971
- const code = await acquireAuthorizationCode(url.toString(), state, callbacks);
972
- const creds = await exchangeOpenAICode(code, verifier);
973
- const accountId = getAccountId(creds.accessToken);
974
- if (!accountId) {
975
- throw new Error("Failed to extract accountId from OpenAI token.");
976
- }
977
- creds.accountId = accountId;
978
- return creds;
979
- }
980
- function parseAuthorizationInput(input) {
981
- const value = input.trim();
982
- if (!value) return {};
983
- try {
984
- const url = new URL(value);
985
- return {
986
- code: url.searchParams.get("code") ?? void 0,
987
- state: url.searchParams.get("state") ?? void 0
988
- };
989
- } catch {
990
- }
991
- if (value.includes("#")) {
992
- const [code, state] = value.split("#", 2);
993
- return { code, state };
994
- }
995
- if (value.includes("code=")) {
996
- const params = new URLSearchParams(value);
997
- return {
998
- code: params.get("code") ?? void 0,
999
- state: params.get("state") ?? void 0
1000
- };
1001
- }
1002
- if (/\s/.test(value) || value.length < MIN_RAW_CODE_LENGTH) return {};
1003
- return { code: value };
1004
- }
1005
- function decodeJwt(token) {
1006
- try {
1007
- const parts = token.split(".");
1008
- if (parts.length !== 3) return null;
1009
- const decoded = atob(parts[1]);
1010
- return JSON.parse(decoded);
1011
- } catch {
1012
- return null;
782
+ var ID_TOKEN_CLOCK_SKEW_S = 60;
783
+ var CHATGPT_PLAN_NOT_ENABLED_MESSAGE = "ChatGPT plan use isn't enabled for GG Coder. Allow Use your ChatGPT plan, or add an OpenAI API key.";
784
+ var ChatGPTPlanNotEnabledError = class extends Error {
785
+ constructor() {
786
+ super(CHATGPT_PLAN_NOT_ENABLED_MESSAGE);
787
+ this.name = "ChatGPTPlanNotEnabledError";
1013
788
  }
789
+ };
790
+ function defaultHostIdFile() {
791
+ return import_node_path3.default.join(getAppPaths().agentDir, "openai-host-id");
1014
792
  }
1015
- function getAccountId(accessToken) {
1016
- const payload = decodeJwt(accessToken);
1017
- const auth = payload?.[JWT_CLAIM_PATH];
1018
- const accountId = auth?.chatgpt_account_id;
1019
- return typeof accountId === "string" && accountId.length > 0 ? accountId : null;
793
+ function defaultRegistrationFile() {
794
+ return import_node_path3.default.join(getAppPaths().agentDir, "openai-chatgpt-client.json");
1020
795
  }
1021
- async function acquireAuthorizationCode(authUrl, expectedState, callbacks) {
1022
- callbacks.onOpenUrl(authUrl);
1023
- callbacks.onStatus("Waiting for browser callback...");
1024
- const done = new AbortController();
796
+ async function writePrivateFile(file, content) {
797
+ await import_promises3.default.mkdir(import_node_path3.default.dirname(file), { recursive: true });
798
+ const tmp = `${file}.${process.pid}.${import_node_crypto2.default.randomUUID().slice(0, 8)}.tmp`;
1025
799
  try {
1026
- return await new Promise((resolve, reject) => {
1027
- let serverFailed = false;
1028
- let pasteError;
1029
- const failIfExhausted = () => {
1030
- if (!serverFailed || pasteError === void 0) return;
1031
- reject(
1032
- pasteError instanceof Error ? pasteError : new Error("Could not obtain an authorization code.")
1033
- );
1034
- };
1035
- listenForCallback(expectedState, done.signal).then(resolve, () => {
1036
- serverFailed = true;
1037
- failIfExhausted();
1038
- });
1039
- promptForCode(expectedState, callbacks, done.signal).then(resolve, (err) => {
1040
- if (err instanceof FatalLoginError) {
1041
- reject(err);
1042
- return;
1043
- }
1044
- pasteError = err;
1045
- failIfExhausted();
1046
- });
800
+ await import_promises3.default.writeFile(tmp, content, { encoding: "utf-8", mode: 384 });
801
+ await import_promises3.default.rename(tmp, file);
802
+ } catch (err) {
803
+ await import_promises3.default.unlink(tmp).catch(() => {
1047
804
  });
1048
- } finally {
1049
- done.abort();
1050
- }
1051
- }
1052
- var FatalLoginError = class extends Error {
1053
- };
1054
- async function promptForCode(expectedState, callbacks, signal) {
1055
- let message = "If the browser is on another machine (SSH/headless), paste the callback URL or code here:";
1056
- for (let attempt = 0; attempt < MAX_PASTE_ATTEMPTS; attempt++) {
1057
- if (signal.aborted) throw new Error("Authorization code already received.");
1058
- const raw = await callbacks.onPromptCode(message, signal);
1059
- if (signal.aborted) throw new Error("Authorization code already received.");
1060
- const parsed = parseAuthorizationInput(raw);
1061
- if (parsed.code && parsed.state && parsed.state !== expectedState) {
1062
- throw new FatalLoginError("Authorization state mismatch \u2014 start the login again.");
1063
- }
1064
- if (parsed.code) return parsed.code;
1065
- message = "That didn't contain an authorization code. Paste the full callback URL:";
805
+ throw err;
1066
806
  }
1067
- throw new Error("No authorization code found in input.");
1068
807
  }
1069
- async function listenForCallback(expectedState, signal) {
1070
- return new Promise((resolve, reject) => {
1071
- let receivedCode = null;
1072
- const server = import_node_http.default.createServer((req, res) => {
1073
- const url = new URL(req.url || "", "http://localhost");
1074
- if (url.pathname !== "/auth/callback") {
1075
- res.statusCode = 404;
1076
- res.end("Not found");
1077
- return;
1078
- }
1079
- if (url.searchParams.get("state") !== expectedState) {
1080
- res.statusCode = 400;
1081
- res.end("State mismatch");
1082
- return;
1083
- }
1084
- receivedCode = url.searchParams.get("code");
1085
- res.writeHead(200, { "Content-Type": "text/html" });
1086
- res.end("<html><body><h1>Login successful!</h1><p>You can close this tab.</p></body></html>");
1087
- server.close();
1088
- });
1089
- server.on("error", (err) => {
1090
- reject(err);
1091
- });
1092
- server.listen(1455, "127.0.0.1");
1093
- const timeout = setTimeout(() => {
1094
- if (!receivedCode) server.close();
1095
- }, CALLBACK_TIMEOUT_MS);
1096
- timeout.unref();
1097
- const onAbort = () => {
1098
- server.close();
1099
- };
1100
- signal.addEventListener("abort", onAbort, { once: true });
1101
- server.on("close", () => {
1102
- clearTimeout(timeout);
1103
- signal.removeEventListener("abort", onAbort);
1104
- if (receivedCode) {
1105
- resolve(receivedCode);
1106
- } else {
1107
- reject(new Error("Server closed without receiving code"));
1108
- }
1109
- });
1110
- });
1111
- }
1112
- async function exchangeOpenAICode(code, verifier) {
1113
- const response = await fetch(TOKEN_URL, {
1114
- method: "POST",
1115
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
1116
- body: new URLSearchParams({
1117
- grant_type: "authorization_code",
1118
- client_id: CLIENT_ID2,
1119
- code,
1120
- redirect_uri: REDIRECT_URI,
1121
- code_verifier: verifier
1122
- }),
1123
- signal: oauthRequestSignal()
1124
- });
1125
- if (!response.ok) {
1126
- const text = await response.text();
1127
- throw new Error(`OpenAI token exchange failed (${response.status}): ${text}`);
1128
- }
1129
- const data = await response.json();
1130
- return {
1131
- accessToken: data.access_token,
1132
- refreshToken: data.refresh_token,
1133
- expiresAt: Date.now() + data.expires_in * 1e3
1134
- };
808
+ var HOST_ID_PATTERN = /^urn:uuid:[0-9a-f-]{36}$/i;
809
+ async function getChatGPTHostId(file = defaultHostIdFile()) {
810
+ const existing = await import_promises3.default.readFile(file, "utf-8").catch(() => "");
811
+ if (HOST_ID_PATTERN.test(existing.trim())) return existing.trim();
812
+ await writePrivateFile(file, `urn:uuid:${import_node_crypto2.default.randomUUID()}`);
813
+ return (await import_promises3.default.readFile(file, "utf-8")).trim();
1135
814
  }
1136
- async function refreshOpenAIToken(refreshToken) {
1137
- const response = await fetch(TOKEN_URL, {
1138
- method: "POST",
1139
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
1140
- body: new URLSearchParams({
1141
- grant_type: "refresh_token",
1142
- refresh_token: refreshToken,
1143
- client_id: CLIENT_ID2
1144
- }),
1145
- signal: oauthRequestSignal()
1146
- });
1147
- if (!response.ok) {
1148
- const text = await response.text();
1149
- throw new Error(`OpenAI token refresh failed (${response.status}): ${text}`);
1150
- }
1151
- const data = await response.json();
1152
- const creds = {
1153
- accessToken: data.access_token,
1154
- refreshToken: data.refresh_token,
1155
- expiresAt: Date.now() + data.expires_in * 1e3
1156
- };
1157
- const accountId = getAccountId(creds.accessToken);
1158
- if (accountId) {
1159
- creds.accountId = accountId;
815
+ async function readRegistration(file) {
816
+ try {
817
+ const value = JSON.parse(await import_promises3.default.readFile(file, "utf-8"));
818
+ if (typeof value.clientId === "string" && typeof value.subject === "string") {
819
+ return { clientId: value.clientId, subject: value.subject };
820
+ }
821
+ } catch {
1160
822
  }
1161
- return creds;
823
+ return void 0;
1162
824
  }
1163
-
1164
- // src/oauth/gemini.ts
1165
- var import_node_http2 = __toESM(require("http"), 1);
1166
- var import_node_crypto4 = __toESM(require("crypto"), 1);
1167
- var CLIENT_ID_ENV = "GGCODER_GEMINI_OAUTH_CLIENT_ID";
1168
- var CLIENT_SECRET_ENV = "GGCODER_GEMINI_OAUTH_CLIENT_SECRET";
1169
- var DEFAULT_CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com";
1170
- var DEFAULT_CLIENT_SECRET = "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";
1171
- var AUTHORIZE_URL2 = "https://accounts.google.com/o/oauth2/v2/auth";
1172
- var TOKEN_URL2 = "https://oauth2.googleapis.com/token";
1173
- var CODE_ASSIST_BASE_URL = "https://cloudcode-pa.googleapis.com";
1174
- var CODE_ASSIST_API_VERSION = "v1internal";
1175
- var CODE_ASSIST_POST_RETRIES = 3;
1176
- var CODE_ASSIST_POST_RETRY_DELAY_MS = 100;
1177
- var SCOPE2 = [
1178
- "https://www.googleapis.com/auth/cloud-platform",
1179
- "https://www.googleapis.com/auth/userinfo.email",
1180
- "https://www.googleapis.com/auth/userinfo.profile"
1181
- ].join(" ");
1182
- var USER_TIER_FREE = "free-tier";
1183
- var USER_TIER_LEGACY = "legacy-tier";
1184
- var USER_TIER_STANDARD = "standard-tier";
1185
- var VALIDATION_REQUIRED_REASON = "VALIDATION_REQUIRED";
1186
- var VPC_SC_REASON = "SECURITY_POLICY_VIOLATED";
1187
- var CodeAssistHttpError = class extends Error {
1188
- status;
1189
- body;
1190
- constructor(label, status, body) {
1191
- super(`Gemini Code Assist ${label} failed (${status}): ${body}`);
1192
- this.name = "CodeAssistHttpError";
1193
- this.status = status;
1194
- this.body = body;
1195
- }
1196
- };
1197
- async function loginGemini(callbacks) {
1198
- const { clientId: clientId2, clientSecret } = getGeminiOAuthClientCredentials();
825
+ async function loginChatGPT(callbacks, options = {}) {
826
+ const hostId = await getChatGPTHostId(options.hostIdFile);
827
+ const registrationFile = options.registrationFile ?? defaultRegistrationFile();
828
+ const registration = await readRegistration(registrationFile);
1199
829
  const { verifier, challenge } = await generatePKCE();
1200
- const state = import_node_crypto4.default.randomBytes(32).toString("hex");
1201
- const redirectUri = await getLoopbackRedirectUri();
1202
- const url = new URL(AUTHORIZE_URL2);
1203
- url.searchParams.set("response_type", "code");
1204
- url.searchParams.set("client_id", clientId2);
1205
- url.searchParams.set("redirect_uri", redirectUri);
1206
- url.searchParams.set("scope", SCOPE2);
1207
- url.searchParams.set("access_type", "offline");
1208
- url.searchParams.set("prompt", "consent");
1209
- url.searchParams.set("code_challenge", challenge);
1210
- url.searchParams.set("code_challenge_method", "S256");
1211
- url.searchParams.set("state", state);
1212
- let code;
830
+ const state = import_node_crypto2.default.randomBytes(16).toString("hex");
831
+ const nonce = import_node_crypto2.default.randomBytes(16).toString("hex");
832
+ const listener = await startCallbackListener(state, Boolean(registration));
1213
833
  try {
1214
- code = await loginWithServer(url.toString(), redirectUri, state, callbacks);
1215
- } catch {
1216
- callbacks.onOpenUrl(url.toString());
1217
- const raw = await callbacks.onPromptCode(
1218
- "Could not start local server. Paste the callback URL or code from the browser:"
1219
- );
1220
- const parsed = parseAuthorizationInput2(raw);
1221
- if (!parsed.code) {
1222
- throw new Error("No authorization code found in input.");
834
+ const redirectUri = `http://127.0.0.1:${listener.port}${CALLBACK_PATH}`;
835
+ const url = new URL(AUTHORIZE_URL);
836
+ url.searchParams.set("client_id", registration?.clientId ?? REGISTRATION_CLIENT_ID);
837
+ if (!registration) url.searchParams.set("agent_name_hint", AGENT_NAME);
838
+ url.searchParams.set("ext_agent_host_id", hostId);
839
+ if (registration && options.previous?.idToken) {
840
+ url.searchParams.set("id_token_hint", options.previous.idToken);
1223
841
  }
1224
- if (parsed.state && parsed.state !== state) {
1225
- throw new Error("Invalid state. Please try again.");
842
+ url.searchParams.set("response_type", "code");
843
+ url.searchParams.set("redirect_uri", redirectUri);
844
+ url.searchParams.set("scope", SCOPES);
845
+ url.searchParams.set("resource", RESOURCE);
846
+ url.searchParams.set("state", state);
847
+ url.searchParams.set("nonce", nonce);
848
+ url.searchParams.set("code_challenge_method", "S256");
849
+ url.searchParams.set("code_challenge", challenge);
850
+ const callback = await acquireCallback(url.toString(), state, listener, callbacks);
851
+ if (callback.get("error")) {
852
+ if (callback.get("error") === "access_denied") throw new ChatGPTPlanNotEnabledError();
853
+ throw new Error(
854
+ `ChatGPT sign-in failed: ${callback.get("error_description") ?? callback.get("error")}`
855
+ );
1226
856
  }
1227
- code = parsed.code;
1228
- }
1229
- const creds = await exchangeGeminiCode(code, verifier, redirectUri, clientId2, clientSecret);
1230
- callbacks.onStatus("Setting up Gemini Code Assist access...");
1231
- const projectId = await setupCodeAssistProject(creds.accessToken, callbacks);
1232
- return {
1233
- ...creds,
1234
- projectId
1235
- };
1236
- }
1237
- async function refreshGeminiToken(refreshToken) {
1238
- const { clientId: clientId2, clientSecret } = getGeminiOAuthClientCredentials();
1239
- const data = await postTokenRequest({
1240
- grant_type: "refresh_token",
1241
- refresh_token: refreshToken,
1242
- client_id: clientId2,
1243
- client_secret: clientSecret
1244
- });
1245
- return {
1246
- accessToken: data.access_token,
1247
- refreshToken: data.refresh_token ?? refreshToken,
1248
- expiresAt: Date.now() + data.expires_in * 1e3 - 5 * 60 * 1e3
1249
- };
1250
- }
1251
- function getGeminiOAuthClientCredentials() {
1252
- const clientId2 = process.env[CLIENT_ID_ENV]?.trim() || DEFAULT_CLIENT_ID;
1253
- const clientSecret = process.env[CLIENT_SECRET_ENV]?.trim() || DEFAULT_CLIENT_SECRET;
1254
- return { clientId: clientId2, clientSecret };
1255
- }
1256
- async function getLoopbackRedirectUri() {
1257
- return new Promise((resolve, reject) => {
1258
- const server = import_node_http2.default.createServer();
1259
- server.listen(0, "127.0.0.1", () => {
1260
- const addr = server.address();
1261
- server.close(() => {
1262
- if (addr && typeof addr === "object") {
1263
- resolve(`http://127.0.0.1:${addr.port}/oauth2callback`);
1264
- } else {
1265
- reject(new Error("Failed to allocate OAuth callback port."));
1266
- }
857
+ const code = callback.get("code");
858
+ if (!code) throw new Error("ChatGPT sign-in returned no authorization code.");
859
+ const issued = callback.get("client_id") ?? void 0;
860
+ if (registration && issued && issued !== registration.clientId) {
861
+ throw new Error("ChatGPT returned a different app registration. Start the sign-in again.");
862
+ }
863
+ const clientId = registration?.clientId ?? issued;
864
+ if (!clientId || clientId === REGISTRATION_CLIENT_ID) {
865
+ throw new Error(
866
+ "ChatGPT didn't finish registering GG Coder. Start the sign-in again, and paste the full callback URL if you're on another machine."
867
+ );
868
+ }
869
+ const tokens = await exchangeCode({ code, verifier, clientId, redirectUri });
870
+ const claims = await verifyIdToken(tokens.idToken, { clientId, nonce });
871
+ const subject = String(claims.sub ?? "");
872
+ if (!subject) throw new Error("ChatGPT ID token has no subject.");
873
+ if (registration && registration.subject !== subject) {
874
+ await import_promises3.default.unlink(registrationFile).catch(() => {
1267
875
  });
1268
- });
1269
- server.on("error", reject);
1270
- });
1271
- }
1272
- function parseAuthorizationInput2(input) {
1273
- const value = input.trim();
1274
- if (!value) return {};
1275
- try {
1276
- const url = new URL(value);
1277
- return {
1278
- code: url.searchParams.get("code") ?? void 0,
1279
- state: url.searchParams.get("state") ?? void 0
1280
- };
1281
- } catch {
1282
- }
1283
- if (value.includes("code=")) {
1284
- const params = new URLSearchParams(value);
876
+ await revokeChatGPTSignIn({ refreshToken: tokens.refreshToken, clientId });
877
+ throw new Error(
878
+ "That was a different ChatGPT account. Continue with ChatGPT again to connect it."
879
+ );
880
+ }
881
+ if (!tokens.scopes.includes(CHATGPT_PLAN_SCOPE)) {
882
+ await revokeChatGPTSignIn({ refreshToken: tokens.refreshToken, clientId });
883
+ throw new ChatGPTPlanNotEnabledError();
884
+ }
885
+ if (!registration) {
886
+ await writePrivateFile(registrationFile, JSON.stringify({ clientId, subject }));
887
+ }
1285
888
  return {
1286
- code: params.get("code") ?? void 0,
1287
- state: params.get("state") ?? void 0
889
+ accessToken: tokens.accessToken,
890
+ refreshToken: tokens.refreshToken,
891
+ expiresAt: Date.now() + tokens.expiresIn * 1e3,
892
+ expiresIn: tokens.expiresIn,
893
+ accountId: subject,
894
+ clientId,
895
+ idToken: tokens.idToken,
896
+ scopes: tokens.scopes,
897
+ baseUrl: CHATGPT_PLAN_BASE_URL
1288
898
  };
899
+ } finally {
900
+ listener.close();
1289
901
  }
1290
- return { code: value };
1291
902
  }
1292
- async function loginWithServer(authUrl, redirectUri, expectedState, callbacks) {
1293
- const redirect = new URL(redirectUri);
1294
- const port = Number(redirect.port);
1295
- return new Promise((resolve, reject) => {
1296
- let receivedCode = null;
1297
- const server = import_node_http2.default.createServer((req, res) => {
1298
- const url = new URL(req.url || "", redirect.origin);
1299
- if (url.pathname !== redirect.pathname) {
1300
- res.statusCode = 404;
1301
- res.end("Not found");
1302
- return;
1303
- }
1304
- if (url.searchParams.get("state") !== expectedState) {
1305
- res.statusCode = 400;
1306
- res.end("State mismatch");
1307
- return;
1308
- }
1309
- receivedCode = url.searchParams.get("code");
1310
- res.writeHead(200, { "Content-Type": "text/html", Connection: "close" });
1311
- res.end("<html><body><h1>Login successful!</h1><p>You can close this tab.</p></body></html>");
1312
- server.close();
1313
- });
1314
- server.on("error", (err) => reject(err));
1315
- server.listen(port, "127.0.0.1", () => {
1316
- callbacks.onOpenUrl(authUrl);
1317
- callbacks.onStatus("Waiting for browser callback...");
1318
- });
1319
- const timeout = setTimeout(() => {
1320
- if (!receivedCode) server.close();
1321
- }, 12e4);
1322
- timeout.unref();
1323
- server.on("close", () => {
1324
- clearTimeout(timeout);
1325
- if (receivedCode) {
1326
- resolve(receivedCode);
1327
- } else {
1328
- reject(new Error("Server closed without receiving code."));
1329
- }
1330
- });
903
+ async function fetchChatGPTPlanModels(accessToken, fetchFn = fetch) {
904
+ const response = await fetchFn(`${CHATGPT_PLAN_BASE_URL}/models`, {
905
+ headers: { Authorization: `Bearer ${accessToken}` },
906
+ signal: oauthRequestSignal()
1331
907
  });
908
+ if (!response.ok) throw new Error(`ChatGPT model list failed (${response.status})`);
909
+ const data = await response.json();
910
+ return (data.models ?? []).filter((m) => typeof m.slug === "string" && (m.visibility ?? "list") === "list").map((m) => m.slug);
1332
911
  }
1333
- async function exchangeGeminiCode(code, verifier, redirectUri, clientId2, clientSecret) {
1334
- const data = await postTokenRequest({
1335
- grant_type: "authorization_code",
1336
- client_id: clientId2,
1337
- client_secret: clientSecret,
1338
- code,
1339
- redirect_uri: redirectUri,
1340
- code_verifier: verifier
912
+ async function startCallbackListener(expectedState, anyPort) {
913
+ let settle;
914
+ const result = new Promise((resolve, reject) => {
915
+ settle = { resolve, reject };
1341
916
  });
1342
- if (!data.refresh_token) {
1343
- throw new Error("Gemini OAuth did not return a refresh token. Please try login again.");
1344
- }
1345
- return {
1346
- accessToken: data.access_token,
1347
- refreshToken: data.refresh_token,
1348
- expiresAt: Date.now() + data.expires_in * 1e3 - 5 * 60 * 1e3
1349
- };
1350
- }
1351
- async function postTokenRequest(body) {
1352
- const response = await fetch(TOKEN_URL2, {
1353
- method: "POST",
1354
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
1355
- body: new URLSearchParams(body),
1356
- signal: oauthRequestSignal()
917
+ result.catch(() => {
1357
918
  });
1358
- if (!response.ok) {
1359
- const text = await response.text();
1360
- throw new Error(`Gemini token request failed (${response.status}): ${text}`);
1361
- }
1362
- return await response.json();
1363
- }
1364
- async function setupCodeAssistProject(accessToken, callbacks) {
1365
- const envProject = process.env.GOOGLE_CLOUD_PROJECT ?? process.env.GOOGLE_CLOUD_PROJECT_ID;
1366
- if (envProject && /^\d+$/.test(envProject)) {
1367
- throw new Error("GOOGLE_CLOUD_PROJECT must be a project ID, not a numeric project number.");
1368
- }
1369
- const coreMetadata = {
1370
- ideType: "IDE_UNSPECIFIED",
1371
- platform: "PLATFORM_UNSPECIFIED",
1372
- pluginType: "GEMINI"
1373
- };
1374
- const projectMetadata = {
1375
- ...coreMetadata,
1376
- ...envProject ? { duetProject: envProject } : {}
1377
- };
1378
- let loadRes;
1379
- while (true) {
1380
- loadRes = await loadCodeAssist(accessToken, envProject, projectMetadata);
1381
- const validation = getValidationRequiredTier(loadRes);
1382
- if (!validation) break;
1383
- callbacks.onStatus(
1384
- `Gemini Code Assist requires account validation${validation.reasonMessage ? `: ${validation.reasonMessage}` : ""}`
1385
- );
1386
- callbacks.onOpenUrl(validation.validationUrl);
1387
- const answer = await callbacks.onPromptCode(
1388
- "Complete validation in the browser, then press Enter to retry (or type cancel):"
919
+ const server = import_node_http.default.createServer((req, res) => {
920
+ const url = new URL(req.url || "", "http://127.0.0.1");
921
+ if (url.pathname !== CALLBACK_PATH) {
922
+ res.statusCode = 404;
923
+ res.end("Not found");
924
+ return;
925
+ }
926
+ if (url.searchParams.get("state") !== expectedState) {
927
+ res.statusCode = 400;
928
+ res.end("State mismatch");
929
+ return;
930
+ }
931
+ res.writeHead(200, { "Content-Type": "text/html" });
932
+ res.end(
933
+ "<html><body><h1>You're signed in to GG Coder.</h1><p>You can close this tab.</p></body></html>"
1389
934
  );
1390
- if (answer.trim().toLowerCase() === "cancel") {
1391
- throw new Error("Gemini Code Assist account validation was cancelled.");
935
+ settle?.resolve(url.searchParams);
936
+ server.close();
937
+ });
938
+ const listen = (port2) => new Promise((resolve, reject) => {
939
+ const onError = (err) => reject(err);
940
+ server.once("error", onError);
941
+ server.listen(port2, "127.0.0.1", () => {
942
+ server.off("error", onError);
943
+ resolve(server.address().port);
944
+ });
945
+ });
946
+ let port;
947
+ for (const candidate of anyPort ? [PREFERRED_PORT, 0] : [PREFERRED_PORT]) {
948
+ try {
949
+ port = await listen(candidate);
950
+ break;
951
+ } catch {
1392
952
  }
1393
953
  }
1394
- if (loadRes.currentTier) {
1395
- const project2 = loadRes.cloudaicompanionProject ?? envProject;
1396
- if (!project2) throwProjectError(loadRes);
1397
- return project2;
1398
- }
1399
- const tier = getOnboardTier(loadRes);
1400
- const onboardReq = tier.id === USER_TIER_FREE ? {
1401
- tierId: tier.id,
1402
- cloudaicompanionProject: void 0,
1403
- metadata: coreMetadata
1404
- } : {
1405
- tierId: tier.id,
1406
- cloudaicompanionProject: envProject,
1407
- metadata: projectMetadata
1408
- };
1409
- let operation = await codeAssistPost(
1410
- accessToken,
1411
- "onboardUser",
1412
- onboardReq
1413
- );
1414
- while (!operation.done && operation.name) {
1415
- await new Promise((resolve) => setTimeout(resolve, 5e3));
1416
- operation = await codeAssistGet(accessToken, operation.name);
1417
- }
1418
- const project = operation.response?.cloudaicompanionProject?.id ?? envProject;
1419
- if (!project) throwProjectError(loadRes);
1420
- return project;
954
+ if (port === void 0) {
955
+ settle?.reject(new Error("Could not start the local sign-in listener."));
956
+ return { port: PREFERRED_PORT, result, close: () => {
957
+ } };
958
+ }
959
+ const timeout = setTimeout(() => server.close(), CALLBACK_TIMEOUT_MS);
960
+ timeout.unref();
961
+ server.on("close", () => {
962
+ clearTimeout(timeout);
963
+ settle?.reject(new Error("Sign-in listener closed without a callback."));
964
+ });
965
+ return { port, result, close: () => server.close() };
1421
966
  }
1422
- async function loadCodeAssist(accessToken, envProject, metadata) {
967
+ async function acquireCallback(authUrl, expectedState, listener, callbacks) {
968
+ callbacks.onOpenUrl(authUrl);
969
+ callbacks.onStatus("Waiting for ChatGPT sign-in in your browser...");
970
+ const done = new AbortController();
1423
971
  try {
1424
- return await codeAssistPost(accessToken, "loadCodeAssist", {
1425
- ...envProject ? { cloudaicompanionProject: envProject } : {},
1426
- metadata
972
+ return await new Promise((resolve, reject) => {
973
+ let listenerFailed = false;
974
+ let pasteError;
975
+ const failIfExhausted = () => {
976
+ if (!listenerFailed || pasteError === void 0) return;
977
+ reject(
978
+ pasteError instanceof Error ? pasteError : new Error("Could not complete the sign-in.")
979
+ );
980
+ };
981
+ listener.result.then(resolve, () => {
982
+ listenerFailed = true;
983
+ failIfExhausted();
984
+ });
985
+ promptForCallback(expectedState, callbacks, done.signal).then(resolve, (err) => {
986
+ if (err instanceof FatalLoginError) {
987
+ reject(err);
988
+ return;
989
+ }
990
+ pasteError = err;
991
+ failIfExhausted();
992
+ });
1427
993
  });
1428
- } catch (err) {
1429
- if (err instanceof CodeAssistHttpError && isVpcScAffectedError(err)) {
1430
- return { currentTier: { id: USER_TIER_STANDARD } };
1431
- }
1432
- if (err instanceof CodeAssistHttpError && err.status === 403 && envProject === "cloudshell-gca") {
1433
- throw new Error(
1434
- "Access to the default Cloud Shell Gemini project was denied.\nPlease set your own Google Cloud project by running:\ngcloud config set project [PROJECT_ID]\nor setting export GOOGLE_CLOUD_PROJECT=...",
1435
- { cause: err }
1436
- );
1437
- }
1438
- throw err;
994
+ } finally {
995
+ done.abort();
996
+ listener.close();
1439
997
  }
1440
998
  }
1441
- function getValidationRequiredTier(response) {
1442
- return response.ineligibleTiers?.find(
1443
- (tier) => tier.reasonCode === VALIDATION_REQUIRED_REASON && typeof tier.validationUrl === "string"
1444
- );
1445
- }
1446
- function getOnboardTier(response) {
1447
- const defaultTier = response.allowedTiers?.find((tier) => tier.isDefault);
1448
- return defaultTier ?? { id: USER_TIER_LEGACY, name: "" };
1449
- }
1450
- function throwProjectError(response) {
1451
- const reasons = response.ineligibleTiers?.map((tier) => tier.reasonMessage ?? tier.tierName).filter((reason) => Boolean(reason));
1452
- if (reasons && reasons.length > 0) {
1453
- throw new Error(`Gemini Code Assist setup failed: ${reasons.join(", ")}`);
999
+ var FatalLoginError = class extends Error {
1000
+ };
1001
+ function parseCallbackInput(input) {
1002
+ const value = input.trim();
1003
+ if (!value) return void 0;
1004
+ try {
1005
+ return new URL(value).searchParams;
1006
+ } catch {
1454
1007
  }
1455
- throw new Error(
1456
- "Gemini requires a Google Cloud project for this account. Set GOOGLE_CLOUD_PROJECT and try again."
1457
- );
1008
+ if (/[?&]?(code|error)=/.test(value)) {
1009
+ return new URLSearchParams(value.replace(/^.*\?/, ""));
1010
+ }
1011
+ return void 0;
1458
1012
  }
1459
- async function codeAssistPost(accessToken, method, body) {
1460
- let lastError;
1461
- for (let attempt = 0; attempt <= CODE_ASSIST_POST_RETRIES; attempt++) {
1462
- try {
1463
- return await codeAssistRequest(getCodeAssistMethodUrl(method), accessToken, method, {
1464
- method: "POST",
1465
- body: JSON.stringify(body)
1466
- });
1467
- } catch (err) {
1468
- if (!(err instanceof CodeAssistHttpError) || attempt === CODE_ASSIST_POST_RETRIES || !shouldRetryCodeAssistStatus(err.status)) {
1469
- throw err;
1013
+ async function promptForCallback(expectedState, callbacks, signal) {
1014
+ let message = "If the browser is on another machine (SSH/headless), paste the full callback URL here:";
1015
+ for (let attempt = 0; attempt < MAX_PASTE_ATTEMPTS; attempt++) {
1016
+ if (signal.aborted) throw new Error("Callback already received.");
1017
+ const raw = await callbacks.onPromptCode(message, signal);
1018
+ if (signal.aborted) throw new Error("Callback already received.");
1019
+ const params = parseCallbackInput(raw);
1020
+ if (params && (params.get("code") || params.get("error"))) {
1021
+ if (params.get("state") !== expectedState) {
1022
+ throw new FatalLoginError("Sign-in state mismatch \u2014 start the sign-in again.");
1470
1023
  }
1471
- lastError = err;
1024
+ return params;
1472
1025
  }
1473
- await new Promise((resolve) => setTimeout(resolve, CODE_ASSIST_POST_RETRY_DELAY_MS));
1026
+ message = "That wasn't a callback URL. Paste the full URL from the browser's address bar:";
1474
1027
  }
1475
- throw lastError ?? new Error(`Gemini Code Assist ${method} failed.`);
1028
+ throw new Error("No callback URL found in input.");
1476
1029
  }
1477
- async function codeAssistGet(accessToken, operationName) {
1478
- return codeAssistRequest(getCodeAssistOperationUrl(operationName), accessToken, "operation", {
1479
- method: "GET"
1030
+ async function postForm(url, form) {
1031
+ return fetch(url, {
1032
+ method: "POST",
1033
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1034
+ body: new URLSearchParams(form),
1035
+ signal: oauthRequestSignal()
1480
1036
  });
1481
1037
  }
1482
- async function codeAssistRequest(url, accessToken, label, init) {
1483
- const response = await fetch(url, {
1484
- ...init,
1485
- headers: codeAssistHeaders(accessToken),
1486
- signal: oauthRequestSignal()
1038
+ async function exchangeCode(input) {
1039
+ const response = await postForm(TOKEN_URL, {
1040
+ grant_type: "authorization_code",
1041
+ client_id: input.clientId,
1042
+ code: input.code,
1043
+ code_verifier: input.verifier,
1044
+ redirect_uri: input.redirectUri,
1045
+ resource: RESOURCE
1487
1046
  });
1488
1047
  if (!response.ok) {
1489
- const text = await response.text();
1490
- throw new CodeAssistHttpError(label, response.status, text);
1048
+ const text = await response.text().catch(() => "");
1049
+ throw new Error(`ChatGPT token exchange failed (${response.status}): ${text.slice(0, 300)}`);
1491
1050
  }
1492
- return await response.json();
1493
- }
1494
- function getCodeAssistBaseUrl() {
1495
- const endpoint = process.env.CODE_ASSIST_ENDPOINT ?? CODE_ASSIST_BASE_URL;
1496
- const version = process.env.CODE_ASSIST_API_VERSION || CODE_ASSIST_API_VERSION;
1497
- return `${endpoint}/${version}`;
1498
- }
1499
- function getCodeAssistMethodUrl(method) {
1500
- return `${getCodeAssistBaseUrl()}:${method}`;
1501
- }
1502
- function getCodeAssistOperationUrl(operationName) {
1503
- return `${getCodeAssistBaseUrl()}/${operationName}`;
1504
- }
1505
- function shouldRetryCodeAssistStatus(status) {
1506
- return status === 429 || status === 499 || status >= 500 && status <= 599;
1507
- }
1508
- function isVpcScAffectedError(error) {
1509
- try {
1510
- const parsed = JSON.parse(error.body);
1511
- if (!parsed || typeof parsed !== "object" || !("error" in parsed)) return false;
1512
- const details = parsed.error?.details;
1513
- return Array.isArray(details) ? details.some(
1514
- (detail) => detail != null && typeof detail === "object" && "reason" in detail && detail.reason === VPC_SC_REASON
1515
- ) : false;
1516
- } catch {
1517
- return false;
1051
+ const data = await response.json();
1052
+ if (!data.access_token || !data.refresh_token || !data.id_token) {
1053
+ throw new Error("ChatGPT token response is missing tokens.");
1518
1054
  }
1519
- }
1520
- function codeAssistHeaders(accessToken) {
1521
- return {
1522
- Authorization: `Bearer ${accessToken}`,
1523
- "Content-Type": "application/json",
1524
- "User-Agent": "google-gemini-cli",
1525
- "X-Goog-Api-Client": "gemini-cli/0.0.0"
1526
- };
1527
- }
1528
-
1529
- // src/oauth/xai.ts
1530
- var DEFAULT_CLIENT_ID2 = "b1a00492-073a-47ea-816f-4c329264a828";
1531
- var DEFAULT_ISSUER = "https://auth.x.ai";
1532
- var DEFAULT_CLI_BASE_URL = "https://cli-chat-proxy.grok.com/v1";
1533
- var OAUTH_SCOPE = "openid profile email offline_access grok-cli:access api:access";
1534
- var DEFAULT_GROK_CLI_VERSION = "1.0.67";
1535
- var DEVICE_TIMEOUT_FALLBACK_MS = 10 * 60 * 1e3;
1536
- var DEFAULT_TOKEN_LIFETIME_SECONDS = 3600;
1537
- function issuer() {
1538
- return (process.env.XAI_OAUTH_ISSUER ?? DEFAULT_ISSUER).replace(/\/+$/, "");
1539
- }
1540
- function clientId() {
1541
- return process.env.XAI_OAUTH_CLIENT_ID ?? DEFAULT_CLIENT_ID2;
1542
- }
1543
- function grokCliBaseUrl() {
1544
- return (process.env.XAI_CLI_BASE_URL ?? DEFAULT_CLI_BASE_URL).replace(/\/+$/, "");
1545
- }
1546
- function grokCliVersion() {
1547
- const raw = process.env.GROK_CLI_VERSION ?? DEFAULT_GROK_CLI_VERSION;
1548
- const cleaned = raw.replace(/[^\u0020-\u007E]/g, "").trim();
1549
- return cleaned.length > 0 ? cleaned : DEFAULT_GROK_CLI_VERSION;
1550
- }
1551
- function grokCliHeaders(modelId) {
1552
1055
  return {
1553
- "X-XAI-Token-Auth": "xai-grok-cli",
1554
- "x-grok-client-version": grokCliVersion(),
1555
- "x-grok-client-identifier": "ggcoder",
1556
- ...modelId ? { "x-grok-model-override": modelId } : {}
1056
+ accessToken: data.access_token,
1057
+ refreshToken: data.refresh_token,
1058
+ idToken: data.id_token,
1059
+ expiresIn: data.expires_in ?? 3600,
1060
+ scopes: (data.scope ?? "").split(/\s+/).filter(Boolean)
1557
1061
  };
1558
1062
  }
1559
- function isGrokCliEndpoint(baseUrl) {
1560
- if (typeof baseUrl !== "string" || baseUrl.length === 0) return false;
1561
- const normalized = baseUrl.replace(/\/+$/, "");
1562
- return normalized === grokCliBaseUrl() || /(^|\.)grok\.com/i.test(normalized);
1563
- }
1564
- async function postForm2(endpoint, params) {
1565
- const response = await fetch(`${issuer()}${endpoint}`, {
1566
- method: "POST",
1567
- headers: {
1568
- "Content-Type": "application/x-www-form-urlencoded",
1569
- Accept: "application/json",
1570
- "User-Agent": `grok-cli/${grokCliVersion()}`
1571
- },
1572
- body: new URLSearchParams(params).toString(),
1573
- signal: oauthRequestSignal()
1574
- });
1575
- let data = {};
1576
- try {
1577
- const parsed = await response.json();
1578
- if (parsed && typeof parsed === "object") data = parsed;
1579
- } catch {
1580
- }
1581
- return { status: response.status, data };
1582
- }
1583
- function errorDetail2(data) {
1584
- const desc = data.error_description ?? data.message ?? data.error;
1585
- return typeof desc === "string" && desc.length > 0 ? desc : "unknown error";
1586
- }
1587
- function jwtExpirySeconds(token) {
1588
- const payload = token.split(".")[1];
1589
- if (!payload) return void 0;
1590
- try {
1591
- const json = Buffer.from(payload.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString(
1592
- "utf-8"
1593
- );
1594
- const claims = JSON.parse(json);
1595
- if (!claims || typeof claims !== "object") return void 0;
1596
- const exp = claims.exp;
1597
- return typeof exp === "number" && Number.isFinite(exp) && exp > 0 ? exp : void 0;
1598
- } catch {
1599
- return void 0;
1600
- }
1601
- }
1602
- function credsFromTokenResponse2(data, opts) {
1603
- const accessToken = data.access_token;
1604
- const responseRefreshToken = data.refresh_token;
1605
- if (typeof accessToken !== "string" || accessToken.length === 0) {
1606
- throw new Error("Grok OAuth response missing access_token.");
1063
+ async function refreshChatGPTToken(creds) {
1064
+ if (!creds.clientId) {
1065
+ throw new Error("ChatGPT token refresh failed (401): no issued client ID saved");
1607
1066
  }
1608
- const refreshToken = typeof responseRefreshToken === "string" && responseRefreshToken.length > 0 ? responseRefreshToken : opts?.fallbackRefreshToken ?? "";
1609
- if (refreshToken.length === 0) {
1610
- throw new Error(
1611
- "Grok OAuth response missing refresh_token \u2014 the offline_access scope was not granted."
1612
- );
1067
+ const response = await postForm(TOKEN_URL, {
1068
+ grant_type: "refresh_token",
1069
+ client_id: creds.clientId,
1070
+ refresh_token: creds.refreshToken,
1071
+ resource: RESOURCE
1072
+ });
1073
+ if (!response.ok) {
1074
+ const text = await response.text().catch(() => "");
1075
+ throw new Error(`ChatGPT token refresh failed (${response.status}): ${text.slice(0, 300)}`);
1613
1076
  }
1614
- const responseExpiresIn = Number(data.expires_in);
1615
- let expiresIn;
1616
- if (Number.isFinite(responseExpiresIn) && responseExpiresIn > 0) {
1617
- expiresIn = responseExpiresIn;
1618
- } else {
1619
- const exp = jwtExpirySeconds(accessToken);
1620
- const fromJwt = exp !== void 0 ? exp - Math.floor(Date.now() / 1e3) : 0;
1621
- expiresIn = fromJwt > 0 ? fromJwt : DEFAULT_TOKEN_LIFETIME_SECONDS;
1077
+ const data = await response.json();
1078
+ if (!data.access_token || !data.refresh_token) {
1079
+ throw new Error("ChatGPT token refresh returned no tokens.");
1622
1080
  }
1081
+ const expiresIn = data.expires_in ?? 3600;
1623
1082
  return {
1624
- accessToken,
1625
- refreshToken,
1083
+ accessToken: data.access_token,
1084
+ refreshToken: data.refresh_token,
1626
1085
  expiresAt: Date.now() + expiresIn * 1e3,
1627
1086
  expiresIn,
1628
- baseUrl: grokCliBaseUrl()
1629
- };
1630
- }
1631
- async function requestDeviceAuthorization2() {
1632
- const { status, data } = await postForm2("/oauth2/device/code", {
1633
- client_id: clientId(),
1634
- scope: OAUTH_SCOPE
1635
- });
1636
- if (status !== 200) {
1637
- throw new Error(`Grok device authorization failed (${status}): ${errorDetail2(data)}`);
1638
- }
1639
- const userCode = data.user_code;
1640
- const deviceCode = data.device_code;
1641
- const verificationUriComplete = data.verification_uri_complete;
1642
- if (typeof userCode !== "string" || typeof deviceCode !== "string") {
1643
- throw new Error("Grok device authorization response missing user_code/device_code.");
1644
- }
1645
- return {
1646
- userCode,
1647
- deviceCode,
1648
- verificationUri: typeof data.verification_uri === "string" ? data.verification_uri : "",
1649
- verificationUriComplete: typeof verificationUriComplete === "string" ? verificationUriComplete : "",
1650
- interval: Number(data.interval ?? 5) || 5,
1651
- // RFC 8628 §3.2: the server states how long the device code lives. Honor it
1652
- // rather than imposing our own budget — polling a code we know is dead only
1653
- // burns requests, and a longer local window would keep a user waiting past
1654
- // the point the code can ever succeed.
1655
- expiresInMs: (Number(data.expires_in) || 0) > 0 ? Number(data.expires_in) * 1e3 : DEVICE_TIMEOUT_FALLBACK_MS
1087
+ ...data.id_token ? { idToken: data.id_token } : {},
1088
+ ...data.scope ? { scopes: data.scope.split(/\s+/).filter(Boolean) } : {}
1656
1089
  };
1657
1090
  }
1658
- async function pollDeviceToken2(deviceCode) {
1659
- const { status, data } = await postForm2("/oauth2/token", {
1660
- client_id: clientId(),
1661
- device_code: deviceCode,
1662
- grant_type: "urn:ietf:params:oauth:grant-type:device_code"
1663
- });
1664
- if (status === 200 && typeof data.access_token === "string") {
1665
- return { kind: "success", creds: credsFromTokenResponse2(data) };
1666
- }
1667
- if (status >= 500) {
1668
- throw new Error(`Grok token polling server error (${status}): ${errorDetail2(data)}`);
1669
- }
1670
- const errorCode = typeof data.error === "string" ? data.error : "unknown_error";
1671
- switch (errorCode) {
1672
- case "authorization_pending":
1673
- return { kind: "pending" };
1674
- case "slow_down":
1675
- return { kind: "slow_down" };
1676
- case "expired_token":
1677
- return { kind: "expired" };
1678
- case "access_denied":
1679
- return { kind: "denied" };
1680
- default:
1681
- throw new Error(`Grok token polling failed (${status}): ${errorDetail2(data)}`);
1682
- }
1683
- }
1684
- function sleep2(ms) {
1685
- return new Promise((resolve) => {
1686
- setTimeout(resolve, ms);
1687
- });
1688
- }
1689
- async function loginXai(callbacks) {
1690
- const auth = await requestDeviceAuthorization2();
1691
- callbacks.onStatus(
1692
- `Visit ${auth.verificationUri || auth.verificationUriComplete} and enter code: ${auth.userCode}`
1693
- );
1694
- callbacks.onOpenUrl(auth.verificationUriComplete || auth.verificationUri);
1695
- callbacks.onStatus("Waiting for you to authorize in the browser...");
1696
- const deadline = Date.now() + auth.expiresInMs;
1697
- let interval = Math.max(auth.interval, 1);
1698
- while (Date.now() < deadline) {
1699
- const remaining = deadline - Date.now();
1700
- await sleep2(Math.min(interval * 1e3, remaining));
1701
- const result = await pollDeviceToken2(auth.deviceCode);
1702
- if (result.kind === "success") return result.creds;
1703
- if (result.kind === "denied") {
1704
- throw new Error("Grok authorization was denied.");
1705
- }
1706
- if (result.kind === "expired") {
1707
- throw new Error("Grok device code expired. Please run login again.");
1091
+ async function revokeChatGPTSignIn(creds, options = {}) {
1092
+ if (!creds.clientId || !creds.refreshToken) return false;
1093
+ const attempts = options.attempts ?? 3;
1094
+ for (let attempt = 0; attempt < attempts; attempt++) {
1095
+ try {
1096
+ const response = await postForm(REVOKE_URL, {
1097
+ token: creds.refreshToken,
1098
+ token_type_hint: "refresh_token",
1099
+ client_id: creds.clientId
1100
+ });
1101
+ if (response.ok) return true;
1102
+ if (response.status < 500) return false;
1103
+ } catch {
1708
1104
  }
1709
- if (result.kind === "slow_down") {
1710
- interval += 5;
1105
+ if (attempt < attempts - 1) {
1106
+ await new Promise((r) => setTimeout(r, (options.backoffMs ?? 500) * 2 ** attempt));
1711
1107
  }
1712
1108
  }
1713
- throw new Error("Grok login timed out. Please run login again.");
1714
- }
1715
- async function refreshXaiToken(refreshToken) {
1716
- const { status, data } = await postForm2("/oauth2/token", {
1717
- client_id: clientId(),
1718
- grant_type: "refresh_token",
1719
- refresh_token: refreshToken
1720
- });
1721
- if (status === 200 && typeof data.access_token === "string") {
1722
- return credsFromTokenResponse2(data, { fallbackRefreshToken: refreshToken });
1109
+ return false;
1110
+ }
1111
+ function decodeSegment(segment) {
1112
+ return JSON.parse(Buffer.from(segment, "base64url").toString("utf-8"));
1113
+ }
1114
+ async function verifyIdToken(idToken, expected) {
1115
+ const [headerSeg, payloadSeg, signatureSeg] = idToken.split(".");
1116
+ if (!headerSeg || !payloadSeg || !signatureSeg) throw new Error("Malformed ChatGPT ID token.");
1117
+ const header = decodeSegment(headerSeg);
1118
+ if (header.alg !== "RS256") throw new Error("Unexpected ChatGPT ID token algorithm.");
1119
+ const response = await fetch(JWKS_URL, { signal: oauthRequestSignal() });
1120
+ if (!response.ok) throw new Error(`Could not load OpenAI signing keys (${response.status}).`);
1121
+ const { keys = [] } = await response.json();
1122
+ const jwk = keys.find((k) => k.kid === header.kid);
1123
+ if (!jwk) throw new Error("ChatGPT ID token is signed with an unknown key.");
1124
+ const valid = import_node_crypto2.default.verify(
1125
+ "RSA-SHA256",
1126
+ Buffer.from(`${headerSeg}.${payloadSeg}`),
1127
+ import_node_crypto2.default.createPublicKey({ key: jwk, format: "jwk" }),
1128
+ Buffer.from(signatureSeg, "base64url")
1129
+ );
1130
+ if (!valid) throw new Error("ChatGPT ID token signature is invalid.");
1131
+ const claims = decodeSegment(payloadSeg);
1132
+ const now = Math.floor((expected.now ?? Date.now()) / 1e3);
1133
+ const aud = claims.aud;
1134
+ const audiences = Array.isArray(aud) ? aud : [aud];
1135
+ if (claims.iss !== ISSUER) throw new Error("ChatGPT ID token has the wrong issuer.");
1136
+ if (!audiences.includes(expected.clientId)) {
1137
+ throw new Error("ChatGPT ID token was issued to another app.");
1138
+ }
1139
+ if (typeof claims.exp !== "number" || claims.exp + ID_TOKEN_CLOCK_SKEW_S < now) {
1140
+ throw new Error("ChatGPT ID token has expired.");
1723
1141
  }
1724
- const errorCode = typeof data.error === "string" ? data.error : "";
1725
- throw new Error(`Grok token refresh failed (${status}): ${errorCode || errorDetail2(data)}`);
1142
+ if (claims.nonce !== expected.nonce) throw new Error("ChatGPT ID token nonce mismatch.");
1143
+ return claims;
1726
1144
  }
1727
1145
 
1728
1146
  // src/file-lock.ts
1729
- var import_promises3 = __toESM(require("fs/promises"), 1);
1730
- var import_promises4 = require("timers/promises");
1147
+ var import_promises4 = __toESM(require("fs/promises"), 1);
1148
+ var import_promises5 = require("timers/promises");
1731
1149
  var STALE_TIMEOUT_MS = 1e4;
1732
1150
  var RETRY_INTERVAL_MS = 50;
1733
1151
  var MAX_WAIT_MS = 5e3;
@@ -1747,7 +1165,7 @@ async function acquireLock(lockPath) {
1747
1165
  while (true) {
1748
1166
  try {
1749
1167
  const info = { pid: process.pid, timestamp: Date.now() };
1750
- await import_promises3.default.writeFile(lockPath, JSON.stringify(info), { flag: "wx" });
1168
+ await import_promises4.default.writeFile(lockPath, JSON.stringify(info), { flag: "wx" });
1751
1169
  return;
1752
1170
  } catch (err) {
1753
1171
  const code = err.code;
@@ -1756,47 +1174,47 @@ async function acquireLock(lockPath) {
1756
1174
  const lockPresent = await lockFileExists(lockPath);
1757
1175
  if (!lockPresent && ++refusedWithoutLock > 3) throw err;
1758
1176
  if (Date.now() - startTime > MAX_WAIT_MS) throw err;
1759
- await (0, import_promises4.setTimeout)(RETRY_INTERVAL_MS);
1177
+ await (0, import_promises5.setTimeout)(RETRY_INTERVAL_MS);
1760
1178
  continue;
1761
1179
  }
1762
1180
  try {
1763
- const content = await import_promises3.default.readFile(lockPath, "utf-8");
1181
+ const content = await import_promises4.default.readFile(lockPath, "utf-8");
1764
1182
  const info = JSON.parse(content);
1765
1183
  const isProcessAlive = isAlive(info.pid);
1766
1184
  const isStale = Date.now() - info.timestamp > STALE_TIMEOUT_MS;
1767
1185
  if (!isProcessAlive || isStale) {
1768
- await import_promises3.default.unlink(lockPath).catch(() => {
1186
+ await import_promises4.default.unlink(lockPath).catch(() => {
1769
1187
  });
1770
1188
  continue;
1771
1189
  }
1772
1190
  } catch {
1773
- const stat = await import_promises3.default.stat(lockPath).catch(() => null);
1191
+ const stat = await import_promises4.default.stat(lockPath).catch(() => null);
1774
1192
  if (!stat) continue;
1775
1193
  if (Date.now() - stat.mtimeMs > STALE_TIMEOUT_MS) {
1776
- await import_promises3.default.unlink(lockPath).catch(() => {
1194
+ await import_promises4.default.unlink(lockPath).catch(() => {
1777
1195
  });
1778
1196
  continue;
1779
1197
  }
1780
1198
  }
1781
1199
  if (Date.now() - startTime > MAX_WAIT_MS) {
1782
- await import_promises3.default.unlink(lockPath).catch(() => {
1200
+ await import_promises4.default.unlink(lockPath).catch(() => {
1783
1201
  });
1784
1202
  continue;
1785
1203
  }
1786
- await (0, import_promises4.setTimeout)(RETRY_INTERVAL_MS);
1204
+ await (0, import_promises5.setTimeout)(RETRY_INTERVAL_MS);
1787
1205
  }
1788
1206
  }
1789
1207
  }
1790
1208
  async function lockFileExists(lockPath) {
1791
1209
  try {
1792
- await import_promises3.default.stat(lockPath);
1210
+ await import_promises4.default.stat(lockPath);
1793
1211
  return true;
1794
1212
  } catch (err) {
1795
1213
  return err.code !== "ENOENT";
1796
1214
  }
1797
1215
  }
1798
1216
  async function releaseLock(lockPath) {
1799
- await import_promises3.default.unlink(lockPath).catch(() => {
1217
+ await import_promises4.default.unlink(lockPath).catch(() => {
1800
1218
  });
1801
1219
  }
1802
1220
  function isAlive(pid) {
@@ -1810,10 +1228,10 @@ function isAlive(pid) {
1810
1228
  }
1811
1229
 
1812
1230
  // src/logger.ts
1813
- var import_node_fs4 = __toESM(require("fs"), 1);
1231
+ var import_node_fs3 = __toESM(require("fs"), 1);
1814
1232
  var import_node_path4 = __toESM(require("path"), 1);
1815
- var import_node_crypto5 = require("crypto");
1816
- var import_gg_ai = require("@kenkaiiii/gg-ai");
1233
+ var import_node_crypto3 = require("crypto");
1234
+ var import_gg_ai2 = require("@kenkaiiii/gg-ai");
1817
1235
  var MAX_BYTES = 10 * 1024 * 1024;
1818
1236
  var MIN_HEADROOM_BYTES = 64 * 1024;
1819
1237
  var fd = null;
@@ -1828,14 +1246,14 @@ var cleanups = [];
1828
1246
  var exactSecrets = [];
1829
1247
  function rotateIfNeeded(filePath, force = false) {
1830
1248
  try {
1831
- const st = import_node_fs4.default.statSync(filePath);
1249
+ const st = import_node_fs3.default.statSync(filePath);
1832
1250
  if (!force && st.size <= MAX_BYTES - MIN_HEADROOM_BYTES) return;
1833
1251
  const rotated = `${filePath}.1`;
1834
1252
  try {
1835
- import_node_fs4.default.unlinkSync(rotated);
1253
+ import_node_fs3.default.unlinkSync(rotated);
1836
1254
  } catch {
1837
1255
  }
1838
- import_node_fs4.default.renameSync(filePath, rotated);
1256
+ import_node_fs3.default.renameSync(filePath, rotated);
1839
1257
  } catch {
1840
1258
  }
1841
1259
  }
@@ -1843,18 +1261,18 @@ function rotateOpenLog() {
1843
1261
  if (fd === null || logPath === null) return false;
1844
1262
  const sizeBefore = fileBytes;
1845
1263
  try {
1846
- import_node_fs4.default.closeSync(fd);
1264
+ import_node_fs3.default.closeSync(fd);
1847
1265
  } catch {
1848
1266
  }
1849
1267
  fd = null;
1850
1268
  rotateIfNeeded(logPath, true);
1851
1269
  try {
1852
- fd = import_node_fs4.default.openSync(logPath, "a");
1853
- fileBytes = import_node_fs4.default.fstatSync(fd).size;
1270
+ fd = import_node_fs3.default.openSync(logPath, "a");
1271
+ fileBytes = import_node_fs3.default.fstatSync(fd).size;
1854
1272
  } catch {
1855
1273
  if (fd !== null) {
1856
1274
  try {
1857
- import_node_fs4.default.closeSync(fd);
1275
+ import_node_fs3.default.closeSync(fd);
1858
1276
  } catch {
1859
1277
  }
1860
1278
  }
@@ -1868,24 +1286,24 @@ function rotateOpenLog() {
1868
1286
  function openLog(filePath, name) {
1869
1287
  if (fd !== null || capped) return false;
1870
1288
  appName = name;
1871
- exactSecrets = (0, import_gg_ai.environmentSecrets)(process.env);
1289
+ exactSecrets = (0, import_gg_ai2.environmentSecrets)(process.env);
1872
1290
  try {
1873
- import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(filePath), { recursive: true, mode: 448 });
1291
+ import_node_fs3.default.mkdirSync(import_node_path4.default.dirname(filePath), { recursive: true, mode: 448 });
1874
1292
  } catch {
1875
1293
  }
1876
1294
  rotateIfNeeded(filePath);
1877
1295
  try {
1878
- fd = import_node_fs4.default.openSync(filePath, "a");
1296
+ fd = import_node_fs3.default.openSync(filePath, "a");
1879
1297
  } catch {
1880
1298
  fd = null;
1881
1299
  fileBytes = 0;
1882
1300
  return false;
1883
1301
  }
1884
1302
  try {
1885
- fileBytes = import_node_fs4.default.fstatSync(fd).size;
1303
+ fileBytes = import_node_fs3.default.fstatSync(fd).size;
1886
1304
  } catch {
1887
1305
  try {
1888
- import_node_fs4.default.closeSync(fd);
1306
+ import_node_fs3.default.closeSync(fd);
1889
1307
  } catch {
1890
1308
  }
1891
1309
  fd = null;
@@ -1895,10 +1313,10 @@ function openLog(filePath, name) {
1895
1313
  logPath = filePath;
1896
1314
  processBytes = 0;
1897
1315
  rotationFailed = false;
1898
- sessionId = (0, import_node_crypto5.randomBytes)(4).toString("hex");
1316
+ sessionId = (0, import_node_crypto3.randomBytes)(4).toString("hex");
1899
1317
  try {
1900
1318
  if (fileBytes < MAX_BYTES) {
1901
- const written = import_node_fs4.default.writeSync(fd, "\n");
1319
+ const written = import_node_fs3.default.writeSync(fd, "\n");
1902
1320
  fileBytes += written;
1903
1321
  processBytes += written;
1904
1322
  }
@@ -1915,10 +1333,10 @@ function isLoggerOpen() {
1915
1333
  function log(level, category, message, data) {
1916
1334
  if (fd === null) return;
1917
1335
  const ts = (/* @__PURE__ */ new Date()).toISOString();
1918
- const safeMessage = (0, import_gg_ai.redactText)(message, { secrets: exactSecrets });
1336
+ const safeMessage = (0, import_gg_ai2.redactText)(message, { secrets: exactSecrets });
1919
1337
  let line = `[${ts}] [sid=${sessionId}] [${level}] [${category}] ${safeMessage}`;
1920
1338
  if (data) {
1921
- const safeData = (0, import_gg_ai.redactValue)(data, { secrets: exactSecrets });
1339
+ const safeData = (0, import_gg_ai2.redactValue)(data, { secrets: exactSecrets });
1922
1340
  const pairs = Object.entries(safeData).map(([k, v]) => {
1923
1341
  if (typeof v === "string") return `${k}=${v}`;
1924
1342
  if (typeof v === "bigint") return `${k}=${String(v)}`;
@@ -1933,9 +1351,9 @@ function log(level, category, message, data) {
1933
1351
  `;
1934
1352
  try {
1935
1353
  if (fileBytes + Buffer.byteLength(capLine) <= MAX_BYTES) {
1936
- fileBytes += import_node_fs4.default.writeSync(fd, capLine);
1354
+ fileBytes += import_node_fs3.default.writeSync(fd, capLine);
1937
1355
  }
1938
- import_node_fs4.default.closeSync(fd);
1356
+ import_node_fs3.default.closeSync(fd);
1939
1357
  } catch {
1940
1358
  }
1941
1359
  fd = null;
@@ -1945,7 +1363,7 @@ function log(level, category, message, data) {
1945
1363
  if (fileBytes + lineBytes > MAX_BYTES && !rotationFailed && !rotateOpenLog()) return;
1946
1364
  if (fd === null) return;
1947
1365
  try {
1948
- const written = import_node_fs4.default.writeSync(fd, line);
1366
+ const written = import_node_fs3.default.writeSync(fd, line);
1949
1367
  fileBytes += written;
1950
1368
  processBytes += written;
1951
1369
  } catch {
@@ -1958,7 +1376,7 @@ function closeLogger(opts) {
1958
1376
  if (fd !== null) {
1959
1377
  if (opts?.shutdownLine !== false) log("INFO", "shutdown", `${appName} shutting down`);
1960
1378
  try {
1961
- if (fd !== null) import_node_fs4.default.closeSync(fd);
1379
+ if (fd !== null) import_node_fs3.default.closeSync(fd);
1962
1380
  } catch {
1963
1381
  }
1964
1382
  }
@@ -1974,13 +1392,13 @@ function closeLogger(opts) {
1974
1392
  }
1975
1393
 
1976
1394
  // src/hidden-providers.ts
1977
- var HIDDEN_PROVIDERS = ["gemini"];
1395
+ var HIDDEN_PROVIDERS = [];
1978
1396
  function isHiddenProvider(provider) {
1979
1397
  return HIDDEN_PROVIDERS.includes(provider);
1980
1398
  }
1981
1399
 
1982
1400
  // src/auth-storage.ts
1983
- var import_gg_ai2 = require("@kenkaiiii/gg-ai");
1401
+ var import_gg_ai3 = require("@kenkaiiii/gg-ai");
1984
1402
 
1985
1403
  // src/minimax-region.ts
1986
1404
  var MINIMAX_GLOBAL_BASE_URL = "https://api.minimax.io/anthropic";
@@ -2065,22 +1483,76 @@ async function detectXiaomiTokenPlanBaseUrl(apiKey, options = {}) {
2065
1483
  }
2066
1484
 
2067
1485
  // src/auth-storage.ts
2068
- var MOONSHOT_OAUTH_KEY = "moonshot-oauth";
2069
- var XAI_OAUTH_KEY = "xai-oauth";
2070
- var DUAL_AUTH_PROVIDERS = [
1486
+ var OPENAI_CHATGPT_KEY = "openai-chatgpt";
1487
+ var KIMI_CODE_KEY = "kimi-code";
1488
+ var RETIRED_SIGN_INS = [
1489
+ {
1490
+ provider: "anthropic",
1491
+ storageKey: "anthropic",
1492
+ message: "Your old Claude sign-in no longer works. Sign in with Claude again to keep using your subscription."
1493
+ },
1494
+ {
1495
+ provider: "openai",
1496
+ storageKey: "openai",
1497
+ message: "Your old ChatGPT sign-in no longer works. Continue with ChatGPT again, or add an OpenAI API key."
1498
+ },
2071
1499
  {
2072
1500
  provider: "moonshot",
2073
- oauthKey: MOONSHOT_OAUTH_KEY,
2074
- oauthLabel: "Kimi OAuth",
2075
- apiKeyLabel: "Moonshot API key",
2076
- restoreHint: 'Run "ggcoder login" and choose Kimi OAuth to restore OAuth auth.'
1501
+ storageKey: "moonshot-oauth",
1502
+ message: "Kimi sign-in was removed. Paste a Kimi Code plan key from the Kimi Code Console, or a Moonshot API key."
2077
1503
  },
2078
1504
  {
2079
1505
  provider: "xai",
2080
- oauthKey: XAI_OAUTH_KEY,
2081
- oauthLabel: "Grok OAuth",
2082
- apiKeyLabel: "xAI API key",
2083
- restoreHint: 'Run "ggcoder login" and choose Grok OAuth to restore OAuth auth.'
1506
+ storageKey: "xai-oauth",
1507
+ message: "Grok sign-in was removed: xAI offers SuperGrok and X Premium usage only in apps it partners with. Add an xAI API key instead."
1508
+ },
1509
+ {
1510
+ provider: "gemini",
1511
+ storageKey: "gemini",
1512
+ message: "Google sign-in was removed. Add a Gemini API key from Google AI Studio instead."
1513
+ }
1514
+ ];
1515
+ function isRetiredEntry(retired, value) {
1516
+ if (!value) return false;
1517
+ switch (retired.storageKey) {
1518
+ case "anthropic":
1519
+ return isLegacyClaudeOAuth(value);
1520
+ // These slots now hold API keys; only a refreshable sign-in token is retired.
1521
+ case "openai":
1522
+ case "gemini":
1523
+ return Boolean(value.refreshToken) || Boolean(value.accountId) || Boolean(value.projectId);
1524
+ default:
1525
+ return true;
1526
+ }
1527
+ }
1528
+ function isRetiredStorageKey(data, storageKey) {
1529
+ return RETIRED_SIGN_INS.some(
1530
+ (r) => r.storageKey === storageKey && isRetiredEntry(r, data[storageKey])
1531
+ );
1532
+ }
1533
+ function usableEntry(data, storageKey) {
1534
+ return isRetiredStorageKey(data, storageKey) ? void 0 : data[storageKey];
1535
+ }
1536
+ function storageKeyProvider(storageKey) {
1537
+ if (storageKey === OPENAI_CHATGPT_KEY) return "openai";
1538
+ if (storageKey === KIMI_CODE_KEY) return "moonshot";
1539
+ if (storageKey === XIAOMI_CREDITS_KEY) return "xiaomi";
1540
+ if (storageKey === GEMINI_API_KEY) return "gemini";
1541
+ return storageKey;
1542
+ }
1543
+ function apiKeyStorageKeys(provider) {
1544
+ if (provider === "moonshot") return [KIMI_CODE_KEY, "moonshot"];
1545
+ if (provider === "xiaomi") return ["xiaomi", XIAOMI_CREDITS_KEY];
1546
+ if (provider === "gemini") return [GEMINI_API_KEY];
1547
+ return [provider];
1548
+ }
1549
+ var DUAL_AUTH_PROVIDERS = [
1550
+ {
1551
+ provider: "openai",
1552
+ oauthKey: OPENAI_CHATGPT_KEY,
1553
+ oauthLabel: "ChatGPT plan",
1554
+ apiKeyLabel: "OpenAI API key",
1555
+ restoreHint: 'Run "ggcoder login" and choose Continue with ChatGPT to use your ChatGPT plan again.'
2084
1556
  }
2085
1557
  ];
2086
1558
  function dualAuthProvider(provider) {
@@ -2105,14 +1577,20 @@ function activeBaseUrlEntry(data, provider) {
2105
1577
  const dual = dualAuthProvider(provider);
2106
1578
  if (dual) {
2107
1579
  const oauth = data[dual.oauthKey];
1580
+ const apiKey = usableEntry(data, dual.provider);
2108
1581
  if (oauth) {
2109
1582
  const exhaustedUntil = oauth.usageExhaustedUntil ?? 0;
2110
- if (Date.now() < exhaustedUntil && data[dual.provider]) return data[dual.provider];
1583
+ if (Date.now() < exhaustedUntil && apiKey) return apiKey;
2111
1584
  return oauth;
2112
1585
  }
2113
- return data[dual.provider];
1586
+ return apiKey;
1587
+ }
1588
+ const key = apiKeyStorageKeys(provider).find((k) => usableEntry(data, k));
1589
+ const entry = key ? data[key] : void 0;
1590
+ if (key === KIMI_CODE_KEY && entry && !entry.baseUrl) {
1591
+ return { ...entry, baseUrl: KIMI_CODE_DEFAULT_BASE_URL };
2114
1592
  }
2115
- return data[provider];
1593
+ return entry;
2116
1594
  }
2117
1595
  var MINIMAX_REGION_RETRY_MS = 5 * 60 * 1e3;
2118
1596
  var miniMaxRegionProbes = /* @__PURE__ */ new Map();
@@ -2123,9 +1601,17 @@ function cachedMiniMaxRegion(apiKey) {
2123
1601
  miniMaxRegionProbes.set(apiKey, { at: Date.now(), result });
2124
1602
  return result;
2125
1603
  }
1604
+ var kimiRegionProbes = /* @__PURE__ */ new Map();
1605
+ function cachedKimiRegion(apiKey) {
1606
+ const cached = kimiRegionProbes.get(apiKey);
1607
+ if (cached && Date.now() - cached.at < MINIMAX_REGION_RETRY_MS) return cached.result;
1608
+ const result = detectKimiCodeBaseUrl(apiKey).catch(() => void 0);
1609
+ kimiRegionProbes.set(apiKey, { at: Date.now(), result });
1610
+ return result;
1611
+ }
2126
1612
  function readStoredBaseUrlSync(authFile, provider) {
2127
1613
  try {
2128
- const data = JSON.parse((0, import_node_fs5.readFileSync)(authFile, "utf-8"));
1614
+ const data = JSON.parse((0, import_node_fs4.readFileSync)(authFile, "utf-8"));
2129
1615
  return activeBaseUrlEntry(data, provider)?.baseUrl;
2130
1616
  } catch {
2131
1617
  return void 0;
@@ -2138,21 +1624,7 @@ function refreshThresholdMs(creds) {
2138
1624
  return Math.max(MIN_REFRESH_THRESHOLD_MS, lifetimeMs * REFRESH_THRESHOLD_RATIO);
2139
1625
  }
2140
1626
  var USAGE_EXHAUSTED_DEFAULT_MS = 15 * 60 * 1e3;
2141
- var STATIC_API_KEY_PROVIDERS = /* @__PURE__ */ new Set([
2142
- // Anthropic is API-key only; Claude subscriptions run through Claude Code.
2143
- "anthropic",
2144
- "glm",
2145
- "moonshot",
2146
- "xiaomi",
2147
- "minimax",
2148
- "deepseek",
2149
- "openrouter",
2150
- "huggingface",
2151
- "sakana",
2152
- "xai",
2153
- // Local endpoints: a fixed (usually placeholder) key, never refreshable.
2154
- "local"
2155
- ]);
1627
+ var REFRESHABLE_STORAGE_KEYS = /* @__PURE__ */ new Set([OPENAI_CHATGPT_KEY]);
2156
1628
  var AuthStorage = class {
2157
1629
  data = {};
2158
1630
  filePath;
@@ -2173,10 +1645,12 @@ var AuthStorage = class {
2173
1645
  refreshLocks = /* @__PURE__ */ new Map();
2174
1646
  detectMiniMaxRegion;
2175
1647
  detectXiaomiCluster;
1648
+ detectKimiRegion;
2176
1649
  constructor(filePath, options) {
2177
1650
  this.filePath = filePath ?? getAppPaths().authFile;
2178
1651
  this.detectMiniMaxRegion = options?.detectMiniMaxBaseUrl ?? cachedMiniMaxRegion;
2179
1652
  this.detectXiaomiCluster = options?.detectXiaomiTokenPlanBaseUrl ?? detectXiaomiTokenPlanBaseUrl;
1653
+ this.detectKimiRegion = options?.detectKimiCodeBaseUrl ?? cachedKimiRegion;
2180
1654
  }
2181
1655
  /** Path to the on-disk auth file. Useful for status output. */
2182
1656
  get path() {
@@ -2204,22 +1678,15 @@ var AuthStorage = class {
2204
1678
  return keys.find((key) => Boolean(this.data[key]));
2205
1679
  }
2206
1680
  /**
2207
- * True if the user has any usable auth for the logical provider. For a
2208
- * dual-auth provider (Kimi/Grok) either the OAuth credential or the API key
2209
- * satisfies it.
1681
+ * True if the user has any usable auth for the logical provider: the plan
1682
+ * sign-in or any of its API keys. A retired sign-in doesn't count.
2210
1683
  */
2211
1684
  async hasProviderAuth(provider) {
2212
1685
  if (isHiddenProvider(provider)) return false;
2213
1686
  await this.ensureFresh();
2214
1687
  const dual = dualAuthProvider(provider);
2215
1688
  if (dual) {
2216
- return Boolean(this.data[dual.oauthKey] || this.data[dual.provider]);
2217
- }
2218
- if (provider === "xiaomi") {
2219
- return Boolean(this.data["xiaomi"] || this.data[XIAOMI_CREDITS_KEY]);
2220
- }
2221
- if (provider === "gemini") {
2222
- return Boolean(this.data["gemini"] || this.data[GEMINI_API_KEY]);
1689
+ return Boolean(this.data[dual.oauthKey] || usableEntry(this.data, dual.provider));
2223
1690
  }
2224
1691
  if (provider === "anthropic") {
2225
1692
  return Boolean(this.data.anthropic) && !isLegacyClaudeOAuth(this.data.anthropic) || isClaudeCodeAuthMarker(this.data[CLAUDE_CODE_AUTH_KEY]);
@@ -2227,7 +1694,25 @@ var AuthStorage = class {
2227
1694
  if (provider === "local") {
2228
1695
  return Object.keys(this.data).some((key) => key.startsWith(LOCAL_AUTH_KEY_PREFIX));
2229
1696
  }
2230
- return Boolean(this.data[provider]);
1697
+ return apiKeyStorageKeys(provider).some((key) => Boolean(usableEntry(this.data, key)));
1698
+ }
1699
+ /** True if any of `provider`'s API keys is stored. A retired sign-in in a key slot doesn't count. */
1700
+ async hasApiKey(provider) {
1701
+ await this.ensureFresh();
1702
+ if (provider === "anthropic" && isLegacyClaudeOAuth(this.data.anthropic)) return false;
1703
+ return apiKeyStorageKeys(provider).some((key) => Boolean(usableEntry(this.data, key)));
1704
+ }
1705
+ /**
1706
+ * `provider`'s first stored API key, never its plan sign-in. For calls that
1707
+ * only an API key may make (OpenAI's Images API isn't on the ChatGPT plan).
1708
+ */
1709
+ async resolveApiKey(provider) {
1710
+ await this.ensureFresh();
1711
+ for (const key of apiKeyStorageKeys(provider)) {
1712
+ const creds = usableEntry(this.data, key);
1713
+ if (creds && !(provider === "anthropic" && isLegacyClaudeOAuth(creds))) return creds;
1714
+ }
1715
+ throw new NotLoggedInError(provider);
2231
1716
  }
2232
1717
  /** True when the user signed in with Claude through Claude Code (marker only, no token). */
2233
1718
  async hasClaudeCodeLogin() {
@@ -2241,17 +1726,24 @@ var AuthStorage = class {
2241
1726
  if (isLegacyClaudeOAuth(latest.anthropic)) delete latest.anthropic;
2242
1727
  });
2243
1728
  }
1729
+ /** Retired sign-ins still on disk, so the UI can ask the user to reconnect. */
1730
+ async legacySignIns() {
1731
+ await this.ensureFresh();
1732
+ return RETIRED_SIGN_INS.filter((r) => isRetiredEntry(r, this.data[r.storageKey]));
1733
+ }
2244
1734
  /**
2245
- * Remove an old GG Claude sign-in token (`sk-ant-oat…`), if any. True when one
2246
- * was removed, so the caller can tell the user once to sign in with Claude again.
1735
+ * Remove `provider`'s retired sign-in, if any. True when one was removed, so
1736
+ * the caller can tell the user once to reconnect.
2247
1737
  */
2248
- async takeLegacyClaudeSignIn() {
1738
+ async takeLegacySignIn(provider) {
1739
+ const retired = RETIRED_SIGN_INS.find((r) => r.provider === provider);
1740
+ if (!retired) return false;
2249
1741
  await this.ensureFresh();
2250
- if (!isLegacyClaudeOAuth(this.data.anthropic)) return false;
1742
+ if (!isRetiredEntry(retired, this.data[retired.storageKey])) return false;
2251
1743
  let removed = false;
2252
1744
  await this.mutateLatest((latest) => {
2253
- if (!isLegacyClaudeOAuth(latest.anthropic)) return;
2254
- delete latest.anthropic;
1745
+ if (!isRetiredEntry(retired, latest[retired.storageKey])) return;
1746
+ delete latest[retired.storageKey];
2255
1747
  removed = true;
2256
1748
  });
2257
1749
  return removed;
@@ -2271,7 +1763,7 @@ var AuthStorage = class {
2271
1763
  */
2272
1764
  async setLocalEndpoint(endpointId, baseUrl, apiKey) {
2273
1765
  await this.setCredentials(`${LOCAL_AUTH_KEY_PREFIX}${endpointId}`, {
2274
- accessToken: apiKey && apiKey.length > 0 ? apiKey : import_gg_ai2.LOCAL_NO_API_KEY,
1766
+ accessToken: apiKey && apiKey.length > 0 ? apiKey : import_gg_ai3.LOCAL_NO_API_KEY,
2275
1767
  refreshToken: "",
2276
1768
  expiresAt: Date.now() + LOCAL_CREDENTIAL_LIFETIME_MS,
2277
1769
  baseUrl
@@ -2292,18 +1784,18 @@ var AuthStorage = class {
2292
1784
  const oauthCreds = dual ? this.data[dual.oauthKey] : void 0;
2293
1785
  if (dual && oauthCreds) {
2294
1786
  const exhaustedUntil = oauthCreds.usageExhaustedUntil ?? 0;
2295
- const apiKeyActive = Date.now() < exhaustedUntil && Boolean(this.data[dual.provider]);
1787
+ const apiKeyActive = Date.now() < exhaustedUntil && Boolean(usableEntry(this.data, dual.provider));
2296
1788
  if (!apiKeyActive) return false;
2297
1789
  }
2298
- return STATIC_API_KEY_PROVIDERS.has(provider);
1790
+ return !REFRESHABLE_STORAGE_KEYS.has(provider);
2299
1791
  }
2300
1792
  /**
2301
1793
  * The base URL on the credential that is active right now, if any.
2302
1794
  * Synchronous — call only after load()/resolveCredentials() populated the
2303
- * snapshot. For a dual-auth provider this is the subscription endpoint (Kimi
2304
- * For Coding, the Grok CLI proxy) whenever the OAuth entry is the one
2305
- * resolveCredentials would serve (i.e. not currently usage-exhausted with an
2306
- * API key configured).
1795
+ * snapshot. For a dual-auth provider this is the plan endpoint whenever the
1796
+ * sign-in is the one resolveCredentials would serve (i.e. not currently
1797
+ * usage-exhausted with an API key configured); for a plan/pay-per-token key
1798
+ * split it is the plan key's endpoint when one is stored.
2307
1799
  */
2308
1800
  getStoredBaseUrl(provider) {
2309
1801
  return activeBaseUrlEntry(this.data, provider)?.baseUrl;
@@ -2312,7 +1804,7 @@ var AuthStorage = class {
2312
1804
  const first = !this.loaded;
2313
1805
  await withFileLock(this.filePath, async () => {
2314
1806
  try {
2315
- const content = await import_promises5.default.readFile(this.filePath, "utf-8");
1807
+ const content = await import_promises6.default.readFile(this.filePath, "utf-8");
2316
1808
  this.data = JSON.parse(content);
2317
1809
  if (first) {
2318
1810
  log("INFO", "auth", `Loaded credentials from ${this.filePath}`, {
@@ -2358,7 +1850,7 @@ var AuthStorage = class {
2358
1850
  }
2359
1851
  let changed;
2360
1852
  try {
2361
- const stat = await import_promises5.default.stat(this.filePath);
1853
+ const stat = await import_promises6.default.stat(this.filePath);
2362
1854
  changed = stat.ino !== this.snapshotIno || stat.mtimeMs !== this.snapshotMtimeMs || stat.size !== this.snapshotSize;
2363
1855
  } catch {
2364
1856
  changed = this.snapshotSize !== -1;
@@ -2371,7 +1863,7 @@ var AuthStorage = class {
2371
1863
  */
2372
1864
  async rememberSnapshot() {
2373
1865
  try {
2374
- const stat = await import_promises5.default.stat(this.filePath);
1866
+ const stat = await import_promises6.default.stat(this.filePath);
2375
1867
  this.snapshotMtimeMs = stat.mtimeMs;
2376
1868
  this.snapshotSize = stat.size;
2377
1869
  this.snapshotIno = stat.ino;
@@ -2426,9 +1918,34 @@ var AuthStorage = class {
2426
1918
  });
2427
1919
  return { ...creds, baseUrl };
2428
1920
  }
2429
- async setCredentials(provider, creds) {
1921
+ /**
1922
+ * A Kimi Code key saved without an endpoint gets the host that issued it
1923
+ * (`api.kimi.com` or `api.kimi.ai`), saved so the check runs once.
1924
+ */
1925
+ async withKimiRegion(creds) {
1926
+ const baseUrl = await this.detectKimiRegion(creds.accessToken);
1927
+ if (!baseUrl) return { ...creds, baseUrl: KIMI_CODE_DEFAULT_BASE_URL };
1928
+ log("INFO", "auth", `Kimi Code key belongs to ${baseUrl} \u2014 saved for future requests`);
1929
+ await this.mutateLatest((latest) => {
1930
+ const current = latest[KIMI_CODE_KEY];
1931
+ if (current?.accessToken === creds.accessToken && !current.baseUrl) {
1932
+ current.baseUrl = baseUrl;
1933
+ }
1934
+ });
1935
+ return { ...creds, baseUrl };
1936
+ }
1937
+ /**
1938
+ * Store `creds` under `storageKey`. Connecting a provider again also removes
1939
+ * its retired sign-in, which is what the reconnect notice asked for.
1940
+ */
1941
+ async setCredentials(storageKey, creds) {
1942
+ const provider = storageKeyProvider(storageKey);
2430
1943
  await this.mutateLatest((latest) => {
2431
- latest[provider] = creds;
1944
+ for (const retired of RETIRED_SIGN_INS) {
1945
+ if (retired.provider !== provider || retired.storageKey === storageKey) continue;
1946
+ if (isRetiredEntry(retired, latest[retired.storageKey])) delete latest[retired.storageKey];
1947
+ }
1948
+ latest[storageKey] = creds;
2432
1949
  });
2433
1950
  }
2434
1951
  /**
@@ -2465,9 +1982,9 @@ var AuthStorage = class {
2465
1982
  * Mark the credential stored under `storageKey` as usage-exhausted until
2466
1983
  * `resetsAt` (unix SECONDS, from the provider's rate-limit response) or a
2467
1984
  * 15-minute default when no reset time is known. While the mark is in the
2468
- * future, `resolveCredentials("moonshot")` serves the Moonshot API key
2469
- * instead of the subscription OAuth credential (when both are configured) — OAuth
2470
- * stays the preferred credential and is retried automatically once the mark
1985
+ * future, `resolveCredentials("openai")` serves the OpenAI API key instead of
1986
+ * the ChatGPT plan sign-in (when both are configured) — the plan stays the
1987
+ * preferred credential and is retried automatically once the mark
2471
1988
  * lapses. Persisted to auth.json so a restart (or another gg-app window)
2472
1989
  * doesn't burn a request rediscovering the same exhausted window. No-op if
2473
1990
  * nothing is stored under `storageKey`.
@@ -2512,12 +2029,12 @@ var AuthStorage = class {
2512
2029
  await this.ensureFresh();
2513
2030
  }
2514
2031
  const directStorageKeys = opts?.storageKeys && !(opts.storageKeys.length === 1 && opts.storageKeys[0] === provider) ? opts.storageKeys : providerStorageKeys(provider);
2515
- if (!directStorageKeys.some((key) => Boolean(this.data[key])) && !(provider === "anthropic" && this.data[CLAUDE_CODE_AUTH_KEY])) {
2032
+ if (!directStorageKeys.some((key) => Boolean(usableEntry(this.data, key))) && !(provider === "anthropic" && this.data[CLAUDE_CODE_AUTH_KEY])) {
2516
2033
  await this.reloadLatest();
2517
2034
  }
2518
2035
  if (opts?.storageKeys && !(opts.storageKeys.length === 1 && opts.storageKeys[0] === provider)) {
2519
2036
  for (const key of opts.storageKeys) {
2520
- const creds2 = this.data[key];
2037
+ const creds2 = usableEntry(this.data, key);
2521
2038
  if (!creds2) continue;
2522
2039
  if (key !== provider && dualAuthProviderByOAuthKey(key)) {
2523
2040
  return await this.resolveCredentials(key, {
@@ -2531,6 +2048,7 @@ var AuthStorage = class {
2531
2048
  ...opts.rejectedToken !== void 0 ? { rejectedToken: opts.rejectedToken } : {}
2532
2049
  });
2533
2050
  }
2051
+ if (key === KIMI_CODE_KEY && !creds2.baseUrl) return await this.withKimiRegion(creds2);
2534
2052
  return creds2;
2535
2053
  }
2536
2054
  throw new NotLoggedInError(provider);
@@ -2543,15 +2061,16 @@ var AuthStorage = class {
2543
2061
  }
2544
2062
  const dual = dualAuthProvider(provider);
2545
2063
  const dualOAuthCreds = dual ? this.data[dual.oauthKey] : void 0;
2064
+ const dualApiKey = dual ? usableEntry(this.data, dual.provider) : void 0;
2546
2065
  if (dual && dualOAuthCreds) {
2547
2066
  const exhaustedUntil = dualOAuthCreds.usageExhaustedUntil ?? 0;
2548
- if (Date.now() < exhaustedUntil && this.data[dual.provider]) {
2067
+ if (Date.now() < exhaustedUntil && dualApiKey) {
2549
2068
  log(
2550
2069
  "WARN",
2551
2070
  "auth",
2552
2071
  `${dual.oauthLabel} usage window is exhausted \u2014 using the ${dual.apiKeyLabel} until ${new Date(exhaustedUntil).toISOString()} (OAuth resumes automatically).`
2553
2072
  );
2554
- return this.data[dual.provider];
2073
+ return dualApiKey;
2555
2074
  }
2556
2075
  try {
2557
2076
  return await this.resolveCredentials(dual.oauthKey, {
@@ -2559,23 +2078,24 @@ var AuthStorage = class {
2559
2078
  ...opts?.rejectedToken !== void 0 ? { rejectedToken: opts.rejectedToken } : {}
2560
2079
  });
2561
2080
  } catch (err) {
2562
- if (err instanceof NotLoggedInError && this.data[dual.provider]) {
2081
+ if (err instanceof NotLoggedInError && dualApiKey) {
2563
2082
  log(
2564
2083
  "WARN",
2565
2084
  "auth",
2566
2085
  `${dual.oauthLabel} credential is no longer valid \u2014 falling back to the ${dual.apiKeyLabel}. ${dual.restoreHint}`
2567
2086
  );
2568
- return this.data[dual.provider];
2087
+ return dualApiKey;
2569
2088
  }
2570
2089
  throw err;
2571
2090
  }
2572
2091
  }
2573
- const creds = this.data[provider];
2092
+ const creds = usableEntry(this.data, provider);
2574
2093
  if (!creds) {
2575
2094
  throw new NotLoggedInError(provider);
2576
2095
  }
2577
- if (STATIC_API_KEY_PROVIDERS.has(provider)) {
2096
+ if (!REFRESHABLE_STORAGE_KEYS.has(provider)) {
2578
2097
  if (provider === "minimax" && !creds.baseUrl) return await this.withMiniMaxRegion(creds);
2098
+ if (provider === KIMI_CODE_KEY && !creds.baseUrl) return await this.withKimiRegion(creds);
2579
2099
  return creds;
2580
2100
  }
2581
2101
  if (!opts?.forceRefresh && Date.now() < creds.expiresAt - refreshThresholdMs(creds)) {
@@ -2603,13 +2123,14 @@ var AuthStorage = class {
2603
2123
  this.data = latest;
2604
2124
  return latestCreds;
2605
2125
  }
2606
- const refreshFn = provider === "gemini" ? refreshGeminiToken : provider === MOONSHOT_OAUTH_KEY ? refreshKimiToken : provider === XAI_OAUTH_KEY ? refreshXaiToken : refreshOpenAIToken;
2607
2126
  let refreshed;
2608
2127
  try {
2609
- refreshed = await refreshFn(latestCreds.refreshToken);
2128
+ refreshed = await refreshChatGPTToken(latestCreds);
2610
2129
  } catch (err) {
2611
2130
  const msg = err instanceof Error ? err.message : String(err);
2612
- const isAuthFailure = /\((401|400)\)/.test(msg) || /invalid_grant|invalid_token|invalid.*refresh/i.test(msg) || /unauthorized/i.test(msg);
2131
+ const isAuthFailure = /\((401|400)\)/.test(msg) || /invalid_grant|invalid_token|invalid.*refresh|token_expired|refresh_token_(?:expired|invalidated|reused)/i.test(
2132
+ msg
2133
+ ) || /unauthorized/i.test(msg);
2613
2134
  if (isAuthFailure) {
2614
2135
  const current = await readAuthData(this.filePath);
2615
2136
  const currentCreds = current[provider];
@@ -2629,15 +2150,10 @@ var AuthStorage = class {
2629
2150
  }
2630
2151
  throw err;
2631
2152
  }
2632
- if (!refreshed.accountId && latestCreds.accountId) {
2633
- refreshed.accountId = latestCreds.accountId;
2634
- }
2635
- if (!refreshed.projectId && latestCreds.projectId) {
2636
- refreshed.projectId = latestCreds.projectId;
2637
- }
2638
- if (!refreshed.baseUrl && latestCreds.baseUrl) {
2639
- refreshed.baseUrl = latestCreds.baseUrl;
2640
- }
2153
+ refreshed = {
2154
+ ...pick(latestCreds, ["accountId", "baseUrl", "clientId", "idToken", "scopes"]),
2155
+ ...refreshed
2156
+ };
2641
2157
  latest[provider] = refreshed;
2642
2158
  this.data = latest;
2643
2159
  await atomicWriteFile(this.filePath, JSON.stringify(latest, null, 2));
@@ -2660,9 +2176,14 @@ var AuthStorage = class {
2660
2176
  return creds.accessToken;
2661
2177
  }
2662
2178
  };
2179
+ function pick(value, keys) {
2180
+ const out = {};
2181
+ for (const key of keys) if (value[key] !== void 0) out[key] = value[key];
2182
+ return out;
2183
+ }
2663
2184
  async function readAuthData(filePath) {
2664
2185
  try {
2665
- const content = await import_promises5.default.readFile(filePath, "utf-8");
2186
+ const content = await import_promises6.default.readFile(filePath, "utf-8");
2666
2187
  return JSON.parse(content);
2667
2188
  } catch (error) {
2668
2189
  if (error.code === "ENOENT") return {};
@@ -2670,12 +2191,12 @@ async function readAuthData(filePath) {
2670
2191
  }
2671
2192
  }
2672
2193
  async function atomicWriteFile(filePath, content) {
2673
- const tmpPath = `${filePath}.${process.pid}.${Date.now()}.${import_node_crypto6.default.randomUUID().slice(0, 8)}.tmp`;
2194
+ const tmpPath = `${filePath}.${process.pid}.${Date.now()}.${import_node_crypto4.default.randomUUID().slice(0, 8)}.tmp`;
2674
2195
  try {
2675
- await import_promises5.default.writeFile(tmpPath, content, { encoding: "utf-8", mode: 384 });
2676
- await import_promises5.default.rename(tmpPath, filePath);
2196
+ await import_promises6.default.writeFile(tmpPath, content, { encoding: "utf-8", mode: 384 });
2197
+ await import_promises6.default.rename(tmpPath, filePath);
2677
2198
  } catch (err) {
2678
- await import_promises5.default.unlink(tmpPath).catch(() => {
2199
+ await import_promises6.default.unlink(tmpPath).catch(() => {
2679
2200
  });
2680
2201
  throw err;
2681
2202
  }
@@ -2707,7 +2228,8 @@ var CODEX_FAST_BILLING = {
2707
2228
  detail: "Codex Fast mode uses your ChatGPT plan limits at 2.5x the standard rate (purchased credits at 2x).",
2708
2229
  paid: false
2709
2230
  };
2710
- var GEMINI_AUTH_KEYS = [GEMINI_API_KEY, "gemini"];
2231
+ var GEMINI_AUTH_KEYS = [GEMINI_API_KEY];
2232
+ var KIMI_AUTH_KEYS = [KIMI_CODE_KEY, "moonshot"];
2711
2233
  var MODELS = [
2712
2234
  // ── Anthropic ──────────────────────────────────────────
2713
2235
  // NOTE: Claude Mythos 5.1 (`claude-mythos-5-1`, 2026-09-01) is kept commented
@@ -2837,14 +2359,13 @@ var MODELS = [
2837
2359
  {
2838
2360
  // GPT-6 Astra — "Our most capable model for complex, demanding work."
2839
2361
  // (Codex catalog priority 2, listed for every ChatGPT plan, requires a
2840
- // Codex client >= 0.153.0 — see CODEX_CLIENT_VERSION). Same split as 5.6:
2841
- // 1.05M on the public Responses API, 272K on the ChatGPT OAuth route
2362
+ // Codex client >= 0.153.0). Same split as 5.6:
2363
+ // 1.05M on the public Responses API, 272K on the ChatGPT plan route
2842
2364
  // (openai/codex models.json, `gpt-6-astra`). Reasoning ladder low → medium
2843
2365
  // → high → xhigh → max → ultra; `ultra` is the Codex orchestration preset
2844
2366
  // (multi_agent v2) and is Codex-only — the public API tops out at `max`.
2845
- // Note: through a plain API key OpenAI requires the Responses API for tool
2846
- // calling on Astra, so the Chat Completions path is text-only; the OAuth
2847
- // Codex route is the supported way to use it as an agent.
2367
+ // OpenAI requires the Responses API for tool calling on Astra, so both
2368
+ // the ChatGPT plan and API keys ride gg-ai's Responses transport.
2848
2369
  id: "gpt-6-astra",
2849
2370
  name: "GPT-6 Astra",
2850
2371
  provider: "openai",
@@ -2887,9 +2408,7 @@ var MODELS = [
2887
2408
  // next start.
2888
2409
  {
2889
2410
  // GPT-6.1 Sol — "Latest workhorse model for coding and everyday work."
2890
- // (Codex priority 1, default low). The catalog says Codex client >= 0.153.0,
2891
- // but the ChatGPT backend only serves it from 0.159.0 (see
2892
- // CODEX_CLIENT_VERSION). $2/$10 MTok, cached input $0.10. Ladder low →
2411
+ // (Codex priority 1, default low). $2/$10 MTok, cached input $0.10. Ladder low →
2893
2412
  // medium → high → xhigh → max → ultra; ultra is the Codex orchestration
2894
2413
  // preset (xhigh effort on the wire + proactive local subagent delegation).
2895
2414
  id: "gpt-6.1-sol",
@@ -3076,8 +2595,8 @@ var MODELS = [
3076
2595
  maxThinkingLevel: "high",
3077
2596
  authStorageKeys: GEMINI_AUTH_KEYS
3078
2597
  },
3079
- // Keep 3.1 Flash Lite first for the working OAuth default and fast-model routing.
3080
- // New GA models are opt-in; Code Assist access varies by account. Gemini 3.7
2598
+ // Keep 3.1 Flash Lite first for the default and fast-model routing.
2599
+ // Gemini 3.7
3081
2600
  // Flash is retired: Google deprecated it on 2026-10-08 and routes every
3082
2601
  // `gemini-3.7-flash` request to 3.8 Flash (ai.google.dev/gemini-api/docs/
3083
2602
  // changelog), so a saved session on it falls back to the provider default.
@@ -3110,9 +2629,7 @@ var MODELS = [
3110
2629
  authStorageKeys: GEMINI_AUTH_KEYS
3111
2630
  },
3112
2631
  {
3113
- // Wire name `gemini-3-flash` — the Code Assist (OAuth) backend rejects the
3114
- // display string `gemini-3.5-flash` with a 404, so gemini-cli keeps this
3115
- // alternative name (SECONDARY_GEMINI_3_5_FLASH_MODEL) for that endpoint.
2632
+ // Wire name `gemini-3-flash`, the id Google serves 3.5 Flash under.
3116
2633
  id: "gemini-3-flash",
3117
2634
  name: "Gemini 3.5 Flash",
3118
2635
  provider: "gemini",
@@ -3127,9 +2644,7 @@ var MODELS = [
3127
2644
  authStorageKeys: GEMINI_AUTH_KEYS
3128
2645
  },
3129
2646
  {
3130
- // Gemini 3.1 Pro is public preview — gated behind Code Assist preview
3131
- // enablement, so free/personal OAuth accounts 404 on it (see
3132
- // ACCOUNT_GATED_MODELS in gg-ai's gemini provider).
2647
+ // Gemini 3.1 Pro is public preview.
3133
2648
  id: "gemini-3.1-pro-preview",
3134
2649
  name: "Gemini 3.1 Pro (Preview)",
3135
2650
  provider: "gemini",
@@ -3146,8 +2661,8 @@ var MODELS = [
3146
2661
  // ── Moonshot (Kimi) ────────────────────────────────────
3147
2662
  // K3 is Kimi's 2.8T-parameter flagship for long-horizon coding, knowledge
3148
2663
  // work, and deep reasoning. Its effort ladder is server-declared as
3149
- // low/high/max on both the public API (default max) and the Kimi For Coding
3150
- // OAuth endpoint (default high); thinking can also be fully disabled.
2664
+ // low/high/max on both the public API (default max) and the Kimi Code plan
2665
+ // endpoint (default high); thinking can also be fully disabled.
3151
2666
  {
3152
2667
  id: "kimi-k3",
3153
2668
  name: "Kimi K3",
@@ -3161,13 +2676,14 @@ var MODELS = [
3161
2676
  supportsVideo: true,
3162
2677
  maxVideoBytes: 100 * 1024 * 1024,
3163
2678
  costTier: "high",
3164
- maxThinkingLevel: "max"
2679
+ maxThinkingLevel: "max",
2680
+ authStorageKeys: KIMI_AUTH_KEYS
3165
2681
  },
3166
- // K2.8 Preview (2026-09-11) is served only on the Kimi For Coding OAuth
3167
- // endpoint, under its rolling `kimi-for-coding` id (live /models, 2026-09-28:
2682
+ // K2.8 Preview (2026-09-11) is served only on the Kimi Code plan endpoint,
2683
+ // under its rolling `kimi-for-coding` id (live /models, 2026-09-28:
3168
2684
  // display_name "K2.8 Preview", 1M context, image + video input, efforts
3169
- // low/high/max default max). The public API-key endpoint does not serve it,
3170
- // so it resolves from the Kimi sign-in credential only.
2685
+ // low/high/max default max). The public Moonshot API does not serve it, so
2686
+ // it resolves from a Kimi Code plan key only.
3171
2687
  {
3172
2688
  id: "kimi-for-coding",
3173
2689
  name: "Kimi K2.8 Preview",
@@ -3180,7 +2696,7 @@ var MODELS = [
3180
2696
  maxVideoBytes: 100 * 1024 * 1024,
3181
2697
  costTier: "medium",
3182
2698
  maxThinkingLevel: "max",
3183
- authStorageKeys: [MOONSHOT_OAUTH_KEY]
2699
+ authStorageKeys: [KIMI_CODE_KEY]
3184
2700
  },
3185
2701
  // K2.7 Code is requested by its pinned id (not the `kimi-for-coding` alias
3186
2702
  // that moved to K2.8), so it stays the real K2.7 on both endpoints.
@@ -3195,7 +2711,8 @@ var MODELS = [
3195
2711
  supportsVideo: true,
3196
2712
  maxVideoBytes: 100 * 1024 * 1024,
3197
2713
  costTier: "medium",
3198
- maxThinkingLevel: "high"
2714
+ maxThinkingLevel: "high",
2715
+ authStorageKeys: KIMI_AUTH_KEYS
3199
2716
  },
3200
2717
  // K2.7 Code HighSpeed — the same model as K2.7 Code served at ~180 tok/s
3201
2718
  // (up to ~260 on short contexts), 256K context (platform.kimi.ai model list,
@@ -3214,7 +2731,8 @@ var MODELS = [
3214
2731
  supportsVideo: true,
3215
2732
  maxVideoBytes: 100 * 1024 * 1024,
3216
2733
  costTier: "medium",
3217
- maxThinkingLevel: "high"
2734
+ maxThinkingLevel: "high",
2735
+ authStorageKeys: KIMI_AUTH_KEYS
3218
2736
  },
3219
2737
  // ── Z.AI (GLM) ─────────────────────────────────────────
3220
2738
  // Two GLM entries, both live on the coding endpoint (verified against its
@@ -3816,12 +3334,12 @@ function clampThinkingForPlanMode(level) {
3816
3334
  }
3817
3335
 
3818
3336
  // src/local-models.ts
3819
- var import_gg_ai3 = require("@kenkaiiii/gg-ai");
3337
+ var import_gg_ai4 = require("@kenkaiiii/gg-ai");
3820
3338
  var DEFAULT_LOCAL_ENDPOINTS = [
3821
3339
  { id: "ollama", label: "Ollama", baseUrl: "http://127.0.0.1:11434/v1", kind: "ollama" }
3822
3340
  ];
3823
3341
  var FALLBACK_CONTEXT_WINDOW = 4096;
3824
- var LOCAL_API_KEY_PLACEHOLDER = import_gg_ai3.LOCAL_NO_API_KEY;
3342
+ var LOCAL_API_KEY_PLACEHOLDER = import_gg_ai4.LOCAL_NO_API_KEY;
3825
3343
  var DEFAULT_PROBE_TIMEOUT_MS = 1200;
3826
3344
  var ENRICH_CONCURRENCY = 6;
3827
3345
  var CACHE_TTL_MS = 3e4;
@@ -3850,7 +3368,7 @@ function endpointRoot(baseUrl) {
3850
3368
  return baseUrl.replace(/\/+$/, "").replace(/\/v1$/, "");
3851
3369
  }
3852
3370
  function authHeaders(endpoint) {
3853
- const key = (0, import_gg_ai3.localBearerKey)(endpoint.apiKey);
3371
+ const key = (0, import_gg_ai4.localBearerKey)(endpoint.apiKey);
3854
3372
  return {
3855
3373
  ...key ? { Authorization: `Bearer ${key}` } : {},
3856
3374
  Accept: "application/json"
@@ -4141,7 +3659,7 @@ function findProbedModel(probes, modelId) {
4141
3659
  }
4142
3660
 
4143
3661
  // src/claude-code/usage.ts
4144
- var import_promises6 = __toESM(require("fs/promises"), 1);
3662
+ var import_promises7 = __toESM(require("fs/promises"), 1);
4145
3663
  var import_node_path5 = __toESM(require("path"), 1);
4146
3664
  var USAGE_FILE = "usage.json";
4147
3665
  var WINDOWS = {
@@ -4177,7 +3695,7 @@ function usagePath(opts) {
4177
3695
  async function readClaudeCodeUsage(opts = {}) {
4178
3696
  let parsed;
4179
3697
  try {
4180
- parsed = JSON.parse(await import_promises6.default.readFile(usagePath(opts), "utf8"));
3698
+ parsed = JSON.parse(await import_promises7.default.readFile(usagePath(opts), "utf8"));
4181
3699
  } catch {
4182
3700
  return null;
4183
3701
  }
@@ -4195,8 +3713,8 @@ function recordClaudeCodeRateLimit(info, opts = {}) {
4195
3713
  const next = mergeClaudeCodeRateLimit(info, await readClaudeCodeUsage({ ...opts, now }), now);
4196
3714
  if (next.windows.length === 0) return;
4197
3715
  const file = usagePath(opts);
4198
- await import_promises6.default.mkdir(import_node_path5.default.dirname(file), { recursive: true });
4199
- await import_promises6.default.writeFile(file, JSON.stringify(next), "utf8");
3716
+ await import_promises7.default.mkdir(import_node_path5.default.dirname(file), { recursive: true });
3717
+ await import_promises7.default.writeFile(file, JSON.stringify(next), "utf8");
4200
3718
  };
4201
3719
  writeQueue = writeQueue.then(write).catch(() => {
4202
3720
  });
@@ -4204,7 +3722,7 @@ function recordClaudeCodeRateLimit(info, opts = {}) {
4204
3722
  }
4205
3723
  async function clearClaudeCodeUsage(opts = {}) {
4206
3724
  await writeQueue;
4207
- await import_promises6.default.rm(usagePath(opts), { force: true }).catch(() => {
3725
+ await import_promises7.default.rm(usagePath(opts), { force: true }).catch(() => {
4208
3726
  });
4209
3727
  }
4210
3728
 
@@ -4245,6 +3763,9 @@ async function signOutOfClaudeCode(auth, opts = {}) {
4245
3763
  }
4246
3764
 
4247
3765
  // src/auth-providers.ts
3766
+ function apiKeyDetails(label, billing, when = "") {
3767
+ return { apikey: { method: "apikey", label, billing, when } };
3768
+ }
4248
3769
  var ALL_AUTH_PROVIDERS = [
4249
3770
  {
4250
3771
  value: "anthropic",
@@ -4272,89 +3793,103 @@ var ALL_AUTH_PROVIDERS = [
4272
3793
  value: "openai",
4273
3794
  label: "OpenAI",
4274
3795
  description: "GPT-6 Astra, GPT-6 Astra Fast, GPT-6.1 Sol, GPT-6.1 Sol Fast, GPT-6.1 Terra, GPT-6.1 Terra Fast, GPT-6 Luna, GPT-6 Luna Fast",
4275
- methods: ["oauth"]
3796
+ methods: ["oauth", "apikey"],
3797
+ apiKeyLabel: "OpenAI",
3798
+ methodDetails: {
3799
+ oauth: {
3800
+ method: "oauth",
3801
+ label: "Continue with ChatGPT",
3802
+ billing: "Uses your ChatGPT plan.",
3803
+ when: "Signs in through OpenAI. Manage what GG Coder may use at chatgpt.com/settings/usage.",
3804
+ requires: "A ChatGPT plan."
3805
+ },
3806
+ apikey: {
3807
+ method: "apikey",
3808
+ label: "OpenAI API key",
3809
+ billing: "Pay-per-token from an OpenAI Platform key.",
3810
+ when: "No subscription needed. Covers you while your ChatGPT plan's usage is out."
3811
+ }
3812
+ }
4276
3813
  },
4277
3814
  {
4278
3815
  value: "gemini",
4279
3816
  label: "Gemini",
4280
3817
  description: "Gemini 3.8 Flash, 3.5 Flash Lite, 3.1 Flash Lite, 3.5 Flash, 3.1 Pro (Preview)",
4281
- methods: ["oauth", "apikey"],
3818
+ methods: ["apikey"],
4282
3819
  apiKeyLabel: "Gemini",
4283
3820
  apiKeyBaseUrl: GEMINI_API_KEY_BASE_URL,
4284
- // Stored under its own key so it never overwrites the `gemini` OAuth login.
3821
+ // Stored under its own key: `gemini` held the retired Google sign-in.
4285
3822
  apiKeyVariants: [
4286
- { key: GEMINI_API_KEY, label: "Google AI Studio", baseUrl: GEMINI_API_KEY_BASE_URL }
4287
- ],
4288
- methodDetails: {
4289
- oauth: {
4290
- method: "oauth",
4291
- label: "Sign in with Google",
4292
- billing: "Gemini Code Assist Standard or Enterprise licence.",
4293
- when: "Personal Google accounts are no longer served here (Google ended it on June 18, 2026)."
4294
- },
4295
- apikey: {
4296
- method: "apikey",
4297
- label: "Gemini API key",
3823
+ {
3824
+ key: GEMINI_API_KEY,
3825
+ label: "Google AI Studio",
3826
+ kind: "payg",
4298
3827
  billing: "Pay-per-token from a Google AI Studio key.",
4299
- when: "Used first whenever it is set."
3828
+ baseUrl: GEMINI_API_KEY_BASE_URL
4300
3829
  }
4301
- }
3830
+ ],
3831
+ methodDetails: apiKeyDetails("Gemini API key", "Pay-per-token from a Google AI Studio key.")
4302
3832
  },
4303
3833
  {
4304
3834
  value: "xai",
4305
3835
  label: "xAI (Grok)",
4306
- description: "Grok 4.7 \xB7 OAuth or API key",
4307
- methods: ["oauth", "apikey"],
3836
+ description: "Grok 4.7",
3837
+ methods: ["apikey"],
4308
3838
  apiKeyLabel: "xAI",
4309
- methodDetails: {
4310
- oauth: {
4311
- method: "oauth",
4312
- label: "Sign in with Grok",
4313
- billing: "Included with SuperGrok or X Premium.",
4314
- when: ""
4315
- },
4316
- apikey: {
4317
- method: "apikey",
4318
- label: "xAI API key",
4319
- billing: "Pay-per-token on console.x.ai credits.",
4320
- when: ""
4321
- }
4322
- }
3839
+ methodDetails: apiKeyDetails(
3840
+ "xAI API key",
3841
+ "Pay-per-token from console.x.ai credits.",
3842
+ "SuperGrok and X Premium usage is only offered in apps xAI partners with."
3843
+ )
4323
3844
  },
4324
3845
  {
4325
3846
  value: "moonshot",
4326
- label: "Moonshot",
4327
- description: "Kimi K3, K2.8 Preview (Kimi sign-in), K2.7 Code, K2.7 Code HighSpeed \xB7 OAuth or API key",
4328
- methods: ["oauth", "apikey"],
4329
- apiKeyLabel: "Moonshot",
4330
- methodDetails: {
4331
- oauth: {
4332
- method: "oauth",
4333
- label: "Sign in with Kimi",
4334
- billing: "Included with a Kimi For Coding plan.",
4335
- when: ""
3847
+ label: "Kimi (Moonshot)",
3848
+ description: "Kimi K3, K2.8 Preview (Kimi Code plan), K2.7 Code, K2.7 Code HighSpeed",
3849
+ methods: ["apikey"],
3850
+ apiKeyLabel: "Kimi",
3851
+ apiKeyVariants: [
3852
+ {
3853
+ key: KIMI_CODE_KEY,
3854
+ label: "Kimi Code plan",
3855
+ kind: "plan",
3856
+ billing: "Uses your Kimi Code plan. Create the key in the Kimi Code Console."
4336
3857
  },
4337
- apikey: {
4338
- method: "apikey",
4339
- label: "Moonshot API key",
4340
- billing: "Pay-per-token on Moonshot credits.",
4341
- when: ""
3858
+ {
3859
+ key: "moonshot",
3860
+ label: "Moonshot API",
3861
+ kind: "payg",
3862
+ billing: "Pay-per-token from platform.moonshot.ai."
4342
3863
  }
4343
- }
3864
+ ],
3865
+ methodDetails: apiKeyDetails(
3866
+ "Kimi API key",
3867
+ "A Kimi Code plan key uses your plan; a Moonshot key is pay-per-token."
3868
+ )
4344
3869
  },
4345
3870
  {
4346
3871
  value: "glm",
4347
3872
  label: "Z.AI (GLM)",
4348
3873
  description: "GLM-5.3, GLM-5.3-Flash",
4349
3874
  methods: ["apikey"],
4350
- apiKeyLabel: "Z.AI"
3875
+ apiKeyLabel: "Z.AI",
3876
+ methodDetails: apiKeyDetails(
3877
+ "Z.AI API key",
3878
+ "Pay-per-token from your Z.AI account balance.",
3879
+ "GLM Coding Plan quota can't be used here: Z.AI limits the plan to the tools it lists."
3880
+ )
4351
3881
  },
4352
3882
  {
4353
3883
  value: "minimax",
4354
3884
  label: "MiniMax",
4355
3885
  description: "MiniMax M3.1 Flash, M3",
4356
3886
  methods: ["apikey"],
4357
- apiKeyLabel: "MiniMax"
3887
+ apiKeyLabel: "MiniMax",
3888
+ methodDetails: apiKeyDetails(
3889
+ "MiniMax API key",
3890
+ "An M Plan key uses your plan; a pay-as-you-go key is pay-per-token.",
3891
+ "The key you paste decides which one is billed."
3892
+ )
4358
3893
  },
4359
3894
  {
4360
3895
  value: "xiaomi",
@@ -4367,44 +3902,66 @@ var ALL_AUTH_PROVIDERS = [
4367
3902
  {
4368
3903
  key: "xiaomi",
4369
3904
  label: "Token Plan",
3905
+ kind: "plan",
3906
+ billing: "Uses your MiMo Token Plan.",
4370
3907
  baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1"
4371
3908
  },
4372
3909
  {
4373
3910
  key: XIAOMI_CREDITS_KEY,
4374
- label: "API Credits (required for UltraSpeed)",
3911
+ label: "API Credits",
3912
+ kind: "payg",
3913
+ billing: "Pay-per-token from MiMo API Credits. Required for UltraSpeed.",
4375
3914
  baseUrl: "https://api.xiaomimimo.com/v1"
4376
3915
  }
4377
- ]
3916
+ ],
3917
+ methodDetails: apiKeyDetails(
3918
+ "Xiaomi MiMo API key",
3919
+ "A Token Plan key uses your plan; an API Credits key is pay-per-token."
3920
+ )
4378
3921
  },
4379
3922
  {
4380
3923
  value: "deepseek",
4381
3924
  label: "DeepSeek",
4382
3925
  description: "DeepSeek V4 Pro, V4.1 Flash",
4383
3926
  methods: ["apikey"],
4384
- apiKeyLabel: "DeepSeek"
3927
+ apiKeyLabel: "DeepSeek",
3928
+ methodDetails: apiKeyDetails("DeepSeek API key", "Pay-per-token from platform.deepseek.com.")
4385
3929
  },
4386
3930
  {
4387
3931
  value: "sakana",
4388
3932
  label: "Sakana (Fugu)",
4389
3933
  description: "Fugu, Fugu Max, Fugu Ultra",
4390
3934
  methods: ["apikey"],
4391
- apiKeyLabel: "Sakana"
3935
+ apiKeyLabel: "Sakana",
3936
+ methodDetails: apiKeyDetails("Sakana API key", "Pay-per-token from your Sakana API account.")
4392
3937
  },
4393
3938
  {
4394
3939
  value: "openrouter",
4395
3940
  label: "OpenRouter",
4396
3941
  description: "Qwen3.8 Max \xB7 multi-provider gateway",
4397
3942
  methods: ["apikey"],
4398
- apiKeyLabel: "OpenRouter"
3943
+ apiKeyLabel: "OpenRouter",
3944
+ methodDetails: apiKeyDetails("OpenRouter API key", "Pay-per-token from OpenRouter credits.")
4399
3945
  },
4400
3946
  {
4401
3947
  value: "huggingface",
4402
3948
  label: "Hugging Face",
4403
3949
  description: "Kimi K2.7 Code, DeepSeek V4.1 Flash, GPT-OSS 120B",
4404
3950
  methods: ["apikey"],
4405
- apiKeyLabel: "Hugging Face"
3951
+ apiKeyLabel: "Hugging Face",
3952
+ methodDetails: apiKeyDetails(
3953
+ "Hugging Face token",
3954
+ "Pay-per-token from Hugging Face Inference Providers credits."
3955
+ )
4406
3956
  }
4407
3957
  ];
3958
+ function requiredKeyLabel(provider, storageKeys) {
3959
+ const variants = ALL_AUTH_PROVIDERS.find((p) => p.value === provider)?.apiKeyVariants ?? [];
3960
+ const needed = variants.filter((v) => storageKeys.includes(v.key));
3961
+ if (needed.length === 0 || needed.length === variants.length) return void 0;
3962
+ const names = needed.map((v) => v.label).join(" or ");
3963
+ return `Needs ${/^[aeiou]/i.test(names) ? "an" : "a"} ${names} key`;
3964
+ }
4408
3965
  var AUTH_PROVIDERS = ALL_AUTH_PROVIDERS.filter(
4409
3966
  (p) => !isHiddenProvider(p.value)
4410
3967
  );
@@ -4431,18 +3988,24 @@ function describeAuthMethods(provider) {
4431
3988
  });
4432
3989
  }
4433
3990
  function authPriorityNote(provider) {
4434
- if (provider === "gemini") {
4435
- return "Uses the Gemini API key first when one is set; otherwise the Google sign-in.";
4436
- }
4437
3991
  if (provider === "anthropic") {
4438
3992
  return "Uses Sign in with Claude while you are signed in; otherwise the Anthropic API key.";
4439
3993
  }
4440
3994
  const dual = dualAuthProvider(provider);
4441
- if (!dual) return void 0;
4442
- return `Uses ${dual.oauthLabel} first; the ${dual.apiKeyLabel} takes over when it runs out, then switches back.`;
3995
+ if (dual) {
3996
+ return `Uses your ${dual.oauthLabel} first; the ${dual.apiKeyLabel} takes over while its usage is out, then switches back.`;
3997
+ }
3998
+ const variants = getAuthProvider(provider)?.apiKeyVariants;
3999
+ const plan = variants?.find((v) => v.kind === "plan");
4000
+ const payg = variants?.find((v) => v.kind === "payg");
4001
+ if (plan && payg) {
4002
+ return `Uses the ${plan.label} key first when both are set; the ${payg.label} key covers models the plan doesn't include.`;
4003
+ }
4004
+ return void 0;
4443
4005
  }
4444
4006
 
4445
4007
  // src/provider-usage.ts
4008
+ var import_gg_ai5 = require("@kenkaiiii/gg-ai");
4446
4009
  var SubscriptionUsageError = class extends Error {
4447
4010
  constructor(message, status, retryAfterMs2) {
4448
4011
  super(message);
@@ -4451,51 +4014,14 @@ var SubscriptionUsageError = class extends Error {
4451
4014
  this.name = "SubscriptionUsageError";
4452
4015
  }
4453
4016
  };
4454
- var CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
4455
4017
  function finiteNumber(value) {
4456
4018
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
4457
4019
  }
4458
- function clampPercent(value) {
4459
- const number = finiteNumber(value);
4460
- return number === void 0 ? void 0 : Math.min(100, Math.max(0, number));
4461
- }
4462
4020
  function isoTimestamp(value) {
4463
4021
  if (typeof value !== "string" || !value.trim()) return void 0;
4464
4022
  const timestamp = Date.parse(value);
4465
4023
  return Number.isFinite(timestamp) ? timestamp : void 0;
4466
4024
  }
4467
- function unixTimestamp(value) {
4468
- const seconds = finiteNumber(value);
4469
- return seconds === void 0 ? void 0 : seconds * 1e3;
4470
- }
4471
- function codexResetAt(window, now) {
4472
- const absolute = unixTimestamp(window.reset_at);
4473
- if (absolute !== void 0) return absolute;
4474
- const afterSeconds = finiteNumber(window.reset_after_seconds);
4475
- return afterSeconds === void 0 ? void 0 : now + afterSeconds * 1e3;
4476
- }
4477
- function currentWindowLabel(seconds, fallbackHours) {
4478
- const duration = finiteNumber(seconds);
4479
- const hours = Math.max(1, Math.round((duration ?? fallbackHours * 3600) / 3600));
4480
- return `${hours}-hour`;
4481
- }
4482
- function codexWindowKind(window, fallback) {
4483
- const duration = finiteNumber(window.limit_window_seconds);
4484
- if (duration === void 0) return fallback;
4485
- return duration >= 6 * 24 * 60 * 60 ? "weekly" : "current";
4486
- }
4487
- function normalizedCodexWindow(window, fallbackKind, now) {
4488
- if (!window) return null;
4489
- const usedPercent = clampPercent(window.used_percent);
4490
- if (usedPercent === void 0) return null;
4491
- const kind = codexWindowKind(window, fallbackKind);
4492
- return {
4493
- kind,
4494
- label: kind === "weekly" ? "Weekly" : currentWindowLabel(window.limit_window_seconds, 5),
4495
- usedPercent,
4496
- resetsAt: codexResetAt(window, now)
4497
- };
4498
- }
4499
4025
  function retryAfterMs(response, now) {
4500
4026
  const value = response.headers.get("retry-after")?.trim();
4501
4027
  if (!value) return void 0;
@@ -4518,31 +4044,6 @@ async function readUsageResponse(response, now) {
4518
4044
  throw new SubscriptionUsageError("Subscription usage response was not valid JSON");
4519
4045
  }
4520
4046
  }
4521
- async function fetchCodexUsage(credentials, fetchFn, signal, now) {
4522
- const headers = {
4523
- Authorization: `Bearer ${credentials.accessToken}`,
4524
- Accept: "application/json",
4525
- originator: "ggcoder",
4526
- "User-Agent": "ggcoder"
4527
- };
4528
- if (credentials.accountId) headers["ChatGPT-Account-Id"] = credentials.accountId;
4529
- const response = await fetchFn(CODEX_USAGE_URL, { method: "GET", signal, headers });
4530
- const data = await readUsageResponse(response, now());
4531
- const windows = [
4532
- normalizedCodexWindow(data.rate_limit?.primary_window, "current", now()),
4533
- normalizedCodexWindow(data.rate_limit?.secondary_window, "weekly", now())
4534
- ].filter((window) => window !== null);
4535
- windows.sort((left, right) => {
4536
- if (left.kind === right.kind) return 0;
4537
- return left.kind === "current" ? -1 : 1;
4538
- });
4539
- return {
4540
- provider: "openai",
4541
- displayName: "Codex",
4542
- windows,
4543
- fetchedAt: now()
4544
- };
4545
- }
4546
4047
  function kimiNumber(value) {
4547
4048
  if (typeof value === "string" && value.trim()) {
4548
4049
  const parsed = Number(value);
@@ -4573,15 +4074,14 @@ function kimiWindowMinutes(window) {
4573
4074
  }
4574
4075
  }
4575
4076
  async function fetchKimiUsage(credentials, fetchFn, signal, now) {
4576
- const response = await fetchFn(`${kimiCodeBaseUrl()}/usages`, {
4077
+ const baseUrl = (credentials.baseUrl ?? KIMI_CODE_DEFAULT_BASE_URL).replace(/\/+$/, "");
4078
+ const response = await fetchFn(`${baseUrl}/usages`, {
4577
4079
  method: "GET",
4578
4080
  signal,
4579
4081
  headers: {
4580
4082
  Authorization: "Bearer " + credentials.accessToken,
4581
4083
  Accept: "application/json",
4582
- // The managed coding endpoint gates on the kimi-code-cli client identity,
4583
- // same as model requests.
4584
- ...kimiCodingHeaders()
4084
+ "User-Agent": import_gg_ai5.GG_CODER_USER_AGENT
4585
4085
  }
4586
4086
  });
4587
4087
  const data = await readUsageResponse(response, now());
@@ -4633,7 +4133,7 @@ async function fetchSubscriptionUsage(provider, credentials, options = {}) {
4633
4133
  const now = options.now ?? Date.now;
4634
4134
  const signal = AbortSignal.timeout(options.timeoutMs ?? 8e3);
4635
4135
  try {
4636
- return provider === "openai" ? await fetchCodexUsage(credentials, fetchFn, signal, now) : await fetchKimiUsage(credentials, fetchFn, signal, now);
4136
+ return await fetchKimiUsage(credentials, fetchFn, signal, now);
4637
4137
  } catch (error) {
4638
4138
  if (error instanceof SubscriptionUsageError) throw error;
4639
4139
  const message = error instanceof Error ? error.message : String(error);
@@ -4698,7 +4198,7 @@ var TelegramBot = class {
4698
4198
  } catch (err) {
4699
4199
  if (!this.running) break;
4700
4200
  console.error(`[telegram] Poll error: ${err instanceof Error ? err.message : err}`);
4701
- await sleep3(3e3);
4201
+ await sleep(3e3);
4702
4202
  }
4703
4203
  }
4704
4204
  }
@@ -4884,7 +4384,7 @@ function splitMessage(text) {
4884
4384
  }
4885
4385
  return chunks;
4886
4386
  }
4887
- function sleep3(ms) {
4387
+ function sleep(ms) {
4888
4388
  return new Promise((r) => setTimeout(r, ms));
4889
4389
  }
4890
4390
 
@@ -4969,8 +4469,8 @@ async function transcribeVoice(fileUrl) {
4969
4469
  }
4970
4470
 
4971
4471
  // src/auto-update.ts
4972
- var import_node_child_process3 = require("child_process");
4973
- var import_node_fs6 = __toESM(require("fs"), 1);
4472
+ var import_node_child_process2 = require("child_process");
4473
+ var import_node_fs5 = __toESM(require("fs"), 1);
4974
4474
  var import_node_path6 = __toESM(require("path"), 1);
4975
4475
  var CHECK_INTERVAL_MS = 60 * 60 * 1e3;
4976
4476
  var FETCH_TIMEOUT_MS = 1e4;
@@ -4986,7 +4486,7 @@ function compareVersions(a, b) {
4986
4486
  function performUpdateInBackground(command) {
4987
4487
  try {
4988
4488
  const parts = command.split(" ");
4989
- const child = (0, import_node_child_process3.spawn)(parts[0], parts.slice(1), {
4489
+ const child = (0, import_node_child_process2.spawn)(parts[0], parts.slice(1), {
4990
4490
  detached: true,
4991
4491
  stdio: "ignore",
4992
4492
  env: { ...process.env, npm_config_loglevel: "silent" }
@@ -5004,7 +4504,7 @@ function createAutoUpdater(config) {
5004
4504
  }
5005
4505
  function readState() {
5006
4506
  try {
5007
- const raw = import_node_fs6.default.readFileSync(stateFilePath(), "utf-8");
4507
+ const raw = import_node_fs5.default.readFileSync(stateFilePath(), "utf-8");
5008
4508
  return JSON.parse(raw);
5009
4509
  } catch {
5010
4510
  return null;
@@ -5013,8 +4513,8 @@ function createAutoUpdater(config) {
5013
4513
  function writeState(state) {
5014
4514
  try {
5015
4515
  const filePath = stateFilePath();
5016
- import_node_fs6.default.mkdirSync(import_node_path6.default.dirname(filePath), { recursive: true, mode: 448 });
5017
- import_node_fs6.default.writeFileSync(filePath, JSON.stringify(state));
4516
+ import_node_fs5.default.mkdirSync(import_node_path6.default.dirname(filePath), { recursive: true, mode: 448 });
4517
+ import_node_fs5.default.writeFileSync(filePath, JSON.stringify(state));
5018
4518
  } catch {
5019
4519
  }
5020
4520
  }
@@ -5140,29 +4640,38 @@ function createAutoUpdater(config) {
5140
4640
  0 && (module.exports = {
5141
4641
  AUTH_PROVIDERS,
5142
4642
  AuthStorage,
4643
+ CHATGPT_PLAN_BASE_URL,
4644
+ CHATGPT_PLAN_NOT_ENABLED_MESSAGE,
4645
+ CHATGPT_PLAN_SCOPE,
4646
+ CHATGPT_USAGE_URL,
5143
4647
  CLAUDE_CODE_AUTH_KEY,
5144
4648
  CLAUDE_CODE_AUTH_MARKER,
5145
4649
  CLAUDE_CODE_MANIFEST,
4650
+ ChatGPTPlanNotEnabledError,
5146
4651
  DEFAULT_LOCAL_ENDPOINTS,
5147
4652
  DEFAULT_MAX_VIDEO_BYTES,
5148
4653
  FALLBACK_CONTEXT_WINDOW,
5149
4654
  GEMINI_API_KEY,
5150
4655
  GEMINI_API_KEY_BASE_URL,
5151
4656
  HIDDEN_PROVIDERS,
4657
+ KIMI_CODE_DEFAULT_BASE_URL,
4658
+ KIMI_CODE_KEY,
4659
+ KIMI_CODE_OVERSEAS_BASE_URL,
5152
4660
  LOCAL_API_KEY_PLACEHOLDER,
5153
4661
  LOCAL_AUTH_KEY_PREFIX,
5154
4662
  MINIMAX_CN_BASE_URL,
5155
4663
  MINIMAX_GLOBAL_BASE_URL,
5156
4664
  MODELS,
5157
- MOONSHOT_OAUTH_KEY,
5158
4665
  NotLoggedInError,
4666
+ OPENAI_CHATGPT_KEY,
5159
4667
  OPENROUTER_DEFAULT_BASE_URL,
5160
4668
  PLAN_MODE_THINKING_CAP,
4669
+ RETIRED_SIGN_INS,
5161
4670
  SubscriptionUsageError,
5162
4671
  TelegramBot,
5163
- XAI_OAUTH_KEY,
5164
4672
  XIAOMI_CREDITS_KEY,
5165
4673
  XIAOMI_TOKEN_PLAN_BASE_URLS,
4674
+ apiKeyStorageKeys,
5166
4675
  authPriorityNote,
5167
4676
  clampThinkingForPlanMode,
5168
4677
  claudeCodeAuthStatus,
@@ -5179,6 +4688,7 @@ function createAutoUpdater(config) {
5179
4688
  createAutoUpdater,
5180
4689
  decodeOggOpus,
5181
4690
  describeAuthMethods,
4691
+ detectKimiCodeBaseUrl,
5182
4692
  detectMiniMaxBaseUrl,
5183
4693
  detectXiaomiTokenPlanBaseUrl,
5184
4694
  discoverLocalModels,
@@ -5187,6 +4697,7 @@ function createAutoUpdater(config) {
5187
4697
  dualAuthProviderByOAuthKey,
5188
4698
  endpointRoot,
5189
4699
  ensureClaudeCodeBinary,
4700
+ fetchChatGPTPlanModels,
5190
4701
  fetchSubscriptionUsage,
5191
4702
  findProbedModel,
5192
4703
  formatClaudeCodeInstallProgress,
@@ -5197,6 +4708,7 @@ function createAutoUpdater(config) {
5197
4708
  getAuthProvider,
5198
4709
  getAuthStorageKey,
5199
4710
  getAuthStorageKeys,
4711
+ getChatGPTHostId,
5200
4712
  getContextWindow,
5201
4713
  getDefaultModel,
5202
4714
  getDefaultThinkingLevel,
@@ -5212,10 +4724,7 @@ function createAutoUpdater(config) {
5212
4724
  getSupportedThinkingLevels,
5213
4725
  getToolResultCharLimit,
5214
4726
  getVideoByteLimit,
5215
- grokCliBaseUrl,
5216
- grokCliHeaders,
5217
4727
  isClaudeCodeAuthMarker,
5218
- isGrokCliEndpoint,
5219
4728
  isHiddenProvider,
5220
4729
  isKimiCodingEndpoint,
5221
4730
  isLegacyClaudeOAuth,
@@ -5224,21 +4733,17 @@ function createAutoUpdater(config) {
5224
4733
  isModelLoaded,
5225
4734
  isPaidModel,
5226
4735
  isThinkingLevelSupported,
5227
- kimiCodeBaseUrl,
5228
- kimiCodingHeaders,
5229
4736
  localAuthStorageKey,
5230
4737
  localModelShortName,
5231
4738
  log,
4739
+ loginChatGPT,
5232
4740
  loginClaudeCode,
5233
- loginGemini,
5234
- loginKimi,
5235
- loginOpenAI,
5236
- loginXai,
5237
4741
  logoutClaudeCode,
5238
4742
  mergeClaudeCodeRateLimit,
5239
4743
  oauthStorageKey,
5240
4744
  ollamaServedContext,
5241
4745
  openLog,
4746
+ parseCallbackInput,
5242
4747
  parseLocalModelId,
5243
4748
  parseOpenRouterCatalog,
5244
4749
  probeEndpoint,
@@ -5246,21 +4751,22 @@ function createAutoUpdater(config) {
5246
4751
  readClaudeCodeUsage,
5247
4752
  readStoredBaseUrlSync,
5248
4753
  recordClaudeCodeRateLimit,
5249
- refreshGeminiToken,
5250
- refreshKimiToken,
5251
- refreshOpenAIToken,
5252
- refreshXaiToken,
4754
+ refreshChatGPTToken,
5253
4755
  registerLogCleanup,
5254
4756
  registerRuntimeModels,
4757
+ requiredKeyLabel,
5255
4758
  resample,
5256
4759
  resolveClaudeCodeBinary,
4760
+ revokeChatGPTSignIn,
5257
4761
  setProgressCallback,
5258
4762
  setProviderCatalog,
5259
4763
  signInWithClaudeCode,
5260
4764
  signOutOfClaudeCode,
4765
+ storageKeyProvider,
5261
4766
  toModelInfo,
5262
4767
  transcribeVoice,
5263
4768
  usesOpenAICodexTransport,
4769
+ verifyIdToken,
5264
4770
  withFileLock,
5265
4771
  withoutPaidTier
5266
4772
  });