@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.
@@ -2,244 +2,49 @@ import {
2
2
  getAppPaths
3
3
  } from "./chunk-GXOFTHM5.js";
4
4
 
5
- // src/oauth/kimi.ts
6
- import { execFileSync } from "child_process";
7
- import { randomUUID } from "crypto";
8
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
9
- import { arch, hostname, release, type } from "os";
10
- import path from "path";
11
-
12
- // src/oauth/request-timeout.ts
13
- var OAUTH_REQUEST_TIMEOUT_MS = 3e4;
14
- function oauthRequestSignal() {
15
- return AbortSignal.timeout(OAUTH_REQUEST_TIMEOUT_MS);
16
- }
17
-
18
- // src/oauth/kimi.ts
19
- var CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098";
20
- var DEFAULT_OAUTH_HOST = "https://auth.kimi.com";
21
- var DEFAULT_CODING_BASE_URL = "https://api.kimi.com/coding/v1";
22
- var KIMI_PLATFORM = "kimi_code_cli";
23
- var DEFAULT_KIMI_VERSION = "1.0.11";
24
- var DEVICE_TIMEOUT_MS = 15 * 60 * 1e3;
25
- function oauthHost() {
26
- const host = process.env.KIMI_CODE_OAUTH_HOST ?? process.env.KIMI_OAUTH_HOST ?? DEFAULT_OAUTH_HOST;
27
- return host.replace(/\/+$/, "");
28
- }
29
- function kimiCodeBaseUrl() {
30
- return (process.env.KIMI_CODE_BASE_URL ?? DEFAULT_CODING_BASE_URL).replace(/\/+$/, "");
31
- }
32
- function kimiVersion() {
33
- const v = process.env.KIMI_CODE_VERSION ?? DEFAULT_KIMI_VERSION;
34
- return asciiHeader(v, DEFAULT_KIMI_VERSION);
35
- }
36
- function asciiHeader(value, fallback = "unknown") {
37
- const cleaned = value.replace(/[^\u0020-\u007E]/g, "").trim();
38
- return cleaned.length > 0 ? cleaned : fallback;
39
- }
40
- function macOsProductVersion() {
5
+ // src/kimi-region.ts
6
+ import { GG_CODER_USER_AGENT } from "@kenkaiiii/gg-ai";
7
+ var KIMI_CODE_DEFAULT_BASE_URL = "https://api.kimi.com/coding/v1";
8
+ var KIMI_CODE_OVERSEAS_BASE_URL = "https://api.kimi.ai/coding/v1";
9
+ var KIMI_CODE_BASE_URLS = [KIMI_CODE_DEFAULT_BASE_URL, KIMI_CODE_OVERSEAS_BASE_URL];
10
+ async function probeHost(baseUrl, apiKey, fetchImpl, timeoutMs) {
41
11
  try {
42
- const version = execFileSync("/usr/bin/sw_vers", ["-productVersion"], {
43
- encoding: "utf-8",
44
- timeout: 1e3
45
- }).trim();
46
- return version.length > 0 ? version : void 0;
47
- } catch {
48
- return void 0;
49
- }
50
- }
51
- function deviceModel() {
52
- const os = type();
53
- const version = release();
54
- const osArch = arch();
55
- if (os === "Darwin") return `macOS ${macOsProductVersion() ?? version} ${osArch}`;
56
- if (os === "Windows_NT") return `Windows ${version} ${osArch}`;
57
- return `${os} ${version} ${osArch}`.trim();
58
- }
59
- function deviceId() {
60
- const idPath = path.join(getAppPaths().agentDir, "kimi_device_id");
61
- if (existsSync(idPath)) {
62
- try {
63
- const text = readFileSync(idPath, "utf-8").trim();
64
- if (text.length > 0) return text;
65
- } catch {
66
- }
67
- }
68
- const id = randomUUID();
69
- try {
70
- mkdirSync(getAppPaths().agentDir, { recursive: true, mode: 448 });
71
- writeFileSync(idPath, id, { encoding: "utf-8", mode: 384 });
12
+ const res = await fetchImpl(`${baseUrl}/models`, {
13
+ headers: { Authorization: `Bearer ${apiKey}`, "User-Agent": GG_CODER_USER_AGENT },
14
+ signal: AbortSignal.timeout(timeoutMs)
15
+ });
16
+ if (res.ok) return "authorized";
17
+ if (res.status === 401 || res.status === 403) return "unauthorized";
18
+ return "inconclusive";
72
19
  } catch {
20
+ return "inconclusive";
73
21
  }
74
- return id;
75
- }
76
- function deviceHeaders() {
77
- return {
78
- "X-Msh-Platform": KIMI_PLATFORM,
79
- "X-Msh-Version": kimiVersion(),
80
- "X-Msh-Device-Name": asciiHeader(hostname()),
81
- "X-Msh-Device-Model": asciiHeader(deviceModel()),
82
- "X-Msh-Os-Version": asciiHeader(release()),
83
- "X-Msh-Device-Id": deviceId()
84
- };
85
22
  }
86
- function kimiCodingHeaders() {
87
- return {
88
- "User-Agent": `kimi-code-cli/${kimiVersion()}`,
89
- ...deviceHeaders()
90
- };
23
+ async function detectKimiCodeBaseUrl(apiKey, options = {}) {
24
+ const fetchImpl = options.fetch ?? fetch;
25
+ const timeoutMs = options.timeoutMs ?? 5e3;
26
+ const results = await Promise.all(
27
+ KIMI_CODE_BASE_URLS.map((url) => probeHost(url, apiKey, fetchImpl, timeoutMs))
28
+ );
29
+ const index = results.indexOf("authorized");
30
+ return index === -1 ? void 0 : KIMI_CODE_BASE_URLS[index];
91
31
  }
92
32
  function isKimiCodingEndpoint(baseUrl) {
93
33
  if (typeof baseUrl !== "string" || baseUrl.length === 0) return false;
94
- const normalized = baseUrl.replace(/\/+$/, "");
95
- return normalized === kimiCodeBaseUrl() || /(^|\.)kimi\.com/i.test(normalized);
96
- }
97
- async function postForm(endpoint, params) {
98
- const response = await fetch(`${oauthHost()}${endpoint}`, {
99
- method: "POST",
100
- headers: {
101
- ...deviceHeaders(),
102
- "Content-Type": "application/x-www-form-urlencoded",
103
- Accept: "application/json"
104
- },
105
- body: new URLSearchParams(params).toString(),
106
- signal: oauthRequestSignal()
107
- });
108
- let data = {};
109
- try {
110
- const parsed = await response.json();
111
- if (parsed && typeof parsed === "object") data = parsed;
112
- } catch {
113
- }
114
- return { status: response.status, data };
115
- }
116
- function errorDetail(data) {
117
- const desc = data.error_description ?? data.message ?? data.error;
118
- return typeof desc === "string" && desc.length > 0 ? desc : "unknown error";
119
- }
120
- function credsFromTokenResponse(data, opts) {
121
- const accessToken = data.access_token;
122
- const responseRefreshToken = data.refresh_token;
123
- const expiresIn = Number(data.expires_in);
124
- if (typeof accessToken !== "string" || accessToken.length === 0) {
125
- throw new Error("Kimi OAuth response missing access_token.");
126
- }
127
- const refreshToken = typeof responseRefreshToken === "string" && responseRefreshToken.length > 0 ? responseRefreshToken : opts?.fallbackRefreshToken ?? "";
128
- if (refreshToken.length === 0) {
129
- throw new Error("Kimi OAuth response missing refresh_token.");
130
- }
131
- if (!Number.isFinite(expiresIn) || expiresIn <= 0) {
132
- throw new Error("Kimi OAuth response missing or invalid expires_in.");
133
- }
134
- return {
135
- accessToken,
136
- refreshToken,
137
- expiresAt: Date.now() + expiresIn * 1e3,
138
- expiresIn,
139
- baseUrl: kimiCodeBaseUrl()
140
- };
141
- }
142
- async function requestDeviceAuthorization() {
143
- const { status, data } = await postForm("/api/oauth/device_authorization", {
144
- client_id: CLIENT_ID
145
- });
146
- if (status !== 200) {
147
- throw new Error(`Kimi device authorization failed (${status}): ${errorDetail(data)}`);
148
- }
149
- const userCode = data.user_code;
150
- const deviceCode = data.device_code;
151
- const verificationUriComplete = data.verification_uri_complete;
152
- if (typeof userCode !== "string" || typeof deviceCode !== "string") {
153
- throw new Error("Kimi device authorization response missing user_code/device_code.");
154
- }
155
- return {
156
- userCode,
157
- deviceCode,
158
- verificationUri: typeof data.verification_uri === "string" ? data.verification_uri : "",
159
- verificationUriComplete: typeof verificationUriComplete === "string" ? verificationUriComplete : "",
160
- interval: Number(data.interval ?? 5) || 5
161
- };
162
- }
163
- async function pollDeviceToken(deviceCode) {
164
- const { status, data } = await postForm("/api/oauth/token", {
165
- client_id: CLIENT_ID,
166
- device_code: deviceCode,
167
- grant_type: "urn:ietf:params:oauth:grant-type:device_code"
168
- });
169
- if (status === 200 && typeof data.access_token === "string") {
170
- return { kind: "success", creds: credsFromTokenResponse(data) };
171
- }
172
- if (status >= 500) {
173
- throw new Error(`Kimi token polling server error (${status}): ${errorDetail(data)}`);
174
- }
175
- const errorCode = typeof data.error === "string" ? data.error : "unknown_error";
176
- switch (errorCode) {
177
- case "authorization_pending":
178
- return { kind: "pending" };
179
- case "slow_down":
180
- return { kind: "slow_down" };
181
- case "expired_token":
182
- return { kind: "expired" };
183
- case "access_denied":
184
- return { kind: "denied" };
185
- default:
186
- throw new Error(`Kimi token polling failed (${status}): ${errorDetail(data)}`);
187
- }
188
- }
189
- function sleep(ms) {
190
- return new Promise((resolve) => {
191
- setTimeout(resolve, ms);
192
- });
193
- }
194
- async function loginKimi(callbacks) {
195
- const auth = await requestDeviceAuthorization();
196
- callbacks.onStatus(
197
- `Visit ${auth.verificationUri || auth.verificationUriComplete} and enter code: ${auth.userCode}`
198
- );
199
- callbacks.onOpenUrl(auth.verificationUriComplete || auth.verificationUri);
200
- callbacks.onStatus("Waiting for you to authorize in the browser...");
201
- const deadline = Date.now() + DEVICE_TIMEOUT_MS;
202
- let interval = Math.max(auth.interval, 1);
203
- while (Date.now() < deadline) {
204
- await sleep(interval * 1e3);
205
- const result = await pollDeviceToken(auth.deviceCode);
206
- if (result.kind === "success") return result.creds;
207
- if (result.kind === "denied") {
208
- throw new Error("Kimi authorization was denied.");
209
- }
210
- if (result.kind === "expired") {
211
- throw new Error("Kimi device code expired. Please run login again.");
212
- }
213
- if (result.kind === "slow_down") {
214
- interval += 5;
215
- }
216
- }
217
- throw new Error("Kimi login timed out. Please run login again.");
218
- }
219
- async function refreshKimiToken(refreshToken) {
220
- const { status, data } = await postForm("/api/oauth/token", {
221
- client_id: CLIENT_ID,
222
- grant_type: "refresh_token",
223
- refresh_token: refreshToken
224
- });
225
- if (status === 200 && typeof data.access_token === "string") {
226
- return credsFromTokenResponse(data, { fallbackRefreshToken: refreshToken });
227
- }
228
- const errorCode = typeof data.error === "string" ? data.error : "";
229
- throw new Error(`Kimi token refresh failed (${status}): ${errorCode || errorDetail(data)}`);
34
+ return /^https:\/\/api\.kimi\.(com|ai)\/coding(\/|$)/i.test(baseUrl);
230
35
  }
231
36
 
232
37
  // src/auth-storage.ts
233
- import fs5 from "fs/promises";
234
- import { readFileSync as readFileSync2 } from "fs";
235
- import crypto5 from "crypto";
38
+ import fs6 from "fs/promises";
39
+ import { readFileSync } from "fs";
40
+ import crypto4 from "crypto";
236
41
 
237
42
  // src/claude-code/runtime.ts
238
43
  import { spawn } from "child_process";
239
44
  import crypto2 from "crypto";
240
45
  import fs2 from "fs";
241
46
  import fsp2 from "fs/promises";
242
- import path2 from "path";
47
+ import path from "path";
243
48
 
244
49
  // src/claude-code/tar.ts
245
50
  import fs from "fs";
@@ -320,7 +125,7 @@ async function extractFileFromTarGz(tgzPath, entryName, destPath) {
320
125
  pos += BLOCK;
321
126
  if (header.every((b) => b === 0)) continue;
322
127
  const size = readSize(header, 124);
323
- const type2 = String.fromCharCode(header[156] || 48);
128
+ const type = String.fromCharCode(header[156] || 48);
324
129
  let name = readString(header, 0, 100);
325
130
  if (readString(header, 257, 6) === "ustar") {
326
131
  const prefix = readString(header, 345, 155);
@@ -329,12 +134,12 @@ async function extractFileFromTarGz(tgzPath, entryName, destPath) {
329
134
  remaining = size;
330
135
  padding = (BLOCK - size % BLOCK) % BLOCK;
331
136
  meta = [];
332
- if (type2 === "x") kind = "pax";
333
- else if (type2 === "L") kind = "longname";
137
+ if (type === "x") kind = "pax";
138
+ else if (type === "L") kind = "longname";
334
139
  else {
335
140
  const path4 = overridePath ?? name;
336
141
  overridePath = void 0;
337
- if ((type2 === "0" || type2 === "\0" || type2 === "7") && path4 === entryName) {
142
+ if ((type === "0" || type === "\0" || type === "7") && path4 === entryName) {
338
143
  kind = "target";
339
144
  out = await fsp.open(destPath, "w", 493);
340
145
  if (size === 0) {
@@ -424,10 +229,10 @@ function isMusl() {
424
229
  return report != null && report.header?.glibcVersionRuntime === void 0;
425
230
  }
426
231
  function claudeCodePlatformKey(info = { platform: process.platform, arch: process.arch, musl: isMusl() }) {
427
- const arch2 = info.arch === "arm64" || info.arch === "x64" ? info.arch : null;
428
- if (!arch2) return null;
429
- if (info.platform === "darwin" || info.platform === "win32") return `${info.platform}-${arch2}`;
430
- if (info.platform === "linux") return `linux-${arch2}${info.musl ? "-musl" : ""}`;
232
+ const arch = info.arch === "arm64" || info.arch === "x64" ? info.arch : null;
233
+ if (!arch) return null;
234
+ if (info.platform === "darwin" || info.platform === "win32") return `${info.platform}-${arch}`;
235
+ if (info.platform === "linux") return `linux-${arch}${info.musl ? "-musl" : ""}`;
431
236
  return null;
432
237
  }
433
238
  function resolveAsset(opts) {
@@ -445,11 +250,11 @@ function claudeCodeHome(opts = {}) {
445
250
  return opts.home ?? getAppPaths().claudeCodeDir;
446
251
  }
447
252
  function claudeCodeConfigDir(opts = {}) {
448
- return opts.home ? path2.join(opts.home, "config") : getAppPaths().claudeCodeConfigDir;
253
+ return opts.home ? path.join(opts.home, "config") : getAppPaths().claudeCodeConfigDir;
449
254
  }
450
255
  function claudeCodeBinaryPath(opts = {}) {
451
256
  const { manifest, asset } = resolveAsset(opts);
452
- return path2.join(claudeCodeHome(opts), manifest.claudeCodeVersion, asset.bin);
257
+ return path.join(claudeCodeHome(opts), manifest.claudeCodeVersion, asset.bin);
453
258
  }
454
259
  function resolveClaudeCodeBinary(opts = {}) {
455
260
  let binary;
@@ -479,12 +284,12 @@ async function ensureClaudeCodeBinary(opts = {}) {
479
284
  async function installClaudeCode(opts) {
480
285
  const { manifest, asset } = resolveAsset(opts);
481
286
  const home = claudeCodeHome(opts);
482
- const versionDir = path2.join(home, manifest.claudeCodeVersion);
483
- const binary = path2.join(versionDir, asset.bin);
287
+ const versionDir = path.join(home, manifest.claudeCodeVersion);
288
+ const binary = path.join(versionDir, asset.bin);
484
289
  await fsp2.mkdir(home, { recursive: true });
485
290
  const tag = `${process.pid}-${crypto2.randomBytes(4).toString("hex")}`;
486
- const tarball = path2.join(home, `.download-${tag}.tgz`);
487
- const staging = path2.join(home, `.staging-${tag}`);
291
+ const tarball = path.join(home, `.download-${tag}.tgz`);
292
+ const staging = path.join(home, `.staging-${tag}`);
488
293
  try {
489
294
  await download(asset, tarball, opts);
490
295
  opts.onProgress?.({ phase: "extract" });
@@ -492,10 +297,10 @@ async function installClaudeCode(opts) {
492
297
  const found = await extractFileFromTarGz(
493
298
  tarball,
494
299
  `package/${asset.bin}`,
495
- path2.join(staging, asset.bin)
300
+ path.join(staging, asset.bin)
496
301
  );
497
302
  if (!found) throw new Error(`The Claude Code download has no ${asset.bin} binary.`);
498
- await fsp2.chmod(path2.join(staging, asset.bin), 493);
303
+ await fsp2.chmod(path.join(staging, asset.bin), 493);
499
304
  try {
500
305
  await fsp2.rename(staging, versionDir);
501
306
  } catch (err) {
@@ -555,7 +360,7 @@ async function removeOldVersions(home, keep) {
555
360
  for (const entry of entries) {
556
361
  if (!entry.isDirectory() || entry.name === keep || entry.name === "config") continue;
557
362
  if (!/^\d+\.\d+\.\d+/.test(entry.name)) continue;
558
- await fsp2.rm(path2.join(home, entry.name), { recursive: true, force: true }).catch(() => {
363
+ await fsp2.rm(path.join(home, entry.name), { recursive: true, force: true }).catch(() => {
559
364
  });
560
365
  }
561
366
  }
@@ -657,8 +462,8 @@ async function loginClaudeCode(callbacks, opts = {}) {
657
462
  const home = claudeCodeHome(opts);
658
463
  await fsp2.mkdir(claudeCodeConfigDir(opts), { recursive: true });
659
464
  const tag = `${process.pid}-${crypto2.randomBytes(4).toString("hex")}`;
660
- const hookFile = path2.join(home, `.open-url-${tag}.cjs`);
661
- const urlFile = path2.join(home, `.open-url-${tag}.txt`);
465
+ const hookFile = path.join(home, `.open-url-${tag}.cjs`);
466
+ const urlFile = path.join(home, `.open-url-${tag}.txt`);
662
467
  await fsp2.writeFile(hookFile, OPEN_URL_HOOK);
663
468
  const env = {
664
469
  ...claudeCodeEnv(opts),
@@ -739,6 +544,8 @@ async function loginClaudeCode(callbacks, opts = {}) {
739
544
  // src/oauth/openai.ts
740
545
  import http from "http";
741
546
  import crypto3 from "crypto";
547
+ import fs3 from "fs/promises";
548
+ import path2 from "path";
742
549
 
743
550
  // src/oauth/pkce.ts
744
551
  function base64urlEncode(bytes) {
@@ -758,100 +565,233 @@ async function generatePKCE() {
758
565
  return { verifier, challenge };
759
566
  }
760
567
 
568
+ // src/oauth/request-timeout.ts
569
+ var OAUTH_REQUEST_TIMEOUT_MS = 3e4;
570
+ function oauthRequestSignal() {
571
+ return AbortSignal.timeout(OAUTH_REQUEST_TIMEOUT_MS);
572
+ }
573
+
761
574
  // src/oauth/openai.ts
762
- var CLIENT_ID2 = "app_EMoamEEZ73f0CkXaXp7hrann";
763
- var AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
764
- var TOKEN_URL = "https://auth.openai.com/oauth/token";
765
- var REDIRECT_URI = "http://localhost:1455/auth/callback";
766
- var SCOPE = "openid profile email offline_access api.connectors.read api.connectors.invoke";
767
- var JWT_CLAIM_PATH = "https://api.openai.com/auth";
575
+ var ISSUER = "https://auth.openai.com";
576
+ var AUTHORIZE_URL = `${ISSUER}/api/accounts/authorize`;
577
+ var TOKEN_URL = `${ISSUER}/api/accounts/oauth/token`;
578
+ var REVOKE_URL = `${ISSUER}/api/accounts/oauth/revoke`;
579
+ var JWKS_URL = `${ISSUER}/.well-known/jwks.json`;
580
+ var REGISTRATION_CLIENT_ID = "dynamic_agent_client";
581
+ var AGENT_NAME = "GG Coder";
582
+ var RESOURCE = "https://api.openai.com/v1";
583
+ var CHATGPT_PLAN_SCOPE = "chatgpt.tokens.use.direct";
584
+ var SCOPES = `openid profile email offline_access resource.invoke ${CHATGPT_PLAN_SCOPE}`;
585
+ var CHATGPT_PLAN_BASE_URL = "https://api.openai.com/v1";
586
+ var CHATGPT_USAGE_URL = "https://chatgpt.com/settings/usage";
587
+ var CALLBACK_PATH = "/auth/callback";
588
+ var PREFERRED_PORT = 1455;
768
589
  var CALLBACK_TIMEOUT_MS = 12e4;
769
590
  var MAX_PASTE_ATTEMPTS = 3;
770
- var MIN_RAW_CODE_LENGTH = 10;
771
- async function loginOpenAI(callbacks) {
772
- const { verifier, challenge } = await generatePKCE();
773
- const state = crypto3.randomBytes(16).toString("hex");
774
- const url = new URL(AUTHORIZE_URL);
775
- url.searchParams.set("response_type", "code");
776
- url.searchParams.set("client_id", CLIENT_ID2);
777
- url.searchParams.set("redirect_uri", REDIRECT_URI);
778
- url.searchParams.set("scope", SCOPE);
779
- url.searchParams.set("code_challenge", challenge);
780
- url.searchParams.set("code_challenge_method", "S256");
781
- url.searchParams.set("state", state);
782
- url.searchParams.set("prompt", "login");
783
- url.searchParams.set("id_token_add_organizations", "true");
784
- url.searchParams.set("codex_cli_simplified_flow", "true");
785
- url.searchParams.set("originator", "ggcoder");
786
- const code = await acquireAuthorizationCode(url.toString(), state, callbacks);
787
- const creds = await exchangeOpenAICode(code, verifier);
788
- const accountId = getAccountId(creds.accessToken);
789
- if (!accountId) {
790
- throw new Error("Failed to extract accountId from OpenAI token.");
791
- }
792
- creds.accountId = accountId;
793
- return creds;
794
- }
795
- function parseAuthorizationInput(input) {
796
- const value = input.trim();
797
- if (!value) return {};
591
+ var ID_TOKEN_CLOCK_SKEW_S = 60;
592
+ 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.";
593
+ var ChatGPTPlanNotEnabledError = class extends Error {
594
+ constructor() {
595
+ super(CHATGPT_PLAN_NOT_ENABLED_MESSAGE);
596
+ this.name = "ChatGPTPlanNotEnabledError";
597
+ }
598
+ };
599
+ function defaultHostIdFile() {
600
+ return path2.join(getAppPaths().agentDir, "openai-host-id");
601
+ }
602
+ function defaultRegistrationFile() {
603
+ return path2.join(getAppPaths().agentDir, "openai-chatgpt-client.json");
604
+ }
605
+ async function writePrivateFile(file, content) {
606
+ await fs3.mkdir(path2.dirname(file), { recursive: true });
607
+ const tmp = `${file}.${process.pid}.${crypto3.randomUUID().slice(0, 8)}.tmp`;
798
608
  try {
799
- const url = new URL(value);
800
- return {
801
- code: url.searchParams.get("code") ?? void 0,
802
- state: url.searchParams.get("state") ?? void 0
803
- };
804
- } catch {
609
+ await fs3.writeFile(tmp, content, { encoding: "utf-8", mode: 384 });
610
+ await fs3.rename(tmp, file);
611
+ } catch (err) {
612
+ await fs3.unlink(tmp).catch(() => {
613
+ });
614
+ throw err;
805
615
  }
806
- if (value.includes("#")) {
807
- const [code, state] = value.split("#", 2);
808
- return { code, state };
616
+ }
617
+ var HOST_ID_PATTERN = /^urn:uuid:[0-9a-f-]{36}$/i;
618
+ async function getChatGPTHostId(file = defaultHostIdFile()) {
619
+ const existing = await fs3.readFile(file, "utf-8").catch(() => "");
620
+ if (HOST_ID_PATTERN.test(existing.trim())) return existing.trim();
621
+ await writePrivateFile(file, `urn:uuid:${crypto3.randomUUID()}`);
622
+ return (await fs3.readFile(file, "utf-8")).trim();
623
+ }
624
+ async function readRegistration(file) {
625
+ try {
626
+ const value = JSON.parse(await fs3.readFile(file, "utf-8"));
627
+ if (typeof value.clientId === "string" && typeof value.subject === "string") {
628
+ return { clientId: value.clientId, subject: value.subject };
629
+ }
630
+ } catch {
809
631
  }
810
- if (value.includes("code=")) {
811
- const params = new URLSearchParams(value);
632
+ return void 0;
633
+ }
634
+ async function loginChatGPT(callbacks, options = {}) {
635
+ const hostId = await getChatGPTHostId(options.hostIdFile);
636
+ const registrationFile = options.registrationFile ?? defaultRegistrationFile();
637
+ const registration = await readRegistration(registrationFile);
638
+ const { verifier, challenge } = await generatePKCE();
639
+ const state = crypto3.randomBytes(16).toString("hex");
640
+ const nonce = crypto3.randomBytes(16).toString("hex");
641
+ const listener = await startCallbackListener(state, Boolean(registration));
642
+ try {
643
+ const redirectUri = `http://127.0.0.1:${listener.port}${CALLBACK_PATH}`;
644
+ const url = new URL(AUTHORIZE_URL);
645
+ url.searchParams.set("client_id", registration?.clientId ?? REGISTRATION_CLIENT_ID);
646
+ if (!registration) url.searchParams.set("agent_name_hint", AGENT_NAME);
647
+ url.searchParams.set("ext_agent_host_id", hostId);
648
+ if (registration && options.previous?.idToken) {
649
+ url.searchParams.set("id_token_hint", options.previous.idToken);
650
+ }
651
+ url.searchParams.set("response_type", "code");
652
+ url.searchParams.set("redirect_uri", redirectUri);
653
+ url.searchParams.set("scope", SCOPES);
654
+ url.searchParams.set("resource", RESOURCE);
655
+ url.searchParams.set("state", state);
656
+ url.searchParams.set("nonce", nonce);
657
+ url.searchParams.set("code_challenge_method", "S256");
658
+ url.searchParams.set("code_challenge", challenge);
659
+ const callback = await acquireCallback(url.toString(), state, listener, callbacks);
660
+ if (callback.get("error")) {
661
+ if (callback.get("error") === "access_denied") throw new ChatGPTPlanNotEnabledError();
662
+ throw new Error(
663
+ `ChatGPT sign-in failed: ${callback.get("error_description") ?? callback.get("error")}`
664
+ );
665
+ }
666
+ const code = callback.get("code");
667
+ if (!code) throw new Error("ChatGPT sign-in returned no authorization code.");
668
+ const issued = callback.get("client_id") ?? void 0;
669
+ if (registration && issued && issued !== registration.clientId) {
670
+ throw new Error("ChatGPT returned a different app registration. Start the sign-in again.");
671
+ }
672
+ const clientId = registration?.clientId ?? issued;
673
+ if (!clientId || clientId === REGISTRATION_CLIENT_ID) {
674
+ throw new Error(
675
+ "ChatGPT didn't finish registering GG Coder. Start the sign-in again, and paste the full callback URL if you're on another machine."
676
+ );
677
+ }
678
+ const tokens = await exchangeCode({ code, verifier, clientId, redirectUri });
679
+ const claims = await verifyIdToken(tokens.idToken, { clientId, nonce });
680
+ const subject = String(claims.sub ?? "");
681
+ if (!subject) throw new Error("ChatGPT ID token has no subject.");
682
+ if (registration && registration.subject !== subject) {
683
+ await fs3.unlink(registrationFile).catch(() => {
684
+ });
685
+ await revokeChatGPTSignIn({ refreshToken: tokens.refreshToken, clientId });
686
+ throw new Error(
687
+ "That was a different ChatGPT account. Continue with ChatGPT again to connect it."
688
+ );
689
+ }
690
+ if (!tokens.scopes.includes(CHATGPT_PLAN_SCOPE)) {
691
+ await revokeChatGPTSignIn({ refreshToken: tokens.refreshToken, clientId });
692
+ throw new ChatGPTPlanNotEnabledError();
693
+ }
694
+ if (!registration) {
695
+ await writePrivateFile(registrationFile, JSON.stringify({ clientId, subject }));
696
+ }
812
697
  return {
813
- code: params.get("code") ?? void 0,
814
- state: params.get("state") ?? void 0
698
+ accessToken: tokens.accessToken,
699
+ refreshToken: tokens.refreshToken,
700
+ expiresAt: Date.now() + tokens.expiresIn * 1e3,
701
+ expiresIn: tokens.expiresIn,
702
+ accountId: subject,
703
+ clientId,
704
+ idToken: tokens.idToken,
705
+ scopes: tokens.scopes,
706
+ baseUrl: CHATGPT_PLAN_BASE_URL
815
707
  };
708
+ } finally {
709
+ listener.close();
816
710
  }
817
- if (/\s/.test(value) || value.length < MIN_RAW_CODE_LENGTH) return {};
818
- return { code: value };
819
711
  }
820
- function decodeJwt(token) {
821
- try {
822
- const parts = token.split(".");
823
- if (parts.length !== 3) return null;
824
- const decoded = atob(parts[1]);
825
- return JSON.parse(decoded);
826
- } catch {
827
- return null;
828
- }
712
+ async function fetchChatGPTPlanModels(accessToken, fetchFn = fetch) {
713
+ const response = await fetchFn(`${CHATGPT_PLAN_BASE_URL}/models`, {
714
+ headers: { Authorization: `Bearer ${accessToken}` },
715
+ signal: oauthRequestSignal()
716
+ });
717
+ if (!response.ok) throw new Error(`ChatGPT model list failed (${response.status})`);
718
+ const data = await response.json();
719
+ return (data.models ?? []).filter((m) => typeof m.slug === "string" && (m.visibility ?? "list") === "list").map((m) => m.slug);
829
720
  }
830
- function getAccountId(accessToken) {
831
- const payload = decodeJwt(accessToken);
832
- const auth = payload?.[JWT_CLAIM_PATH];
833
- const accountId = auth?.chatgpt_account_id;
834
- return typeof accountId === "string" && accountId.length > 0 ? accountId : null;
721
+ async function startCallbackListener(expectedState, anyPort) {
722
+ let settle;
723
+ const result = new Promise((resolve, reject) => {
724
+ settle = { resolve, reject };
725
+ });
726
+ result.catch(() => {
727
+ });
728
+ const server = http.createServer((req, res) => {
729
+ const url = new URL(req.url || "", "http://127.0.0.1");
730
+ if (url.pathname !== CALLBACK_PATH) {
731
+ res.statusCode = 404;
732
+ res.end("Not found");
733
+ return;
734
+ }
735
+ if (url.searchParams.get("state") !== expectedState) {
736
+ res.statusCode = 400;
737
+ res.end("State mismatch");
738
+ return;
739
+ }
740
+ res.writeHead(200, { "Content-Type": "text/html" });
741
+ res.end(
742
+ "<html><body><h1>You're signed in to GG Coder.</h1><p>You can close this tab.</p></body></html>"
743
+ );
744
+ settle?.resolve(url.searchParams);
745
+ server.close();
746
+ });
747
+ const listen = (port2) => new Promise((resolve, reject) => {
748
+ const onError = (err) => reject(err);
749
+ server.once("error", onError);
750
+ server.listen(port2, "127.0.0.1", () => {
751
+ server.off("error", onError);
752
+ resolve(server.address().port);
753
+ });
754
+ });
755
+ let port;
756
+ for (const candidate of anyPort ? [PREFERRED_PORT, 0] : [PREFERRED_PORT]) {
757
+ try {
758
+ port = await listen(candidate);
759
+ break;
760
+ } catch {
761
+ }
762
+ }
763
+ if (port === void 0) {
764
+ settle?.reject(new Error("Could not start the local sign-in listener."));
765
+ return { port: PREFERRED_PORT, result, close: () => {
766
+ } };
767
+ }
768
+ const timeout = setTimeout(() => server.close(), CALLBACK_TIMEOUT_MS);
769
+ timeout.unref();
770
+ server.on("close", () => {
771
+ clearTimeout(timeout);
772
+ settle?.reject(new Error("Sign-in listener closed without a callback."));
773
+ });
774
+ return { port, result, close: () => server.close() };
835
775
  }
836
- async function acquireAuthorizationCode(authUrl, expectedState, callbacks) {
776
+ async function acquireCallback(authUrl, expectedState, listener, callbacks) {
837
777
  callbacks.onOpenUrl(authUrl);
838
- callbacks.onStatus("Waiting for browser callback...");
778
+ callbacks.onStatus("Waiting for ChatGPT sign-in in your browser...");
839
779
  const done = new AbortController();
840
780
  try {
841
781
  return await new Promise((resolve, reject) => {
842
- let serverFailed = false;
782
+ let listenerFailed = false;
843
783
  let pasteError;
844
784
  const failIfExhausted = () => {
845
- if (!serverFailed || pasteError === void 0) return;
785
+ if (!listenerFailed || pasteError === void 0) return;
846
786
  reject(
847
- pasteError instanceof Error ? pasteError : new Error("Could not obtain an authorization code.")
787
+ pasteError instanceof Error ? pasteError : new Error("Could not complete the sign-in.")
848
788
  );
849
789
  };
850
- listenForCallback(expectedState, done.signal).then(resolve, () => {
851
- serverFailed = true;
790
+ listener.result.then(resolve, () => {
791
+ listenerFailed = true;
852
792
  failIfExhausted();
853
793
  });
854
- promptForCode(expectedState, callbacks, done.signal).then(resolve, (err) => {
794
+ promptForCallback(expectedState, callbacks, done.signal).then(resolve, (err) => {
855
795
  if (err instanceof FatalLoginError) {
856
796
  reject(err);
857
797
  return;
@@ -862,686 +802,158 @@ async function acquireAuthorizationCode(authUrl, expectedState, callbacks) {
862
802
  });
863
803
  } finally {
864
804
  done.abort();
805
+ listener.close();
865
806
  }
866
807
  }
867
808
  var FatalLoginError = class extends Error {
868
809
  };
869
- async function promptForCode(expectedState, callbacks, signal) {
870
- let message = "If the browser is on another machine (SSH/headless), paste the callback URL or code here:";
810
+ function parseCallbackInput(input) {
811
+ const value = input.trim();
812
+ if (!value) return void 0;
813
+ try {
814
+ return new URL(value).searchParams;
815
+ } catch {
816
+ }
817
+ if (/[?&]?(code|error)=/.test(value)) {
818
+ return new URLSearchParams(value.replace(/^.*\?/, ""));
819
+ }
820
+ return void 0;
821
+ }
822
+ async function promptForCallback(expectedState, callbacks, signal) {
823
+ let message = "If the browser is on another machine (SSH/headless), paste the full callback URL here:";
871
824
  for (let attempt = 0; attempt < MAX_PASTE_ATTEMPTS; attempt++) {
872
- if (signal.aborted) throw new Error("Authorization code already received.");
825
+ if (signal.aborted) throw new Error("Callback already received.");
873
826
  const raw = await callbacks.onPromptCode(message, signal);
874
- if (signal.aborted) throw new Error("Authorization code already received.");
875
- const parsed = parseAuthorizationInput(raw);
876
- if (parsed.code && parsed.state && parsed.state !== expectedState) {
877
- throw new FatalLoginError("Authorization state mismatch \u2014 start the login again.");
827
+ if (signal.aborted) throw new Error("Callback already received.");
828
+ const params = parseCallbackInput(raw);
829
+ if (params && (params.get("code") || params.get("error"))) {
830
+ if (params.get("state") !== expectedState) {
831
+ throw new FatalLoginError("Sign-in state mismatch \u2014 start the sign-in again.");
832
+ }
833
+ return params;
878
834
  }
879
- if (parsed.code) return parsed.code;
880
- message = "That didn't contain an authorization code. Paste the full callback URL:";
835
+ message = "That wasn't a callback URL. Paste the full URL from the browser's address bar:";
881
836
  }
882
- throw new Error("No authorization code found in input.");
837
+ throw new Error("No callback URL found in input.");
883
838
  }
884
- async function listenForCallback(expectedState, signal) {
885
- return new Promise((resolve, reject) => {
886
- let receivedCode = null;
887
- const server = http.createServer((req, res) => {
888
- const url = new URL(req.url || "", "http://localhost");
889
- if (url.pathname !== "/auth/callback") {
890
- res.statusCode = 404;
891
- res.end("Not found");
892
- return;
893
- }
894
- if (url.searchParams.get("state") !== expectedState) {
895
- res.statusCode = 400;
896
- res.end("State mismatch");
897
- return;
898
- }
899
- receivedCode = url.searchParams.get("code");
900
- res.writeHead(200, { "Content-Type": "text/html" });
901
- res.end("<html><body><h1>Login successful!</h1><p>You can close this tab.</p></body></html>");
902
- server.close();
903
- });
904
- server.on("error", (err) => {
905
- reject(err);
906
- });
907
- server.listen(1455, "127.0.0.1");
908
- const timeout = setTimeout(() => {
909
- if (!receivedCode) server.close();
910
- }, CALLBACK_TIMEOUT_MS);
911
- timeout.unref();
912
- const onAbort = () => {
913
- server.close();
914
- };
915
- signal.addEventListener("abort", onAbort, { once: true });
916
- server.on("close", () => {
917
- clearTimeout(timeout);
918
- signal.removeEventListener("abort", onAbort);
919
- if (receivedCode) {
920
- resolve(receivedCode);
921
- } else {
922
- reject(new Error("Server closed without receiving code"));
923
- }
924
- });
925
- });
926
- }
927
- async function exchangeOpenAICode(code, verifier) {
928
- const response = await fetch(TOKEN_URL, {
839
+ async function postForm(url, form) {
840
+ return fetch(url, {
929
841
  method: "POST",
930
842
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
931
- body: new URLSearchParams({
932
- grant_type: "authorization_code",
933
- client_id: CLIENT_ID2,
934
- code,
935
- redirect_uri: REDIRECT_URI,
936
- code_verifier: verifier
937
- }),
843
+ body: new URLSearchParams(form),
938
844
  signal: oauthRequestSignal()
939
845
  });
940
- if (!response.ok) {
941
- const text = await response.text();
942
- throw new Error(`OpenAI token exchange failed (${response.status}): ${text}`);
943
- }
944
- const data = await response.json();
945
- return {
946
- accessToken: data.access_token,
947
- refreshToken: data.refresh_token,
948
- expiresAt: Date.now() + data.expires_in * 1e3
949
- };
950
846
  }
951
- async function refreshOpenAIToken(refreshToken) {
952
- const response = await fetch(TOKEN_URL, {
953
- method: "POST",
954
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
955
- body: new URLSearchParams({
956
- grant_type: "refresh_token",
957
- refresh_token: refreshToken,
958
- client_id: CLIENT_ID2
959
- }),
960
- signal: oauthRequestSignal()
847
+ async function exchangeCode(input) {
848
+ const response = await postForm(TOKEN_URL, {
849
+ grant_type: "authorization_code",
850
+ client_id: input.clientId,
851
+ code: input.code,
852
+ code_verifier: input.verifier,
853
+ redirect_uri: input.redirectUri,
854
+ resource: RESOURCE
961
855
  });
962
856
  if (!response.ok) {
963
- const text = await response.text();
964
- throw new Error(`OpenAI token refresh failed (${response.status}): ${text}`);
857
+ const text = await response.text().catch(() => "");
858
+ throw new Error(`ChatGPT token exchange failed (${response.status}): ${text.slice(0, 300)}`);
965
859
  }
966
860
  const data = await response.json();
967
- const creds = {
861
+ if (!data.access_token || !data.refresh_token || !data.id_token) {
862
+ throw new Error("ChatGPT token response is missing tokens.");
863
+ }
864
+ return {
968
865
  accessToken: data.access_token,
969
866
  refreshToken: data.refresh_token,
970
- expiresAt: Date.now() + data.expires_in * 1e3
867
+ idToken: data.id_token,
868
+ expiresIn: data.expires_in ?? 3600,
869
+ scopes: (data.scope ?? "").split(/\s+/).filter(Boolean)
971
870
  };
972
- const accountId = getAccountId(creds.accessToken);
973
- if (accountId) {
974
- creds.accountId = accountId;
975
- }
976
- return creds;
977
871
  }
978
-
979
- // src/oauth/gemini.ts
980
- import http2 from "http";
981
- import crypto4 from "crypto";
982
- var CLIENT_ID_ENV = "GGCODER_GEMINI_OAUTH_CLIENT_ID";
983
- var CLIENT_SECRET_ENV = "GGCODER_GEMINI_OAUTH_CLIENT_SECRET";
984
- var DEFAULT_CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com";
985
- var DEFAULT_CLIENT_SECRET = "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";
986
- var AUTHORIZE_URL2 = "https://accounts.google.com/o/oauth2/v2/auth";
987
- var TOKEN_URL2 = "https://oauth2.googleapis.com/token";
988
- var CODE_ASSIST_BASE_URL = "https://cloudcode-pa.googleapis.com";
989
- var CODE_ASSIST_API_VERSION = "v1internal";
990
- var CODE_ASSIST_POST_RETRIES = 3;
991
- var CODE_ASSIST_POST_RETRY_DELAY_MS = 100;
992
- var SCOPE2 = [
993
- "https://www.googleapis.com/auth/cloud-platform",
994
- "https://www.googleapis.com/auth/userinfo.email",
995
- "https://www.googleapis.com/auth/userinfo.profile"
996
- ].join(" ");
997
- var USER_TIER_FREE = "free-tier";
998
- var USER_TIER_LEGACY = "legacy-tier";
999
- var USER_TIER_STANDARD = "standard-tier";
1000
- var VALIDATION_REQUIRED_REASON = "VALIDATION_REQUIRED";
1001
- var VPC_SC_REASON = "SECURITY_POLICY_VIOLATED";
1002
- var CodeAssistHttpError = class extends Error {
1003
- status;
1004
- body;
1005
- constructor(label, status, body) {
1006
- super(`Gemini Code Assist ${label} failed (${status}): ${body}`);
1007
- this.name = "CodeAssistHttpError";
1008
- this.status = status;
1009
- this.body = body;
1010
- }
1011
- };
1012
- async function loginGemini(callbacks) {
1013
- const { clientId: clientId2, clientSecret } = getGeminiOAuthClientCredentials();
1014
- const { verifier, challenge } = await generatePKCE();
1015
- const state = crypto4.randomBytes(32).toString("hex");
1016
- const redirectUri = await getLoopbackRedirectUri();
1017
- const url = new URL(AUTHORIZE_URL2);
1018
- url.searchParams.set("response_type", "code");
1019
- url.searchParams.set("client_id", clientId2);
1020
- url.searchParams.set("redirect_uri", redirectUri);
1021
- url.searchParams.set("scope", SCOPE2);
1022
- url.searchParams.set("access_type", "offline");
1023
- url.searchParams.set("prompt", "consent");
1024
- url.searchParams.set("code_challenge", challenge);
1025
- url.searchParams.set("code_challenge_method", "S256");
1026
- url.searchParams.set("state", state);
1027
- let code;
1028
- try {
1029
- code = await loginWithServer(url.toString(), redirectUri, state, callbacks);
1030
- } catch {
1031
- callbacks.onOpenUrl(url.toString());
1032
- const raw = await callbacks.onPromptCode(
1033
- "Could not start local server. Paste the callback URL or code from the browser:"
1034
- );
1035
- const parsed = parseAuthorizationInput2(raw);
1036
- if (!parsed.code) {
1037
- throw new Error("No authorization code found in input.");
1038
- }
1039
- if (parsed.state && parsed.state !== state) {
1040
- throw new Error("Invalid state. Please try again.");
1041
- }
1042
- code = parsed.code;
872
+ async function refreshChatGPTToken(creds) {
873
+ if (!creds.clientId) {
874
+ throw new Error("ChatGPT token refresh failed (401): no issued client ID saved");
1043
875
  }
1044
- const creds = await exchangeGeminiCode(code, verifier, redirectUri, clientId2, clientSecret);
1045
- callbacks.onStatus("Setting up Gemini Code Assist access...");
1046
- const projectId = await setupCodeAssistProject(creds.accessToken, callbacks);
1047
- return {
1048
- ...creds,
1049
- projectId
1050
- };
1051
- }
1052
- async function refreshGeminiToken(refreshToken) {
1053
- const { clientId: clientId2, clientSecret } = getGeminiOAuthClientCredentials();
1054
- const data = await postTokenRequest({
876
+ const response = await postForm(TOKEN_URL, {
1055
877
  grant_type: "refresh_token",
1056
- refresh_token: refreshToken,
1057
- client_id: clientId2,
1058
- client_secret: clientSecret
1059
- });
1060
- return {
1061
- accessToken: data.access_token,
1062
- refreshToken: data.refresh_token ?? refreshToken,
1063
- expiresAt: Date.now() + data.expires_in * 1e3 - 5 * 60 * 1e3
1064
- };
1065
- }
1066
- function getGeminiOAuthClientCredentials() {
1067
- const clientId2 = process.env[CLIENT_ID_ENV]?.trim() || DEFAULT_CLIENT_ID;
1068
- const clientSecret = process.env[CLIENT_SECRET_ENV]?.trim() || DEFAULT_CLIENT_SECRET;
1069
- return { clientId: clientId2, clientSecret };
1070
- }
1071
- async function getLoopbackRedirectUri() {
1072
- return new Promise((resolve, reject) => {
1073
- const server = http2.createServer();
1074
- server.listen(0, "127.0.0.1", () => {
1075
- const addr = server.address();
1076
- server.close(() => {
1077
- if (addr && typeof addr === "object") {
1078
- resolve(`http://127.0.0.1:${addr.port}/oauth2callback`);
1079
- } else {
1080
- reject(new Error("Failed to allocate OAuth callback port."));
1081
- }
1082
- });
1083
- });
1084
- server.on("error", reject);
878
+ client_id: creds.clientId,
879
+ refresh_token: creds.refreshToken,
880
+ resource: RESOURCE
1085
881
  });
1086
- }
1087
- function parseAuthorizationInput2(input) {
1088
- const value = input.trim();
1089
- if (!value) return {};
1090
- try {
1091
- const url = new URL(value);
1092
- return {
1093
- code: url.searchParams.get("code") ?? void 0,
1094
- state: url.searchParams.get("state") ?? void 0
1095
- };
1096
- } catch {
1097
- }
1098
- if (value.includes("code=")) {
1099
- const params = new URLSearchParams(value);
1100
- return {
1101
- code: params.get("code") ?? void 0,
1102
- state: params.get("state") ?? void 0
1103
- };
882
+ if (!response.ok) {
883
+ const text = await response.text().catch(() => "");
884
+ throw new Error(`ChatGPT token refresh failed (${response.status}): ${text.slice(0, 300)}`);
1104
885
  }
1105
- return { code: value };
1106
- }
1107
- async function loginWithServer(authUrl, redirectUri, expectedState, callbacks) {
1108
- const redirect = new URL(redirectUri);
1109
- const port = Number(redirect.port);
1110
- return new Promise((resolve, reject) => {
1111
- let receivedCode = null;
1112
- const server = http2.createServer((req, res) => {
1113
- const url = new URL(req.url || "", redirect.origin);
1114
- if (url.pathname !== redirect.pathname) {
1115
- res.statusCode = 404;
1116
- res.end("Not found");
1117
- return;
1118
- }
1119
- if (url.searchParams.get("state") !== expectedState) {
1120
- res.statusCode = 400;
1121
- res.end("State mismatch");
1122
- return;
1123
- }
1124
- receivedCode = url.searchParams.get("code");
1125
- res.writeHead(200, { "Content-Type": "text/html", Connection: "close" });
1126
- res.end("<html><body><h1>Login successful!</h1><p>You can close this tab.</p></body></html>");
1127
- server.close();
1128
- });
1129
- server.on("error", (err) => reject(err));
1130
- server.listen(port, "127.0.0.1", () => {
1131
- callbacks.onOpenUrl(authUrl);
1132
- callbacks.onStatus("Waiting for browser callback...");
1133
- });
1134
- const timeout = setTimeout(() => {
1135
- if (!receivedCode) server.close();
1136
- }, 12e4);
1137
- timeout.unref();
1138
- server.on("close", () => {
1139
- clearTimeout(timeout);
1140
- if (receivedCode) {
1141
- resolve(receivedCode);
1142
- } else {
1143
- reject(new Error("Server closed without receiving code."));
1144
- }
1145
- });
1146
- });
1147
- }
1148
- async function exchangeGeminiCode(code, verifier, redirectUri, clientId2, clientSecret) {
1149
- const data = await postTokenRequest({
1150
- grant_type: "authorization_code",
1151
- client_id: clientId2,
1152
- client_secret: clientSecret,
1153
- code,
1154
- redirect_uri: redirectUri,
1155
- code_verifier: verifier
1156
- });
1157
- if (!data.refresh_token) {
1158
- throw new Error("Gemini OAuth did not return a refresh token. Please try login again.");
886
+ const data = await response.json();
887
+ if (!data.access_token || !data.refresh_token) {
888
+ throw new Error("ChatGPT token refresh returned no tokens.");
1159
889
  }
890
+ const expiresIn = data.expires_in ?? 3600;
1160
891
  return {
1161
892
  accessToken: data.access_token,
1162
893
  refreshToken: data.refresh_token,
1163
- expiresAt: Date.now() + data.expires_in * 1e3 - 5 * 60 * 1e3
1164
- };
1165
- }
1166
- async function postTokenRequest(body) {
1167
- const response = await fetch(TOKEN_URL2, {
1168
- method: "POST",
1169
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
1170
- body: new URLSearchParams(body),
1171
- signal: oauthRequestSignal()
1172
- });
1173
- if (!response.ok) {
1174
- const text = await response.text();
1175
- throw new Error(`Gemini token request failed (${response.status}): ${text}`);
1176
- }
1177
- return await response.json();
1178
- }
1179
- async function setupCodeAssistProject(accessToken, callbacks) {
1180
- const envProject = process.env.GOOGLE_CLOUD_PROJECT ?? process.env.GOOGLE_CLOUD_PROJECT_ID;
1181
- if (envProject && /^\d+$/.test(envProject)) {
1182
- throw new Error("GOOGLE_CLOUD_PROJECT must be a project ID, not a numeric project number.");
1183
- }
1184
- const coreMetadata = {
1185
- ideType: "IDE_UNSPECIFIED",
1186
- platform: "PLATFORM_UNSPECIFIED",
1187
- pluginType: "GEMINI"
1188
- };
1189
- const projectMetadata = {
1190
- ...coreMetadata,
1191
- ...envProject ? { duetProject: envProject } : {}
1192
- };
1193
- let loadRes;
1194
- while (true) {
1195
- loadRes = await loadCodeAssist(accessToken, envProject, projectMetadata);
1196
- const validation = getValidationRequiredTier(loadRes);
1197
- if (!validation) break;
1198
- callbacks.onStatus(
1199
- `Gemini Code Assist requires account validation${validation.reasonMessage ? `: ${validation.reasonMessage}` : ""}`
1200
- );
1201
- callbacks.onOpenUrl(validation.validationUrl);
1202
- const answer = await callbacks.onPromptCode(
1203
- "Complete validation in the browser, then press Enter to retry (or type cancel):"
1204
- );
1205
- if (answer.trim().toLowerCase() === "cancel") {
1206
- throw new Error("Gemini Code Assist account validation was cancelled.");
1207
- }
1208
- }
1209
- if (loadRes.currentTier) {
1210
- const project2 = loadRes.cloudaicompanionProject ?? envProject;
1211
- if (!project2) throwProjectError(loadRes);
1212
- return project2;
1213
- }
1214
- const tier = getOnboardTier(loadRes);
1215
- const onboardReq = tier.id === USER_TIER_FREE ? {
1216
- tierId: tier.id,
1217
- cloudaicompanionProject: void 0,
1218
- metadata: coreMetadata
1219
- } : {
1220
- tierId: tier.id,
1221
- cloudaicompanionProject: envProject,
1222
- metadata: projectMetadata
894
+ expiresAt: Date.now() + expiresIn * 1e3,
895
+ expiresIn,
896
+ ...data.id_token ? { idToken: data.id_token } : {},
897
+ ...data.scope ? { scopes: data.scope.split(/\s+/).filter(Boolean) } : {}
1223
898
  };
1224
- let operation = await codeAssistPost(
1225
- accessToken,
1226
- "onboardUser",
1227
- onboardReq
1228
- );
1229
- while (!operation.done && operation.name) {
1230
- await new Promise((resolve) => setTimeout(resolve, 5e3));
1231
- operation = await codeAssistGet(accessToken, operation.name);
1232
- }
1233
- const project = operation.response?.cloudaicompanionProject?.id ?? envProject;
1234
- if (!project) throwProjectError(loadRes);
1235
- return project;
1236
899
  }
1237
- async function loadCodeAssist(accessToken, envProject, metadata) {
1238
- try {
1239
- return await codeAssistPost(accessToken, "loadCodeAssist", {
1240
- ...envProject ? { cloudaicompanionProject: envProject } : {},
1241
- metadata
1242
- });
1243
- } catch (err) {
1244
- if (err instanceof CodeAssistHttpError && isVpcScAffectedError(err)) {
1245
- return { currentTier: { id: USER_TIER_STANDARD } };
1246
- }
1247
- if (err instanceof CodeAssistHttpError && err.status === 403 && envProject === "cloudshell-gca") {
1248
- throw new Error(
1249
- "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=...",
1250
- { cause: err }
1251
- );
1252
- }
1253
- throw err;
1254
- }
1255
- }
1256
- function getValidationRequiredTier(response) {
1257
- return response.ineligibleTiers?.find(
1258
- (tier) => tier.reasonCode === VALIDATION_REQUIRED_REASON && typeof tier.validationUrl === "string"
1259
- );
1260
- }
1261
- function getOnboardTier(response) {
1262
- const defaultTier = response.allowedTiers?.find((tier) => tier.isDefault);
1263
- return defaultTier ?? { id: USER_TIER_LEGACY, name: "" };
1264
- }
1265
- function throwProjectError(response) {
1266
- const reasons = response.ineligibleTiers?.map((tier) => tier.reasonMessage ?? tier.tierName).filter((reason) => Boolean(reason));
1267
- if (reasons && reasons.length > 0) {
1268
- throw new Error(`Gemini Code Assist setup failed: ${reasons.join(", ")}`);
1269
- }
1270
- throw new Error(
1271
- "Gemini requires a Google Cloud project for this account. Set GOOGLE_CLOUD_PROJECT and try again."
1272
- );
1273
- }
1274
- async function codeAssistPost(accessToken, method, body) {
1275
- let lastError;
1276
- for (let attempt = 0; attempt <= CODE_ASSIST_POST_RETRIES; attempt++) {
900
+ async function revokeChatGPTSignIn(creds, options = {}) {
901
+ if (!creds.clientId || !creds.refreshToken) return false;
902
+ const attempts = options.attempts ?? 3;
903
+ for (let attempt = 0; attempt < attempts; attempt++) {
1277
904
  try {
1278
- return await codeAssistRequest(getCodeAssistMethodUrl(method), accessToken, method, {
1279
- method: "POST",
1280
- body: JSON.stringify(body)
905
+ const response = await postForm(REVOKE_URL, {
906
+ token: creds.refreshToken,
907
+ token_type_hint: "refresh_token",
908
+ client_id: creds.clientId
1281
909
  });
1282
- } catch (err) {
1283
- if (!(err instanceof CodeAssistHttpError) || attempt === CODE_ASSIST_POST_RETRIES || !shouldRetryCodeAssistStatus(err.status)) {
1284
- throw err;
1285
- }
1286
- lastError = err;
910
+ if (response.ok) return true;
911
+ if (response.status < 500) return false;
912
+ } catch {
913
+ }
914
+ if (attempt < attempts - 1) {
915
+ await new Promise((r) => setTimeout(r, (options.backoffMs ?? 500) * 2 ** attempt));
1287
916
  }
1288
- await new Promise((resolve) => setTimeout(resolve, CODE_ASSIST_POST_RETRY_DELAY_MS));
1289
- }
1290
- throw lastError ?? new Error(`Gemini Code Assist ${method} failed.`);
1291
- }
1292
- async function codeAssistGet(accessToken, operationName) {
1293
- return codeAssistRequest(getCodeAssistOperationUrl(operationName), accessToken, "operation", {
1294
- method: "GET"
1295
- });
1296
- }
1297
- async function codeAssistRequest(url, accessToken, label, init) {
1298
- const response = await fetch(url, {
1299
- ...init,
1300
- headers: codeAssistHeaders(accessToken),
1301
- signal: oauthRequestSignal()
1302
- });
1303
- if (!response.ok) {
1304
- const text = await response.text();
1305
- throw new CodeAssistHttpError(label, response.status, text);
1306
- }
1307
- return await response.json();
1308
- }
1309
- function getCodeAssistBaseUrl() {
1310
- const endpoint = process.env.CODE_ASSIST_ENDPOINT ?? CODE_ASSIST_BASE_URL;
1311
- const version = process.env.CODE_ASSIST_API_VERSION || CODE_ASSIST_API_VERSION;
1312
- return `${endpoint}/${version}`;
1313
- }
1314
- function getCodeAssistMethodUrl(method) {
1315
- return `${getCodeAssistBaseUrl()}:${method}`;
1316
- }
1317
- function getCodeAssistOperationUrl(operationName) {
1318
- return `${getCodeAssistBaseUrl()}/${operationName}`;
1319
- }
1320
- function shouldRetryCodeAssistStatus(status) {
1321
- return status === 429 || status === 499 || status >= 500 && status <= 599;
1322
- }
1323
- function isVpcScAffectedError(error) {
1324
- try {
1325
- const parsed = JSON.parse(error.body);
1326
- if (!parsed || typeof parsed !== "object" || !("error" in parsed)) return false;
1327
- const details = parsed.error?.details;
1328
- return Array.isArray(details) ? details.some(
1329
- (detail) => detail != null && typeof detail === "object" && "reason" in detail && detail.reason === VPC_SC_REASON
1330
- ) : false;
1331
- } catch {
1332
- return false;
1333
- }
1334
- }
1335
- function codeAssistHeaders(accessToken) {
1336
- return {
1337
- Authorization: `Bearer ${accessToken}`,
1338
- "Content-Type": "application/json",
1339
- "User-Agent": "google-gemini-cli",
1340
- "X-Goog-Api-Client": "gemini-cli/0.0.0"
1341
- };
1342
- }
1343
-
1344
- // src/oauth/xai.ts
1345
- var DEFAULT_CLIENT_ID2 = "b1a00492-073a-47ea-816f-4c329264a828";
1346
- var DEFAULT_ISSUER = "https://auth.x.ai";
1347
- var DEFAULT_CLI_BASE_URL = "https://cli-chat-proxy.grok.com/v1";
1348
- var OAUTH_SCOPE = "openid profile email offline_access grok-cli:access api:access";
1349
- var DEFAULT_GROK_CLI_VERSION = "1.0.67";
1350
- var DEVICE_TIMEOUT_FALLBACK_MS = 10 * 60 * 1e3;
1351
- var DEFAULT_TOKEN_LIFETIME_SECONDS = 3600;
1352
- function issuer() {
1353
- return (process.env.XAI_OAUTH_ISSUER ?? DEFAULT_ISSUER).replace(/\/+$/, "");
1354
- }
1355
- function clientId() {
1356
- return process.env.XAI_OAUTH_CLIENT_ID ?? DEFAULT_CLIENT_ID2;
1357
- }
1358
- function grokCliBaseUrl() {
1359
- return (process.env.XAI_CLI_BASE_URL ?? DEFAULT_CLI_BASE_URL).replace(/\/+$/, "");
1360
- }
1361
- function grokCliVersion() {
1362
- const raw = process.env.GROK_CLI_VERSION ?? DEFAULT_GROK_CLI_VERSION;
1363
- const cleaned = raw.replace(/[^\u0020-\u007E]/g, "").trim();
1364
- return cleaned.length > 0 ? cleaned : DEFAULT_GROK_CLI_VERSION;
1365
- }
1366
- function grokCliHeaders(modelId) {
1367
- return {
1368
- "X-XAI-Token-Auth": "xai-grok-cli",
1369
- "x-grok-client-version": grokCliVersion(),
1370
- "x-grok-client-identifier": "ggcoder",
1371
- ...modelId ? { "x-grok-model-override": modelId } : {}
1372
- };
1373
- }
1374
- function isGrokCliEndpoint(baseUrl) {
1375
- if (typeof baseUrl !== "string" || baseUrl.length === 0) return false;
1376
- const normalized = baseUrl.replace(/\/+$/, "");
1377
- return normalized === grokCliBaseUrl() || /(^|\.)grok\.com/i.test(normalized);
1378
- }
1379
- async function postForm2(endpoint, params) {
1380
- const response = await fetch(`${issuer()}${endpoint}`, {
1381
- method: "POST",
1382
- headers: {
1383
- "Content-Type": "application/x-www-form-urlencoded",
1384
- Accept: "application/json",
1385
- "User-Agent": `grok-cli/${grokCliVersion()}`
1386
- },
1387
- body: new URLSearchParams(params).toString(),
1388
- signal: oauthRequestSignal()
1389
- });
1390
- let data = {};
1391
- try {
1392
- const parsed = await response.json();
1393
- if (parsed && typeof parsed === "object") data = parsed;
1394
- } catch {
1395
- }
1396
- return { status: response.status, data };
1397
- }
1398
- function errorDetail2(data) {
1399
- const desc = data.error_description ?? data.message ?? data.error;
1400
- return typeof desc === "string" && desc.length > 0 ? desc : "unknown error";
1401
- }
1402
- function jwtExpirySeconds(token) {
1403
- const payload = token.split(".")[1];
1404
- if (!payload) return void 0;
1405
- try {
1406
- const json = Buffer.from(payload.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString(
1407
- "utf-8"
1408
- );
1409
- const claims = JSON.parse(json);
1410
- if (!claims || typeof claims !== "object") return void 0;
1411
- const exp = claims.exp;
1412
- return typeof exp === "number" && Number.isFinite(exp) && exp > 0 ? exp : void 0;
1413
- } catch {
1414
- return void 0;
1415
- }
1416
- }
1417
- function credsFromTokenResponse2(data, opts) {
1418
- const accessToken = data.access_token;
1419
- const responseRefreshToken = data.refresh_token;
1420
- if (typeof accessToken !== "string" || accessToken.length === 0) {
1421
- throw new Error("Grok OAuth response missing access_token.");
1422
- }
1423
- const refreshToken = typeof responseRefreshToken === "string" && responseRefreshToken.length > 0 ? responseRefreshToken : opts?.fallbackRefreshToken ?? "";
1424
- if (refreshToken.length === 0) {
1425
- throw new Error(
1426
- "Grok OAuth response missing refresh_token \u2014 the offline_access scope was not granted."
1427
- );
1428
- }
1429
- const responseExpiresIn = Number(data.expires_in);
1430
- let expiresIn;
1431
- if (Number.isFinite(responseExpiresIn) && responseExpiresIn > 0) {
1432
- expiresIn = responseExpiresIn;
1433
- } else {
1434
- const exp = jwtExpirySeconds(accessToken);
1435
- const fromJwt = exp !== void 0 ? exp - Math.floor(Date.now() / 1e3) : 0;
1436
- expiresIn = fromJwt > 0 ? fromJwt : DEFAULT_TOKEN_LIFETIME_SECONDS;
1437
- }
1438
- return {
1439
- accessToken,
1440
- refreshToken,
1441
- expiresAt: Date.now() + expiresIn * 1e3,
1442
- expiresIn,
1443
- baseUrl: grokCliBaseUrl()
1444
- };
1445
- }
1446
- async function requestDeviceAuthorization2() {
1447
- const { status, data } = await postForm2("/oauth2/device/code", {
1448
- client_id: clientId(),
1449
- scope: OAUTH_SCOPE
1450
- });
1451
- if (status !== 200) {
1452
- throw new Error(`Grok device authorization failed (${status}): ${errorDetail2(data)}`);
1453
- }
1454
- const userCode = data.user_code;
1455
- const deviceCode = data.device_code;
1456
- const verificationUriComplete = data.verification_uri_complete;
1457
- if (typeof userCode !== "string" || typeof deviceCode !== "string") {
1458
- throw new Error("Grok device authorization response missing user_code/device_code.");
1459
- }
1460
- return {
1461
- userCode,
1462
- deviceCode,
1463
- verificationUri: typeof data.verification_uri === "string" ? data.verification_uri : "",
1464
- verificationUriComplete: typeof verificationUriComplete === "string" ? verificationUriComplete : "",
1465
- interval: Number(data.interval ?? 5) || 5,
1466
- // RFC 8628 §3.2: the server states how long the device code lives. Honor it
1467
- // rather than imposing our own budget — polling a code we know is dead only
1468
- // burns requests, and a longer local window would keep a user waiting past
1469
- // the point the code can ever succeed.
1470
- expiresInMs: (Number(data.expires_in) || 0) > 0 ? Number(data.expires_in) * 1e3 : DEVICE_TIMEOUT_FALLBACK_MS
1471
- };
1472
- }
1473
- async function pollDeviceToken2(deviceCode) {
1474
- const { status, data } = await postForm2("/oauth2/token", {
1475
- client_id: clientId(),
1476
- device_code: deviceCode,
1477
- grant_type: "urn:ietf:params:oauth:grant-type:device_code"
1478
- });
1479
- if (status === 200 && typeof data.access_token === "string") {
1480
- return { kind: "success", creds: credsFromTokenResponse2(data) };
1481
- }
1482
- if (status >= 500) {
1483
- throw new Error(`Grok token polling server error (${status}): ${errorDetail2(data)}`);
1484
- }
1485
- const errorCode = typeof data.error === "string" ? data.error : "unknown_error";
1486
- switch (errorCode) {
1487
- case "authorization_pending":
1488
- return { kind: "pending" };
1489
- case "slow_down":
1490
- return { kind: "slow_down" };
1491
- case "expired_token":
1492
- return { kind: "expired" };
1493
- case "access_denied":
1494
- return { kind: "denied" };
1495
- default:
1496
- throw new Error(`Grok token polling failed (${status}): ${errorDetail2(data)}`);
1497
917
  }
1498
- }
1499
- function sleep2(ms) {
1500
- return new Promise((resolve) => {
1501
- setTimeout(resolve, ms);
1502
- });
1503
- }
1504
- async function loginXai(callbacks) {
1505
- const auth = await requestDeviceAuthorization2();
1506
- callbacks.onStatus(
1507
- `Visit ${auth.verificationUri || auth.verificationUriComplete} and enter code: ${auth.userCode}`
918
+ return false;
919
+ }
920
+ function decodeSegment(segment) {
921
+ return JSON.parse(Buffer.from(segment, "base64url").toString("utf-8"));
922
+ }
923
+ async function verifyIdToken(idToken, expected) {
924
+ const [headerSeg, payloadSeg, signatureSeg] = idToken.split(".");
925
+ if (!headerSeg || !payloadSeg || !signatureSeg) throw new Error("Malformed ChatGPT ID token.");
926
+ const header = decodeSegment(headerSeg);
927
+ if (header.alg !== "RS256") throw new Error("Unexpected ChatGPT ID token algorithm.");
928
+ const response = await fetch(JWKS_URL, { signal: oauthRequestSignal() });
929
+ if (!response.ok) throw new Error(`Could not load OpenAI signing keys (${response.status}).`);
930
+ const { keys = [] } = await response.json();
931
+ const jwk = keys.find((k) => k.kid === header.kid);
932
+ if (!jwk) throw new Error("ChatGPT ID token is signed with an unknown key.");
933
+ const valid = crypto3.verify(
934
+ "RSA-SHA256",
935
+ Buffer.from(`${headerSeg}.${payloadSeg}`),
936
+ crypto3.createPublicKey({ key: jwk, format: "jwk" }),
937
+ Buffer.from(signatureSeg, "base64url")
1508
938
  );
1509
- callbacks.onOpenUrl(auth.verificationUriComplete || auth.verificationUri);
1510
- callbacks.onStatus("Waiting for you to authorize in the browser...");
1511
- const deadline = Date.now() + auth.expiresInMs;
1512
- let interval = Math.max(auth.interval, 1);
1513
- while (Date.now() < deadline) {
1514
- const remaining = deadline - Date.now();
1515
- await sleep2(Math.min(interval * 1e3, remaining));
1516
- const result = await pollDeviceToken2(auth.deviceCode);
1517
- if (result.kind === "success") return result.creds;
1518
- if (result.kind === "denied") {
1519
- throw new Error("Grok authorization was denied.");
1520
- }
1521
- if (result.kind === "expired") {
1522
- throw new Error("Grok device code expired. Please run login again.");
1523
- }
1524
- if (result.kind === "slow_down") {
1525
- interval += 5;
1526
- }
939
+ if (!valid) throw new Error("ChatGPT ID token signature is invalid.");
940
+ const claims = decodeSegment(payloadSeg);
941
+ const now = Math.floor((expected.now ?? Date.now()) / 1e3);
942
+ const aud = claims.aud;
943
+ const audiences = Array.isArray(aud) ? aud : [aud];
944
+ if (claims.iss !== ISSUER) throw new Error("ChatGPT ID token has the wrong issuer.");
945
+ if (!audiences.includes(expected.clientId)) {
946
+ throw new Error("ChatGPT ID token was issued to another app.");
1527
947
  }
1528
- throw new Error("Grok login timed out. Please run login again.");
1529
- }
1530
- async function refreshXaiToken(refreshToken) {
1531
- const { status, data } = await postForm2("/oauth2/token", {
1532
- client_id: clientId(),
1533
- grant_type: "refresh_token",
1534
- refresh_token: refreshToken
1535
- });
1536
- if (status === 200 && typeof data.access_token === "string") {
1537
- return credsFromTokenResponse2(data, { fallbackRefreshToken: refreshToken });
948
+ if (typeof claims.exp !== "number" || claims.exp + ID_TOKEN_CLOCK_SKEW_S < now) {
949
+ throw new Error("ChatGPT ID token has expired.");
1538
950
  }
1539
- const errorCode = typeof data.error === "string" ? data.error : "";
1540
- throw new Error(`Grok token refresh failed (${status}): ${errorCode || errorDetail2(data)}`);
951
+ if (claims.nonce !== expected.nonce) throw new Error("ChatGPT ID token nonce mismatch.");
952
+ return claims;
1541
953
  }
1542
954
 
1543
955
  // src/file-lock.ts
1544
- import fs3 from "fs/promises";
956
+ import fs4 from "fs/promises";
1545
957
  import { setTimeout as setTimeout2 } from "timers/promises";
1546
958
  var STALE_TIMEOUT_MS = 1e4;
1547
959
  var RETRY_INTERVAL_MS = 50;
@@ -1562,7 +974,7 @@ async function acquireLock(lockPath) {
1562
974
  while (true) {
1563
975
  try {
1564
976
  const info = { pid: process.pid, timestamp: Date.now() };
1565
- await fs3.writeFile(lockPath, JSON.stringify(info), { flag: "wx" });
977
+ await fs4.writeFile(lockPath, JSON.stringify(info), { flag: "wx" });
1566
978
  return;
1567
979
  } catch (err) {
1568
980
  const code = err.code;
@@ -1575,26 +987,26 @@ async function acquireLock(lockPath) {
1575
987
  continue;
1576
988
  }
1577
989
  try {
1578
- const content = await fs3.readFile(lockPath, "utf-8");
990
+ const content = await fs4.readFile(lockPath, "utf-8");
1579
991
  const info = JSON.parse(content);
1580
992
  const isProcessAlive = isAlive(info.pid);
1581
993
  const isStale = Date.now() - info.timestamp > STALE_TIMEOUT_MS;
1582
994
  if (!isProcessAlive || isStale) {
1583
- await fs3.unlink(lockPath).catch(() => {
995
+ await fs4.unlink(lockPath).catch(() => {
1584
996
  });
1585
997
  continue;
1586
998
  }
1587
999
  } catch {
1588
- const stat = await fs3.stat(lockPath).catch(() => null);
1000
+ const stat = await fs4.stat(lockPath).catch(() => null);
1589
1001
  if (!stat) continue;
1590
1002
  if (Date.now() - stat.mtimeMs > STALE_TIMEOUT_MS) {
1591
- await fs3.unlink(lockPath).catch(() => {
1003
+ await fs4.unlink(lockPath).catch(() => {
1592
1004
  });
1593
1005
  continue;
1594
1006
  }
1595
1007
  }
1596
1008
  if (Date.now() - startTime > MAX_WAIT_MS) {
1597
- await fs3.unlink(lockPath).catch(() => {
1009
+ await fs4.unlink(lockPath).catch(() => {
1598
1010
  });
1599
1011
  continue;
1600
1012
  }
@@ -1604,14 +1016,14 @@ async function acquireLock(lockPath) {
1604
1016
  }
1605
1017
  async function lockFileExists(lockPath) {
1606
1018
  try {
1607
- await fs3.stat(lockPath);
1019
+ await fs4.stat(lockPath);
1608
1020
  return true;
1609
1021
  } catch (err) {
1610
1022
  return err.code !== "ENOENT";
1611
1023
  }
1612
1024
  }
1613
1025
  async function releaseLock(lockPath) {
1614
- await fs3.unlink(lockPath).catch(() => {
1026
+ await fs4.unlink(lockPath).catch(() => {
1615
1027
  });
1616
1028
  }
1617
1029
  function isAlive(pid) {
@@ -1625,7 +1037,7 @@ function isAlive(pid) {
1625
1037
  }
1626
1038
 
1627
1039
  // src/logger.ts
1628
- import fs4 from "fs";
1040
+ import fs5 from "fs";
1629
1041
  import path3 from "path";
1630
1042
  import { randomBytes } from "crypto";
1631
1043
  import { environmentSecrets, redactText, redactValue } from "@kenkaiiii/gg-ai";
@@ -1643,14 +1055,14 @@ var cleanups = [];
1643
1055
  var exactSecrets = [];
1644
1056
  function rotateIfNeeded(filePath, force = false) {
1645
1057
  try {
1646
- const st = fs4.statSync(filePath);
1058
+ const st = fs5.statSync(filePath);
1647
1059
  if (!force && st.size <= MAX_BYTES - MIN_HEADROOM_BYTES) return;
1648
1060
  const rotated = `${filePath}.1`;
1649
1061
  try {
1650
- fs4.unlinkSync(rotated);
1062
+ fs5.unlinkSync(rotated);
1651
1063
  } catch {
1652
1064
  }
1653
- fs4.renameSync(filePath, rotated);
1065
+ fs5.renameSync(filePath, rotated);
1654
1066
  } catch {
1655
1067
  }
1656
1068
  }
@@ -1658,18 +1070,18 @@ function rotateOpenLog() {
1658
1070
  if (fd === null || logPath === null) return false;
1659
1071
  const sizeBefore = fileBytes;
1660
1072
  try {
1661
- fs4.closeSync(fd);
1073
+ fs5.closeSync(fd);
1662
1074
  } catch {
1663
1075
  }
1664
1076
  fd = null;
1665
1077
  rotateIfNeeded(logPath, true);
1666
1078
  try {
1667
- fd = fs4.openSync(logPath, "a");
1668
- fileBytes = fs4.fstatSync(fd).size;
1079
+ fd = fs5.openSync(logPath, "a");
1080
+ fileBytes = fs5.fstatSync(fd).size;
1669
1081
  } catch {
1670
1082
  if (fd !== null) {
1671
1083
  try {
1672
- fs4.closeSync(fd);
1084
+ fs5.closeSync(fd);
1673
1085
  } catch {
1674
1086
  }
1675
1087
  }
@@ -1685,22 +1097,22 @@ function openLog(filePath, name) {
1685
1097
  appName = name;
1686
1098
  exactSecrets = environmentSecrets(process.env);
1687
1099
  try {
1688
- fs4.mkdirSync(path3.dirname(filePath), { recursive: true, mode: 448 });
1100
+ fs5.mkdirSync(path3.dirname(filePath), { recursive: true, mode: 448 });
1689
1101
  } catch {
1690
1102
  }
1691
1103
  rotateIfNeeded(filePath);
1692
1104
  try {
1693
- fd = fs4.openSync(filePath, "a");
1105
+ fd = fs5.openSync(filePath, "a");
1694
1106
  } catch {
1695
1107
  fd = null;
1696
1108
  fileBytes = 0;
1697
1109
  return false;
1698
1110
  }
1699
1111
  try {
1700
- fileBytes = fs4.fstatSync(fd).size;
1112
+ fileBytes = fs5.fstatSync(fd).size;
1701
1113
  } catch {
1702
1114
  try {
1703
- fs4.closeSync(fd);
1115
+ fs5.closeSync(fd);
1704
1116
  } catch {
1705
1117
  }
1706
1118
  fd = null;
@@ -1713,7 +1125,7 @@ function openLog(filePath, name) {
1713
1125
  sessionId = randomBytes(4).toString("hex");
1714
1126
  try {
1715
1127
  if (fileBytes < MAX_BYTES) {
1716
- const written = fs4.writeSync(fd, "\n");
1128
+ const written = fs5.writeSync(fd, "\n");
1717
1129
  fileBytes += written;
1718
1130
  processBytes += written;
1719
1131
  }
@@ -1748,9 +1160,9 @@ function log(level, category, message, data) {
1748
1160
  `;
1749
1161
  try {
1750
1162
  if (fileBytes + Buffer.byteLength(capLine) <= MAX_BYTES) {
1751
- fileBytes += fs4.writeSync(fd, capLine);
1163
+ fileBytes += fs5.writeSync(fd, capLine);
1752
1164
  }
1753
- fs4.closeSync(fd);
1165
+ fs5.closeSync(fd);
1754
1166
  } catch {
1755
1167
  }
1756
1168
  fd = null;
@@ -1760,7 +1172,7 @@ function log(level, category, message, data) {
1760
1172
  if (fileBytes + lineBytes > MAX_BYTES && !rotationFailed && !rotateOpenLog()) return;
1761
1173
  if (fd === null) return;
1762
1174
  try {
1763
- const written = fs4.writeSync(fd, line);
1175
+ const written = fs5.writeSync(fd, line);
1764
1176
  fileBytes += written;
1765
1177
  processBytes += written;
1766
1178
  } catch {
@@ -1773,7 +1185,7 @@ function closeLogger(opts) {
1773
1185
  if (fd !== null) {
1774
1186
  if (opts?.shutdownLine !== false) log("INFO", "shutdown", `${appName} shutting down`);
1775
1187
  try {
1776
- if (fd !== null) fs4.closeSync(fd);
1188
+ if (fd !== null) fs5.closeSync(fd);
1777
1189
  } catch {
1778
1190
  }
1779
1191
  }
@@ -1789,7 +1201,7 @@ function closeLogger(opts) {
1789
1201
  }
1790
1202
 
1791
1203
  // src/hidden-providers.ts
1792
- var HIDDEN_PROVIDERS = ["gemini"];
1204
+ var HIDDEN_PROVIDERS = [];
1793
1205
  function isHiddenProvider(provider) {
1794
1206
  return HIDDEN_PROVIDERS.includes(provider);
1795
1207
  }
@@ -1880,22 +1292,76 @@ async function detectXiaomiTokenPlanBaseUrl(apiKey, options = {}) {
1880
1292
  }
1881
1293
 
1882
1294
  // src/auth-storage.ts
1883
- var MOONSHOT_OAUTH_KEY = "moonshot-oauth";
1884
- var XAI_OAUTH_KEY = "xai-oauth";
1885
- var DUAL_AUTH_PROVIDERS = [
1295
+ var OPENAI_CHATGPT_KEY = "openai-chatgpt";
1296
+ var KIMI_CODE_KEY = "kimi-code";
1297
+ var RETIRED_SIGN_INS = [
1298
+ {
1299
+ provider: "anthropic",
1300
+ storageKey: "anthropic",
1301
+ message: "Your old Claude sign-in no longer works. Sign in with Claude again to keep using your subscription."
1302
+ },
1303
+ {
1304
+ provider: "openai",
1305
+ storageKey: "openai",
1306
+ message: "Your old ChatGPT sign-in no longer works. Continue with ChatGPT again, or add an OpenAI API key."
1307
+ },
1886
1308
  {
1887
1309
  provider: "moonshot",
1888
- oauthKey: MOONSHOT_OAUTH_KEY,
1889
- oauthLabel: "Kimi OAuth",
1890
- apiKeyLabel: "Moonshot API key",
1891
- restoreHint: 'Run "ggcoder login" and choose Kimi OAuth to restore OAuth auth.'
1310
+ storageKey: "moonshot-oauth",
1311
+ message: "Kimi sign-in was removed. Paste a Kimi Code plan key from the Kimi Code Console, or a Moonshot API key."
1892
1312
  },
1893
1313
  {
1894
1314
  provider: "xai",
1895
- oauthKey: XAI_OAUTH_KEY,
1896
- oauthLabel: "Grok OAuth",
1897
- apiKeyLabel: "xAI API key",
1898
- restoreHint: 'Run "ggcoder login" and choose Grok OAuth to restore OAuth auth.'
1315
+ storageKey: "xai-oauth",
1316
+ 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."
1317
+ },
1318
+ {
1319
+ provider: "gemini",
1320
+ storageKey: "gemini",
1321
+ message: "Google sign-in was removed. Add a Gemini API key from Google AI Studio instead."
1322
+ }
1323
+ ];
1324
+ function isRetiredEntry(retired, value) {
1325
+ if (!value) return false;
1326
+ switch (retired.storageKey) {
1327
+ case "anthropic":
1328
+ return isLegacyClaudeOAuth(value);
1329
+ // These slots now hold API keys; only a refreshable sign-in token is retired.
1330
+ case "openai":
1331
+ case "gemini":
1332
+ return Boolean(value.refreshToken) || Boolean(value.accountId) || Boolean(value.projectId);
1333
+ default:
1334
+ return true;
1335
+ }
1336
+ }
1337
+ function isRetiredStorageKey(data, storageKey) {
1338
+ return RETIRED_SIGN_INS.some(
1339
+ (r) => r.storageKey === storageKey && isRetiredEntry(r, data[storageKey])
1340
+ );
1341
+ }
1342
+ function usableEntry(data, storageKey) {
1343
+ return isRetiredStorageKey(data, storageKey) ? void 0 : data[storageKey];
1344
+ }
1345
+ function storageKeyProvider(storageKey) {
1346
+ if (storageKey === OPENAI_CHATGPT_KEY) return "openai";
1347
+ if (storageKey === KIMI_CODE_KEY) return "moonshot";
1348
+ if (storageKey === XIAOMI_CREDITS_KEY) return "xiaomi";
1349
+ if (storageKey === GEMINI_API_KEY) return "gemini";
1350
+ return storageKey;
1351
+ }
1352
+ function apiKeyStorageKeys(provider) {
1353
+ if (provider === "moonshot") return [KIMI_CODE_KEY, "moonshot"];
1354
+ if (provider === "xiaomi") return ["xiaomi", XIAOMI_CREDITS_KEY];
1355
+ if (provider === "gemini") return [GEMINI_API_KEY];
1356
+ return [provider];
1357
+ }
1358
+ var DUAL_AUTH_PROVIDERS = [
1359
+ {
1360
+ provider: "openai",
1361
+ oauthKey: OPENAI_CHATGPT_KEY,
1362
+ oauthLabel: "ChatGPT plan",
1363
+ apiKeyLabel: "OpenAI API key",
1364
+ restoreHint: 'Run "ggcoder login" and choose Continue with ChatGPT to use your ChatGPT plan again.'
1899
1365
  }
1900
1366
  ];
1901
1367
  function dualAuthProvider(provider) {
@@ -1920,14 +1386,20 @@ function activeBaseUrlEntry(data, provider) {
1920
1386
  const dual = dualAuthProvider(provider);
1921
1387
  if (dual) {
1922
1388
  const oauth = data[dual.oauthKey];
1389
+ const apiKey = usableEntry(data, dual.provider);
1923
1390
  if (oauth) {
1924
1391
  const exhaustedUntil = oauth.usageExhaustedUntil ?? 0;
1925
- if (Date.now() < exhaustedUntil && data[dual.provider]) return data[dual.provider];
1392
+ if (Date.now() < exhaustedUntil && apiKey) return apiKey;
1926
1393
  return oauth;
1927
1394
  }
1928
- return data[dual.provider];
1395
+ return apiKey;
1929
1396
  }
1930
- return data[provider];
1397
+ const key = apiKeyStorageKeys(provider).find((k) => usableEntry(data, k));
1398
+ const entry = key ? data[key] : void 0;
1399
+ if (key === KIMI_CODE_KEY && entry && !entry.baseUrl) {
1400
+ return { ...entry, baseUrl: KIMI_CODE_DEFAULT_BASE_URL };
1401
+ }
1402
+ return entry;
1931
1403
  }
1932
1404
  var MINIMAX_REGION_RETRY_MS = 5 * 60 * 1e3;
1933
1405
  var miniMaxRegionProbes = /* @__PURE__ */ new Map();
@@ -1938,9 +1410,17 @@ function cachedMiniMaxRegion(apiKey) {
1938
1410
  miniMaxRegionProbes.set(apiKey, { at: Date.now(), result });
1939
1411
  return result;
1940
1412
  }
1413
+ var kimiRegionProbes = /* @__PURE__ */ new Map();
1414
+ function cachedKimiRegion(apiKey) {
1415
+ const cached = kimiRegionProbes.get(apiKey);
1416
+ if (cached && Date.now() - cached.at < MINIMAX_REGION_RETRY_MS) return cached.result;
1417
+ const result = detectKimiCodeBaseUrl(apiKey).catch(() => void 0);
1418
+ kimiRegionProbes.set(apiKey, { at: Date.now(), result });
1419
+ return result;
1420
+ }
1941
1421
  function readStoredBaseUrlSync(authFile, provider) {
1942
1422
  try {
1943
- const data = JSON.parse(readFileSync2(authFile, "utf-8"));
1423
+ const data = JSON.parse(readFileSync(authFile, "utf-8"));
1944
1424
  return activeBaseUrlEntry(data, provider)?.baseUrl;
1945
1425
  } catch {
1946
1426
  return void 0;
@@ -1953,21 +1433,7 @@ function refreshThresholdMs(creds) {
1953
1433
  return Math.max(MIN_REFRESH_THRESHOLD_MS, lifetimeMs * REFRESH_THRESHOLD_RATIO);
1954
1434
  }
1955
1435
  var USAGE_EXHAUSTED_DEFAULT_MS = 15 * 60 * 1e3;
1956
- var STATIC_API_KEY_PROVIDERS = /* @__PURE__ */ new Set([
1957
- // Anthropic is API-key only; Claude subscriptions run through Claude Code.
1958
- "anthropic",
1959
- "glm",
1960
- "moonshot",
1961
- "xiaomi",
1962
- "minimax",
1963
- "deepseek",
1964
- "openrouter",
1965
- "huggingface",
1966
- "sakana",
1967
- "xai",
1968
- // Local endpoints: a fixed (usually placeholder) key, never refreshable.
1969
- "local"
1970
- ]);
1436
+ var REFRESHABLE_STORAGE_KEYS = /* @__PURE__ */ new Set([OPENAI_CHATGPT_KEY]);
1971
1437
  var AuthStorage = class {
1972
1438
  data = {};
1973
1439
  filePath;
@@ -1988,10 +1454,12 @@ var AuthStorage = class {
1988
1454
  refreshLocks = /* @__PURE__ */ new Map();
1989
1455
  detectMiniMaxRegion;
1990
1456
  detectXiaomiCluster;
1457
+ detectKimiRegion;
1991
1458
  constructor(filePath, options) {
1992
1459
  this.filePath = filePath ?? getAppPaths().authFile;
1993
1460
  this.detectMiniMaxRegion = options?.detectMiniMaxBaseUrl ?? cachedMiniMaxRegion;
1994
1461
  this.detectXiaomiCluster = options?.detectXiaomiTokenPlanBaseUrl ?? detectXiaomiTokenPlanBaseUrl;
1462
+ this.detectKimiRegion = options?.detectKimiCodeBaseUrl ?? cachedKimiRegion;
1995
1463
  }
1996
1464
  /** Path to the on-disk auth file. Useful for status output. */
1997
1465
  get path() {
@@ -2019,22 +1487,15 @@ var AuthStorage = class {
2019
1487
  return keys.find((key) => Boolean(this.data[key]));
2020
1488
  }
2021
1489
  /**
2022
- * True if the user has any usable auth for the logical provider. For a
2023
- * dual-auth provider (Kimi/Grok) either the OAuth credential or the API key
2024
- * satisfies it.
1490
+ * True if the user has any usable auth for the logical provider: the plan
1491
+ * sign-in or any of its API keys. A retired sign-in doesn't count.
2025
1492
  */
2026
1493
  async hasProviderAuth(provider) {
2027
1494
  if (isHiddenProvider(provider)) return false;
2028
1495
  await this.ensureFresh();
2029
1496
  const dual = dualAuthProvider(provider);
2030
1497
  if (dual) {
2031
- return Boolean(this.data[dual.oauthKey] || this.data[dual.provider]);
2032
- }
2033
- if (provider === "xiaomi") {
2034
- return Boolean(this.data["xiaomi"] || this.data[XIAOMI_CREDITS_KEY]);
2035
- }
2036
- if (provider === "gemini") {
2037
- return Boolean(this.data["gemini"] || this.data[GEMINI_API_KEY]);
1498
+ return Boolean(this.data[dual.oauthKey] || usableEntry(this.data, dual.provider));
2038
1499
  }
2039
1500
  if (provider === "anthropic") {
2040
1501
  return Boolean(this.data.anthropic) && !isLegacyClaudeOAuth(this.data.anthropic) || isClaudeCodeAuthMarker(this.data[CLAUDE_CODE_AUTH_KEY]);
@@ -2042,7 +1503,25 @@ var AuthStorage = class {
2042
1503
  if (provider === "local") {
2043
1504
  return Object.keys(this.data).some((key) => key.startsWith(LOCAL_AUTH_KEY_PREFIX));
2044
1505
  }
2045
- return Boolean(this.data[provider]);
1506
+ return apiKeyStorageKeys(provider).some((key) => Boolean(usableEntry(this.data, key)));
1507
+ }
1508
+ /** True if any of `provider`'s API keys is stored. A retired sign-in in a key slot doesn't count. */
1509
+ async hasApiKey(provider) {
1510
+ await this.ensureFresh();
1511
+ if (provider === "anthropic" && isLegacyClaudeOAuth(this.data.anthropic)) return false;
1512
+ return apiKeyStorageKeys(provider).some((key) => Boolean(usableEntry(this.data, key)));
1513
+ }
1514
+ /**
1515
+ * `provider`'s first stored API key, never its plan sign-in. For calls that
1516
+ * only an API key may make (OpenAI's Images API isn't on the ChatGPT plan).
1517
+ */
1518
+ async resolveApiKey(provider) {
1519
+ await this.ensureFresh();
1520
+ for (const key of apiKeyStorageKeys(provider)) {
1521
+ const creds = usableEntry(this.data, key);
1522
+ if (creds && !(provider === "anthropic" && isLegacyClaudeOAuth(creds))) return creds;
1523
+ }
1524
+ throw new NotLoggedInError(provider);
2046
1525
  }
2047
1526
  /** True when the user signed in with Claude through Claude Code (marker only, no token). */
2048
1527
  async hasClaudeCodeLogin() {
@@ -2056,17 +1535,24 @@ var AuthStorage = class {
2056
1535
  if (isLegacyClaudeOAuth(latest.anthropic)) delete latest.anthropic;
2057
1536
  });
2058
1537
  }
1538
+ /** Retired sign-ins still on disk, so the UI can ask the user to reconnect. */
1539
+ async legacySignIns() {
1540
+ await this.ensureFresh();
1541
+ return RETIRED_SIGN_INS.filter((r) => isRetiredEntry(r, this.data[r.storageKey]));
1542
+ }
2059
1543
  /**
2060
- * Remove an old GG Claude sign-in token (`sk-ant-oat…`), if any. True when one
2061
- * was removed, so the caller can tell the user once to sign in with Claude again.
1544
+ * Remove `provider`'s retired sign-in, if any. True when one was removed, so
1545
+ * the caller can tell the user once to reconnect.
2062
1546
  */
2063
- async takeLegacyClaudeSignIn() {
1547
+ async takeLegacySignIn(provider) {
1548
+ const retired = RETIRED_SIGN_INS.find((r) => r.provider === provider);
1549
+ if (!retired) return false;
2064
1550
  await this.ensureFresh();
2065
- if (!isLegacyClaudeOAuth(this.data.anthropic)) return false;
1551
+ if (!isRetiredEntry(retired, this.data[retired.storageKey])) return false;
2066
1552
  let removed = false;
2067
1553
  await this.mutateLatest((latest) => {
2068
- if (!isLegacyClaudeOAuth(latest.anthropic)) return;
2069
- delete latest.anthropic;
1554
+ if (!isRetiredEntry(retired, latest[retired.storageKey])) return;
1555
+ delete latest[retired.storageKey];
2070
1556
  removed = true;
2071
1557
  });
2072
1558
  return removed;
@@ -2107,18 +1593,18 @@ var AuthStorage = class {
2107
1593
  const oauthCreds = dual ? this.data[dual.oauthKey] : void 0;
2108
1594
  if (dual && oauthCreds) {
2109
1595
  const exhaustedUntil = oauthCreds.usageExhaustedUntil ?? 0;
2110
- const apiKeyActive = Date.now() < exhaustedUntil && Boolean(this.data[dual.provider]);
1596
+ const apiKeyActive = Date.now() < exhaustedUntil && Boolean(usableEntry(this.data, dual.provider));
2111
1597
  if (!apiKeyActive) return false;
2112
1598
  }
2113
- return STATIC_API_KEY_PROVIDERS.has(provider);
1599
+ return !REFRESHABLE_STORAGE_KEYS.has(provider);
2114
1600
  }
2115
1601
  /**
2116
1602
  * The base URL on the credential that is active right now, if any.
2117
1603
  * Synchronous — call only after load()/resolveCredentials() populated the
2118
- * snapshot. For a dual-auth provider this is the subscription endpoint (Kimi
2119
- * For Coding, the Grok CLI proxy) whenever the OAuth entry is the one
2120
- * resolveCredentials would serve (i.e. not currently usage-exhausted with an
2121
- * API key configured).
1604
+ * snapshot. For a dual-auth provider this is the plan endpoint whenever the
1605
+ * sign-in is the one resolveCredentials would serve (i.e. not currently
1606
+ * usage-exhausted with an API key configured); for a plan/pay-per-token key
1607
+ * split it is the plan key's endpoint when one is stored.
2122
1608
  */
2123
1609
  getStoredBaseUrl(provider) {
2124
1610
  return activeBaseUrlEntry(this.data, provider)?.baseUrl;
@@ -2127,7 +1613,7 @@ var AuthStorage = class {
2127
1613
  const first = !this.loaded;
2128
1614
  await withFileLock(this.filePath, async () => {
2129
1615
  try {
2130
- const content = await fs5.readFile(this.filePath, "utf-8");
1616
+ const content = await fs6.readFile(this.filePath, "utf-8");
2131
1617
  this.data = JSON.parse(content);
2132
1618
  if (first) {
2133
1619
  log("INFO", "auth", `Loaded credentials from ${this.filePath}`, {
@@ -2173,7 +1659,7 @@ var AuthStorage = class {
2173
1659
  }
2174
1660
  let changed;
2175
1661
  try {
2176
- const stat = await fs5.stat(this.filePath);
1662
+ const stat = await fs6.stat(this.filePath);
2177
1663
  changed = stat.ino !== this.snapshotIno || stat.mtimeMs !== this.snapshotMtimeMs || stat.size !== this.snapshotSize;
2178
1664
  } catch {
2179
1665
  changed = this.snapshotSize !== -1;
@@ -2186,7 +1672,7 @@ var AuthStorage = class {
2186
1672
  */
2187
1673
  async rememberSnapshot() {
2188
1674
  try {
2189
- const stat = await fs5.stat(this.filePath);
1675
+ const stat = await fs6.stat(this.filePath);
2190
1676
  this.snapshotMtimeMs = stat.mtimeMs;
2191
1677
  this.snapshotSize = stat.size;
2192
1678
  this.snapshotIno = stat.ino;
@@ -2241,9 +1727,34 @@ var AuthStorage = class {
2241
1727
  });
2242
1728
  return { ...creds, baseUrl };
2243
1729
  }
2244
- async setCredentials(provider, creds) {
1730
+ /**
1731
+ * A Kimi Code key saved without an endpoint gets the host that issued it
1732
+ * (`api.kimi.com` or `api.kimi.ai`), saved so the check runs once.
1733
+ */
1734
+ async withKimiRegion(creds) {
1735
+ const baseUrl = await this.detectKimiRegion(creds.accessToken);
1736
+ if (!baseUrl) return { ...creds, baseUrl: KIMI_CODE_DEFAULT_BASE_URL };
1737
+ log("INFO", "auth", `Kimi Code key belongs to ${baseUrl} \u2014 saved for future requests`);
1738
+ await this.mutateLatest((latest) => {
1739
+ const current = latest[KIMI_CODE_KEY];
1740
+ if (current?.accessToken === creds.accessToken && !current.baseUrl) {
1741
+ current.baseUrl = baseUrl;
1742
+ }
1743
+ });
1744
+ return { ...creds, baseUrl };
1745
+ }
1746
+ /**
1747
+ * Store `creds` under `storageKey`. Connecting a provider again also removes
1748
+ * its retired sign-in, which is what the reconnect notice asked for.
1749
+ */
1750
+ async setCredentials(storageKey, creds) {
1751
+ const provider = storageKeyProvider(storageKey);
2245
1752
  await this.mutateLatest((latest) => {
2246
- latest[provider] = creds;
1753
+ for (const retired of RETIRED_SIGN_INS) {
1754
+ if (retired.provider !== provider || retired.storageKey === storageKey) continue;
1755
+ if (isRetiredEntry(retired, latest[retired.storageKey])) delete latest[retired.storageKey];
1756
+ }
1757
+ latest[storageKey] = creds;
2247
1758
  });
2248
1759
  }
2249
1760
  /**
@@ -2280,9 +1791,9 @@ var AuthStorage = class {
2280
1791
  * Mark the credential stored under `storageKey` as usage-exhausted until
2281
1792
  * `resetsAt` (unix SECONDS, from the provider's rate-limit response) or a
2282
1793
  * 15-minute default when no reset time is known. While the mark is in the
2283
- * future, `resolveCredentials("moonshot")` serves the Moonshot API key
2284
- * instead of the subscription OAuth credential (when both are configured) — OAuth
2285
- * stays the preferred credential and is retried automatically once the mark
1794
+ * future, `resolveCredentials("openai")` serves the OpenAI API key instead of
1795
+ * the ChatGPT plan sign-in (when both are configured) — the plan stays the
1796
+ * preferred credential and is retried automatically once the mark
2286
1797
  * lapses. Persisted to auth.json so a restart (or another gg-app window)
2287
1798
  * doesn't burn a request rediscovering the same exhausted window. No-op if
2288
1799
  * nothing is stored under `storageKey`.
@@ -2327,12 +1838,12 @@ var AuthStorage = class {
2327
1838
  await this.ensureFresh();
2328
1839
  }
2329
1840
  const directStorageKeys = opts?.storageKeys && !(opts.storageKeys.length === 1 && opts.storageKeys[0] === provider) ? opts.storageKeys : providerStorageKeys(provider);
2330
- if (!directStorageKeys.some((key) => Boolean(this.data[key])) && !(provider === "anthropic" && this.data[CLAUDE_CODE_AUTH_KEY])) {
1841
+ if (!directStorageKeys.some((key) => Boolean(usableEntry(this.data, key))) && !(provider === "anthropic" && this.data[CLAUDE_CODE_AUTH_KEY])) {
2331
1842
  await this.reloadLatest();
2332
1843
  }
2333
1844
  if (opts?.storageKeys && !(opts.storageKeys.length === 1 && opts.storageKeys[0] === provider)) {
2334
1845
  for (const key of opts.storageKeys) {
2335
- const creds2 = this.data[key];
1846
+ const creds2 = usableEntry(this.data, key);
2336
1847
  if (!creds2) continue;
2337
1848
  if (key !== provider && dualAuthProviderByOAuthKey(key)) {
2338
1849
  return await this.resolveCredentials(key, {
@@ -2346,6 +1857,7 @@ var AuthStorage = class {
2346
1857
  ...opts.rejectedToken !== void 0 ? { rejectedToken: opts.rejectedToken } : {}
2347
1858
  });
2348
1859
  }
1860
+ if (key === KIMI_CODE_KEY && !creds2.baseUrl) return await this.withKimiRegion(creds2);
2349
1861
  return creds2;
2350
1862
  }
2351
1863
  throw new NotLoggedInError(provider);
@@ -2358,15 +1870,16 @@ var AuthStorage = class {
2358
1870
  }
2359
1871
  const dual = dualAuthProvider(provider);
2360
1872
  const dualOAuthCreds = dual ? this.data[dual.oauthKey] : void 0;
1873
+ const dualApiKey = dual ? usableEntry(this.data, dual.provider) : void 0;
2361
1874
  if (dual && dualOAuthCreds) {
2362
1875
  const exhaustedUntil = dualOAuthCreds.usageExhaustedUntil ?? 0;
2363
- if (Date.now() < exhaustedUntil && this.data[dual.provider]) {
1876
+ if (Date.now() < exhaustedUntil && dualApiKey) {
2364
1877
  log(
2365
1878
  "WARN",
2366
1879
  "auth",
2367
1880
  `${dual.oauthLabel} usage window is exhausted \u2014 using the ${dual.apiKeyLabel} until ${new Date(exhaustedUntil).toISOString()} (OAuth resumes automatically).`
2368
1881
  );
2369
- return this.data[dual.provider];
1882
+ return dualApiKey;
2370
1883
  }
2371
1884
  try {
2372
1885
  return await this.resolveCredentials(dual.oauthKey, {
@@ -2374,23 +1887,24 @@ var AuthStorage = class {
2374
1887
  ...opts?.rejectedToken !== void 0 ? { rejectedToken: opts.rejectedToken } : {}
2375
1888
  });
2376
1889
  } catch (err) {
2377
- if (err instanceof NotLoggedInError && this.data[dual.provider]) {
1890
+ if (err instanceof NotLoggedInError && dualApiKey) {
2378
1891
  log(
2379
1892
  "WARN",
2380
1893
  "auth",
2381
1894
  `${dual.oauthLabel} credential is no longer valid \u2014 falling back to the ${dual.apiKeyLabel}. ${dual.restoreHint}`
2382
1895
  );
2383
- return this.data[dual.provider];
1896
+ return dualApiKey;
2384
1897
  }
2385
1898
  throw err;
2386
1899
  }
2387
1900
  }
2388
- const creds = this.data[provider];
1901
+ const creds = usableEntry(this.data, provider);
2389
1902
  if (!creds) {
2390
1903
  throw new NotLoggedInError(provider);
2391
1904
  }
2392
- if (STATIC_API_KEY_PROVIDERS.has(provider)) {
1905
+ if (!REFRESHABLE_STORAGE_KEYS.has(provider)) {
2393
1906
  if (provider === "minimax" && !creds.baseUrl) return await this.withMiniMaxRegion(creds);
1907
+ if (provider === KIMI_CODE_KEY && !creds.baseUrl) return await this.withKimiRegion(creds);
2394
1908
  return creds;
2395
1909
  }
2396
1910
  if (!opts?.forceRefresh && Date.now() < creds.expiresAt - refreshThresholdMs(creds)) {
@@ -2418,13 +1932,14 @@ var AuthStorage = class {
2418
1932
  this.data = latest;
2419
1933
  return latestCreds;
2420
1934
  }
2421
- const refreshFn = provider === "gemini" ? refreshGeminiToken : provider === MOONSHOT_OAUTH_KEY ? refreshKimiToken : provider === XAI_OAUTH_KEY ? refreshXaiToken : refreshOpenAIToken;
2422
1935
  let refreshed;
2423
1936
  try {
2424
- refreshed = await refreshFn(latestCreds.refreshToken);
1937
+ refreshed = await refreshChatGPTToken(latestCreds);
2425
1938
  } catch (err) {
2426
1939
  const msg = err instanceof Error ? err.message : String(err);
2427
- const isAuthFailure = /\((401|400)\)/.test(msg) || /invalid_grant|invalid_token|invalid.*refresh/i.test(msg) || /unauthorized/i.test(msg);
1940
+ const isAuthFailure = /\((401|400)\)/.test(msg) || /invalid_grant|invalid_token|invalid.*refresh|token_expired|refresh_token_(?:expired|invalidated|reused)/i.test(
1941
+ msg
1942
+ ) || /unauthorized/i.test(msg);
2428
1943
  if (isAuthFailure) {
2429
1944
  const current = await readAuthData(this.filePath);
2430
1945
  const currentCreds = current[provider];
@@ -2444,15 +1959,10 @@ var AuthStorage = class {
2444
1959
  }
2445
1960
  throw err;
2446
1961
  }
2447
- if (!refreshed.accountId && latestCreds.accountId) {
2448
- refreshed.accountId = latestCreds.accountId;
2449
- }
2450
- if (!refreshed.projectId && latestCreds.projectId) {
2451
- refreshed.projectId = latestCreds.projectId;
2452
- }
2453
- if (!refreshed.baseUrl && latestCreds.baseUrl) {
2454
- refreshed.baseUrl = latestCreds.baseUrl;
2455
- }
1962
+ refreshed = {
1963
+ ...pick(latestCreds, ["accountId", "baseUrl", "clientId", "idToken", "scopes"]),
1964
+ ...refreshed
1965
+ };
2456
1966
  latest[provider] = refreshed;
2457
1967
  this.data = latest;
2458
1968
  await atomicWriteFile(this.filePath, JSON.stringify(latest, null, 2));
@@ -2475,9 +1985,14 @@ var AuthStorage = class {
2475
1985
  return creds.accessToken;
2476
1986
  }
2477
1987
  };
1988
+ function pick(value, keys) {
1989
+ const out = {};
1990
+ for (const key of keys) if (value[key] !== void 0) out[key] = value[key];
1991
+ return out;
1992
+ }
2478
1993
  async function readAuthData(filePath) {
2479
1994
  try {
2480
- const content = await fs5.readFile(filePath, "utf-8");
1995
+ const content = await fs6.readFile(filePath, "utf-8");
2481
1996
  return JSON.parse(content);
2482
1997
  } catch (error) {
2483
1998
  if (error.code === "ENOENT") return {};
@@ -2485,12 +2000,12 @@ async function readAuthData(filePath) {
2485
2000
  }
2486
2001
  }
2487
2002
  async function atomicWriteFile(filePath, content) {
2488
- const tmpPath = `${filePath}.${process.pid}.${Date.now()}.${crypto5.randomUUID().slice(0, 8)}.tmp`;
2003
+ const tmpPath = `${filePath}.${process.pid}.${Date.now()}.${crypto4.randomUUID().slice(0, 8)}.tmp`;
2489
2004
  try {
2490
- await fs5.writeFile(tmpPath, content, { encoding: "utf-8", mode: 384 });
2491
- await fs5.rename(tmpPath, filePath);
2005
+ await fs6.writeFile(tmpPath, content, { encoding: "utf-8", mode: 384 });
2006
+ await fs6.rename(tmpPath, filePath);
2492
2007
  } catch (err) {
2493
- await fs5.unlink(tmpPath).catch(() => {
2008
+ await fs6.unlink(tmpPath).catch(() => {
2494
2009
  });
2495
2010
  throw err;
2496
2011
  }
@@ -2522,7 +2037,8 @@ var CODEX_FAST_BILLING = {
2522
2037
  detail: "Codex Fast mode uses your ChatGPT plan limits at 2.5x the standard rate (purchased credits at 2x).",
2523
2038
  paid: false
2524
2039
  };
2525
- var GEMINI_AUTH_KEYS = [GEMINI_API_KEY, "gemini"];
2040
+ var GEMINI_AUTH_KEYS = [GEMINI_API_KEY];
2041
+ var KIMI_AUTH_KEYS = [KIMI_CODE_KEY, "moonshot"];
2526
2042
  var MODELS = [
2527
2043
  // ── Anthropic ──────────────────────────────────────────
2528
2044
  // NOTE: Claude Mythos 5.1 (`claude-mythos-5-1`, 2026-09-01) is kept commented
@@ -2652,14 +2168,13 @@ var MODELS = [
2652
2168
  {
2653
2169
  // GPT-6 Astra — "Our most capable model for complex, demanding work."
2654
2170
  // (Codex catalog priority 2, listed for every ChatGPT plan, requires a
2655
- // Codex client >= 0.153.0 — see CODEX_CLIENT_VERSION). Same split as 5.6:
2656
- // 1.05M on the public Responses API, 272K on the ChatGPT OAuth route
2171
+ // Codex client >= 0.153.0). Same split as 5.6:
2172
+ // 1.05M on the public Responses API, 272K on the ChatGPT plan route
2657
2173
  // (openai/codex models.json, `gpt-6-astra`). Reasoning ladder low → medium
2658
2174
  // → high → xhigh → max → ultra; `ultra` is the Codex orchestration preset
2659
2175
  // (multi_agent v2) and is Codex-only — the public API tops out at `max`.
2660
- // Note: through a plain API key OpenAI requires the Responses API for tool
2661
- // calling on Astra, so the Chat Completions path is text-only; the OAuth
2662
- // Codex route is the supported way to use it as an agent.
2176
+ // OpenAI requires the Responses API for tool calling on Astra, so both
2177
+ // the ChatGPT plan and API keys ride gg-ai's Responses transport.
2663
2178
  id: "gpt-6-astra",
2664
2179
  name: "GPT-6 Astra",
2665
2180
  provider: "openai",
@@ -2702,9 +2217,7 @@ var MODELS = [
2702
2217
  // next start.
2703
2218
  {
2704
2219
  // GPT-6.1 Sol — "Latest workhorse model for coding and everyday work."
2705
- // (Codex priority 1, default low). The catalog says Codex client >= 0.153.0,
2706
- // but the ChatGPT backend only serves it from 0.159.0 (see
2707
- // CODEX_CLIENT_VERSION). $2/$10 MTok, cached input $0.10. Ladder low →
2220
+ // (Codex priority 1, default low). $2/$10 MTok, cached input $0.10. Ladder low →
2708
2221
  // medium → high → xhigh → max → ultra; ultra is the Codex orchestration
2709
2222
  // preset (xhigh effort on the wire + proactive local subagent delegation).
2710
2223
  id: "gpt-6.1-sol",
@@ -2891,8 +2404,8 @@ var MODELS = [
2891
2404
  maxThinkingLevel: "high",
2892
2405
  authStorageKeys: GEMINI_AUTH_KEYS
2893
2406
  },
2894
- // Keep 3.1 Flash Lite first for the working OAuth default and fast-model routing.
2895
- // New GA models are opt-in; Code Assist access varies by account. Gemini 3.7
2407
+ // Keep 3.1 Flash Lite first for the default and fast-model routing.
2408
+ // Gemini 3.7
2896
2409
  // Flash is retired: Google deprecated it on 2026-10-08 and routes every
2897
2410
  // `gemini-3.7-flash` request to 3.8 Flash (ai.google.dev/gemini-api/docs/
2898
2411
  // changelog), so a saved session on it falls back to the provider default.
@@ -2925,9 +2438,7 @@ var MODELS = [
2925
2438
  authStorageKeys: GEMINI_AUTH_KEYS
2926
2439
  },
2927
2440
  {
2928
- // Wire name `gemini-3-flash` — the Code Assist (OAuth) backend rejects the
2929
- // display string `gemini-3.5-flash` with a 404, so gemini-cli keeps this
2930
- // alternative name (SECONDARY_GEMINI_3_5_FLASH_MODEL) for that endpoint.
2441
+ // Wire name `gemini-3-flash`, the id Google serves 3.5 Flash under.
2931
2442
  id: "gemini-3-flash",
2932
2443
  name: "Gemini 3.5 Flash",
2933
2444
  provider: "gemini",
@@ -2942,9 +2453,7 @@ var MODELS = [
2942
2453
  authStorageKeys: GEMINI_AUTH_KEYS
2943
2454
  },
2944
2455
  {
2945
- // Gemini 3.1 Pro is public preview — gated behind Code Assist preview
2946
- // enablement, so free/personal OAuth accounts 404 on it (see
2947
- // ACCOUNT_GATED_MODELS in gg-ai's gemini provider).
2456
+ // Gemini 3.1 Pro is public preview.
2948
2457
  id: "gemini-3.1-pro-preview",
2949
2458
  name: "Gemini 3.1 Pro (Preview)",
2950
2459
  provider: "gemini",
@@ -2961,8 +2470,8 @@ var MODELS = [
2961
2470
  // ── Moonshot (Kimi) ────────────────────────────────────
2962
2471
  // K3 is Kimi's 2.8T-parameter flagship for long-horizon coding, knowledge
2963
2472
  // work, and deep reasoning. Its effort ladder is server-declared as
2964
- // low/high/max on both the public API (default max) and the Kimi For Coding
2965
- // OAuth endpoint (default high); thinking can also be fully disabled.
2473
+ // low/high/max on both the public API (default max) and the Kimi Code plan
2474
+ // endpoint (default high); thinking can also be fully disabled.
2966
2475
  {
2967
2476
  id: "kimi-k3",
2968
2477
  name: "Kimi K3",
@@ -2976,13 +2485,14 @@ var MODELS = [
2976
2485
  supportsVideo: true,
2977
2486
  maxVideoBytes: 100 * 1024 * 1024,
2978
2487
  costTier: "high",
2979
- maxThinkingLevel: "max"
2488
+ maxThinkingLevel: "max",
2489
+ authStorageKeys: KIMI_AUTH_KEYS
2980
2490
  },
2981
- // K2.8 Preview (2026-09-11) is served only on the Kimi For Coding OAuth
2982
- // endpoint, under its rolling `kimi-for-coding` id (live /models, 2026-09-28:
2491
+ // K2.8 Preview (2026-09-11) is served only on the Kimi Code plan endpoint,
2492
+ // under its rolling `kimi-for-coding` id (live /models, 2026-09-28:
2983
2493
  // display_name "K2.8 Preview", 1M context, image + video input, efforts
2984
- // low/high/max default max). The public API-key endpoint does not serve it,
2985
- // so it resolves from the Kimi sign-in credential only.
2494
+ // low/high/max default max). The public Moonshot API does not serve it, so
2495
+ // it resolves from a Kimi Code plan key only.
2986
2496
  {
2987
2497
  id: "kimi-for-coding",
2988
2498
  name: "Kimi K2.8 Preview",
@@ -2995,7 +2505,7 @@ var MODELS = [
2995
2505
  maxVideoBytes: 100 * 1024 * 1024,
2996
2506
  costTier: "medium",
2997
2507
  maxThinkingLevel: "max",
2998
- authStorageKeys: [MOONSHOT_OAUTH_KEY]
2508
+ authStorageKeys: [KIMI_CODE_KEY]
2999
2509
  },
3000
2510
  // K2.7 Code is requested by its pinned id (not the `kimi-for-coding` alias
3001
2511
  // that moved to K2.8), so it stays the real K2.7 on both endpoints.
@@ -3010,7 +2520,8 @@ var MODELS = [
3010
2520
  supportsVideo: true,
3011
2521
  maxVideoBytes: 100 * 1024 * 1024,
3012
2522
  costTier: "medium",
3013
- maxThinkingLevel: "high"
2523
+ maxThinkingLevel: "high",
2524
+ authStorageKeys: KIMI_AUTH_KEYS
3014
2525
  },
3015
2526
  // K2.7 Code HighSpeed — the same model as K2.7 Code served at ~180 tok/s
3016
2527
  // (up to ~260 on short contexts), 256K context (platform.kimi.ai model list,
@@ -3029,7 +2540,8 @@ var MODELS = [
3029
2540
  supportsVideo: true,
3030
2541
  maxVideoBytes: 100 * 1024 * 1024,
3031
2542
  costTier: "medium",
3032
- maxThinkingLevel: "high"
2543
+ maxThinkingLevel: "high",
2544
+ authStorageKeys: KIMI_AUTH_KEYS
3033
2545
  },
3034
2546
  // ── Z.AI (GLM) ─────────────────────────────────────────
3035
2547
  // Two GLM entries, both live on the coding endpoint (verified against its
@@ -3442,11 +2954,10 @@ function getSummaryModel(provider, currentModelId) {
3442
2954
  }
3443
2955
 
3444
2956
  export {
3445
- kimiCodeBaseUrl,
3446
- kimiCodingHeaders,
2957
+ KIMI_CODE_DEFAULT_BASE_URL,
2958
+ KIMI_CODE_OVERSEAS_BASE_URL,
2959
+ detectKimiCodeBaseUrl,
3447
2960
  isKimiCodingEndpoint,
3448
- loginKimi,
3449
- refreshKimiToken,
3450
2961
  CLAUDE_CODE_MANIFEST,
3451
2962
  CLAUDE_CODE_AUTH_KEY,
3452
2963
  CLAUDE_CODE_AUTH_MARKER,
@@ -3465,15 +2976,18 @@ export {
3465
2976
  claudeCodeCallbackUrl,
3466
2977
  loginClaudeCode,
3467
2978
  generatePKCE,
3468
- loginOpenAI,
3469
- refreshOpenAIToken,
3470
- loginGemini,
3471
- refreshGeminiToken,
3472
- grokCliBaseUrl,
3473
- grokCliHeaders,
3474
- isGrokCliEndpoint,
3475
- loginXai,
3476
- refreshXaiToken,
2979
+ CHATGPT_PLAN_SCOPE,
2980
+ CHATGPT_PLAN_BASE_URL,
2981
+ CHATGPT_USAGE_URL,
2982
+ CHATGPT_PLAN_NOT_ENABLED_MESSAGE,
2983
+ ChatGPTPlanNotEnabledError,
2984
+ getChatGPTHostId,
2985
+ loginChatGPT,
2986
+ fetchChatGPTPlanModels,
2987
+ parseCallbackInput,
2988
+ refreshChatGPTToken,
2989
+ revokeChatGPTSignIn,
2990
+ verifyIdToken,
3477
2991
  withFileLock,
3478
2992
  openLog,
3479
2993
  getSessionId,
@@ -3488,8 +3002,11 @@ export {
3488
3002
  detectMiniMaxBaseUrl,
3489
3003
  XIAOMI_TOKEN_PLAN_BASE_URLS,
3490
3004
  detectXiaomiTokenPlanBaseUrl,
3491
- MOONSHOT_OAUTH_KEY,
3492
- XAI_OAUTH_KEY,
3005
+ OPENAI_CHATGPT_KEY,
3006
+ KIMI_CODE_KEY,
3007
+ RETIRED_SIGN_INS,
3008
+ storageKeyProvider,
3009
+ apiKeyStorageKeys,
3493
3010
  dualAuthProvider,
3494
3011
  dualAuthProviderByOAuthKey,
3495
3012
  oauthStorageKey,
@@ -3524,4 +3041,4 @@ export {
3524
3041
  getDefaultThinkingLevel,
3525
3042
  getSummaryModel
3526
3043
  };
3527
- //# sourceMappingURL=chunk-JLEBGMG4.js.map
3044
+ //# sourceMappingURL=chunk-J7W5HS6X.js.map