@kenkaiiii/gg-core 5.93.0 → 5.95.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -32,34 +32,55 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  AUTH_PROVIDERS: () => AUTH_PROVIDERS,
34
34
  AuthStorage: () => AuthStorage,
35
+ CHATGPT_PLAN_BASE_URL: () => CHATGPT_PLAN_BASE_URL,
36
+ CHATGPT_PLAN_NOT_ENABLED_MESSAGE: () => CHATGPT_PLAN_NOT_ENABLED_MESSAGE,
37
+ CHATGPT_PLAN_SCOPE: () => CHATGPT_PLAN_SCOPE,
38
+ CHATGPT_USAGE_URL: () => CHATGPT_USAGE_URL,
39
+ CLAUDE_CODE_AUTH_KEY: () => CLAUDE_CODE_AUTH_KEY,
40
+ CLAUDE_CODE_AUTH_MARKER: () => CLAUDE_CODE_AUTH_MARKER,
41
+ CLAUDE_CODE_MANIFEST: () => CLAUDE_CODE_MANIFEST,
42
+ ChatGPTPlanNotEnabledError: () => ChatGPTPlanNotEnabledError,
35
43
  DEFAULT_LOCAL_ENDPOINTS: () => DEFAULT_LOCAL_ENDPOINTS,
36
44
  DEFAULT_MAX_VIDEO_BYTES: () => DEFAULT_MAX_VIDEO_BYTES,
37
45
  FALLBACK_CONTEXT_WINDOW: () => FALLBACK_CONTEXT_WINDOW,
38
46
  GEMINI_API_KEY: () => GEMINI_API_KEY,
39
47
  GEMINI_API_KEY_BASE_URL: () => GEMINI_API_KEY_BASE_URL,
40
48
  HIDDEN_PROVIDERS: () => HIDDEN_PROVIDERS,
49
+ KIMI_CODE_DEFAULT_BASE_URL: () => KIMI_CODE_DEFAULT_BASE_URL,
50
+ KIMI_CODE_KEY: () => KIMI_CODE_KEY,
51
+ KIMI_CODE_OVERSEAS_BASE_URL: () => KIMI_CODE_OVERSEAS_BASE_URL,
41
52
  LOCAL_API_KEY_PLACEHOLDER: () => LOCAL_API_KEY_PLACEHOLDER,
42
53
  LOCAL_AUTH_KEY_PREFIX: () => LOCAL_AUTH_KEY_PREFIX,
43
54
  MINIMAX_CN_BASE_URL: () => MINIMAX_CN_BASE_URL,
44
55
  MINIMAX_GLOBAL_BASE_URL: () => MINIMAX_GLOBAL_BASE_URL,
45
56
  MODELS: () => MODELS,
46
- MOONSHOT_OAUTH_KEY: () => MOONSHOT_OAUTH_KEY,
47
57
  NotLoggedInError: () => NotLoggedInError,
58
+ OPENAI_CHATGPT_KEY: () => OPENAI_CHATGPT_KEY,
48
59
  OPENROUTER_DEFAULT_BASE_URL: () => OPENROUTER_DEFAULT_BASE_URL,
49
60
  PLAN_MODE_THINKING_CAP: () => PLAN_MODE_THINKING_CAP,
61
+ RETIRED_SIGN_INS: () => RETIRED_SIGN_INS,
50
62
  SubscriptionUsageError: () => SubscriptionUsageError,
51
63
  TelegramBot: () => TelegramBot,
52
- XAI_OAUTH_KEY: () => XAI_OAUTH_KEY,
53
64
  XIAOMI_CREDITS_KEY: () => XIAOMI_CREDITS_KEY,
54
65
  XIAOMI_TOKEN_PLAN_BASE_URLS: () => XIAOMI_TOKEN_PLAN_BASE_URLS,
66
+ apiKeyStorageKeys: () => apiKeyStorageKeys,
55
67
  authPriorityNote: () => authPriorityNote,
56
68
  clampThinkingForPlanMode: () => clampThinkingForPlanMode,
69
+ claudeCodeAuthStatus: () => claudeCodeAuthStatus,
70
+ claudeCodeBinaryPath: () => claudeCodeBinaryPath,
71
+ claudeCodeCallbackUrl: () => claudeCodeCallbackUrl,
72
+ claudeCodeConfigDir: () => claudeCodeConfigDir,
73
+ claudeCodeEnv: () => claudeCodeEnv,
74
+ claudeCodeHome: () => claudeCodeHome,
75
+ claudeCodePlatformKey: () => claudeCodePlatformKey,
76
+ clearClaudeCodeUsage: () => clearClaudeCodeUsage,
57
77
  clearLocalDiscoveryCache: () => clearLocalDiscoveryCache,
58
78
  clearRuntimeModels: () => clearRuntimeModels,
59
79
  closeLogger: () => closeLogger,
60
80
  createAutoUpdater: () => createAutoUpdater,
61
81
  decodeOggOpus: () => decodeOggOpus,
62
82
  describeAuthMethods: () => describeAuthMethods,
83
+ detectKimiCodeBaseUrl: () => detectKimiCodeBaseUrl,
63
84
  detectMiniMaxBaseUrl: () => detectMiniMaxBaseUrl,
64
85
  detectXiaomiTokenPlanBaseUrl: () => detectXiaomiTokenPlanBaseUrl,
65
86
  discoverLocalModels: () => discoverLocalModels,
@@ -67,8 +88,11 @@ __export(index_exports, {
67
88
  dualAuthProvider: () => dualAuthProvider,
68
89
  dualAuthProviderByOAuthKey: () => dualAuthProviderByOAuthKey,
69
90
  endpointRoot: () => endpointRoot,
91
+ ensureClaudeCodeBinary: () => ensureClaudeCodeBinary,
92
+ fetchChatGPTPlanModels: () => fetchChatGPTPlanModels,
70
93
  fetchSubscriptionUsage: () => fetchSubscriptionUsage,
71
94
  findProbedModel: () => findProbedModel,
95
+ formatClaudeCodeInstallProgress: () => formatClaudeCodeInstallProgress,
72
96
  formatLocalModelId: () => formatLocalModelId,
73
97
  generatePKCE: () => generatePKCE,
74
98
  getAllModels: () => getAllModels,
@@ -76,8 +100,7 @@ __export(index_exports, {
76
100
  getAuthProvider: () => getAuthProvider,
77
101
  getAuthStorageKey: () => getAuthStorageKey,
78
102
  getAuthStorageKeys: () => getAuthStorageKeys,
79
- getClaudeCliUserAgent: () => import_gg_ai.getClaudeCliUserAgent,
80
- getClaudeCodeVersion: () => import_gg_ai.getClaudeCodeVersion,
103
+ getChatGPTHostId: () => getChatGPTHostId,
81
104
  getContextWindow: () => getContextWindow,
82
105
  getDefaultModel: () => getDefaultModel,
83
106
  getDefaultThinkingLevel: () => getDefaultThinkingLevel,
@@ -93,59 +116,90 @@ __export(index_exports, {
93
116
  getSupportedThinkingLevels: () => getSupportedThinkingLevels,
94
117
  getToolResultCharLimit: () => getToolResultCharLimit,
95
118
  getVideoByteLimit: () => getVideoByteLimit,
96
- grokCliBaseUrl: () => grokCliBaseUrl,
97
- grokCliHeaders: () => grokCliHeaders,
98
- isGrokCliEndpoint: () => isGrokCliEndpoint,
119
+ isClaudeCodeAuthMarker: () => isClaudeCodeAuthMarker,
99
120
  isHiddenProvider: () => isHiddenProvider,
100
121
  isKimiCodingEndpoint: () => isKimiCodingEndpoint,
122
+ isLegacyClaudeOAuth: () => isLegacyClaudeOAuth,
101
123
  isLocalModelId: () => isLocalModelId,
102
124
  isLoggerOpen: () => isLoggerOpen,
103
125
  isModelLoaded: () => isModelLoaded,
104
126
  isPaidModel: () => isPaidModel,
105
127
  isThinkingLevelSupported: () => isThinkingLevelSupported,
106
- kimiCodeBaseUrl: () => kimiCodeBaseUrl,
107
- kimiCodingHeaders: () => kimiCodingHeaders,
108
128
  localAuthStorageKey: () => localAuthStorageKey,
109
129
  localModelShortName: () => localModelShortName,
110
130
  log: () => log,
111
- loginAnthropic: () => loginAnthropic,
112
- loginGemini: () => loginGemini,
113
- loginKimi: () => loginKimi,
114
- loginOpenAI: () => loginOpenAI,
115
- loginXai: () => loginXai,
131
+ loginChatGPT: () => loginChatGPT,
132
+ loginClaudeCode: () => loginClaudeCode,
133
+ logoutClaudeCode: () => logoutClaudeCode,
134
+ mergeClaudeCodeRateLimit: () => mergeClaudeCodeRateLimit,
116
135
  oauthStorageKey: () => oauthStorageKey,
117
136
  ollamaServedContext: () => ollamaServedContext,
118
137
  openLog: () => openLog,
138
+ parseCallbackInput: () => parseCallbackInput,
119
139
  parseLocalModelId: () => parseLocalModelId,
120
140
  parseOpenRouterCatalog: () => parseOpenRouterCatalog,
121
- pendingClaudeCodeVersionRefresh: () => import_gg_ai.pendingClaudeCodeVersionRefresh,
122
141
  probeEndpoint: () => probeEndpoint,
123
142
  providerStorageKeys: () => providerStorageKeys,
143
+ readClaudeCodeUsage: () => readClaudeCodeUsage,
124
144
  readStoredBaseUrlSync: () => readStoredBaseUrlSync,
125
- refreshAnthropicToken: () => refreshAnthropicToken,
126
- refreshGeminiToken: () => refreshGeminiToken,
127
- refreshKimiToken: () => refreshKimiToken,
128
- refreshOpenAIToken: () => refreshOpenAIToken,
129
- refreshXaiToken: () => refreshXaiToken,
145
+ recordClaudeCodeRateLimit: () => recordClaudeCodeRateLimit,
146
+ refreshChatGPTToken: () => refreshChatGPTToken,
130
147
  registerLogCleanup: () => registerLogCleanup,
131
148
  registerRuntimeModels: () => registerRuntimeModels,
149
+ requiredKeyLabel: () => requiredKeyLabel,
132
150
  resample: () => resample,
151
+ resolveClaudeCodeBinary: () => resolveClaudeCodeBinary,
152
+ revokeChatGPTSignIn: () => revokeChatGPTSignIn,
133
153
  setProgressCallback: () => setProgressCallback,
134
154
  setProviderCatalog: () => setProviderCatalog,
155
+ signInWithClaudeCode: () => signInWithClaudeCode,
156
+ signOutOfClaudeCode: () => signOutOfClaudeCode,
157
+ storageKeyProvider: () => storageKeyProvider,
135
158
  toModelInfo: () => toModelInfo,
136
159
  transcribeVoice: () => transcribeVoice,
137
160
  usesOpenAICodexTransport: () => usesOpenAICodexTransport,
161
+ verifyIdToken: () => verifyIdToken,
138
162
  withFileLock: () => withFileLock,
139
163
  withoutPaidTier: () => withoutPaidTier
140
164
  });
141
165
  module.exports = __toCommonJS(index_exports);
142
166
 
143
- // src/oauth/kimi.ts
144
- var import_node_child_process = require("child_process");
145
- var import_node_crypto = require("crypto");
146
- var import_node_fs = require("fs");
147
- var import_node_os2 = require("os");
148
- var import_node_path2 = __toESM(require("path"), 1);
167
+ // src/kimi-region.ts
168
+ var import_gg_ai = require("@kenkaiiii/gg-ai");
169
+ var KIMI_CODE_DEFAULT_BASE_URL = "https://api.kimi.com/coding/v1";
170
+ var KIMI_CODE_OVERSEAS_BASE_URL = "https://api.kimi.ai/coding/v1";
171
+ var KIMI_CODE_BASE_URLS = [KIMI_CODE_DEFAULT_BASE_URL, KIMI_CODE_OVERSEAS_BASE_URL];
172
+ async function probeHost(baseUrl, apiKey, fetchImpl, timeoutMs) {
173
+ try {
174
+ const res = await fetchImpl(`${baseUrl}/models`, {
175
+ headers: { Authorization: `Bearer ${apiKey}`, "User-Agent": import_gg_ai.GG_CODER_USER_AGENT },
176
+ signal: AbortSignal.timeout(timeoutMs)
177
+ });
178
+ if (res.ok) return "authorized";
179
+ if (res.status === 401 || res.status === 403) return "unauthorized";
180
+ return "inconclusive";
181
+ } catch {
182
+ return "inconclusive";
183
+ }
184
+ }
185
+ async function detectKimiCodeBaseUrl(apiKey, options = {}) {
186
+ const fetchImpl = options.fetch ?? fetch;
187
+ const timeoutMs = options.timeoutMs ?? 5e3;
188
+ const results = await Promise.all(
189
+ KIMI_CODE_BASE_URLS.map((url) => probeHost(url, apiKey, fetchImpl, timeoutMs))
190
+ );
191
+ const index = results.indexOf("authorized");
192
+ return index === -1 ? void 0 : KIMI_CODE_BASE_URLS[index];
193
+ }
194
+ function isKimiCodingEndpoint(baseUrl) {
195
+ if (typeof baseUrl !== "string" || baseUrl.length === 0) return false;
196
+ return /^https:\/\/api\.kimi\.(com|ai)\/coding(\/|$)/i.test(baseUrl);
197
+ }
198
+
199
+ // src/auth-storage.ts
200
+ var import_promises6 = __toESM(require("fs/promises"), 1);
201
+ var import_node_fs4 = require("fs");
202
+ var import_node_crypto4 = __toESM(require("crypto"), 1);
149
203
 
150
204
  // src/paths.ts
151
205
  var import_node_path = __toESM(require("path"), 1);
@@ -170,237 +224,519 @@ function getAppPaths() {
170
224
  agentsDir: import_node_path.default.join(agentDir, "agents"),
171
225
  progressFile: import_node_path.default.join(agentDir, "progress.json"),
172
226
  progressBackupFile: import_node_path.default.join(agentDir, "progress.backup.json"),
173
- trophiesFile: import_node_path.default.join(agentDir, "trophies.json")
227
+ trophiesFile: import_node_path.default.join(agentDir, "trophies.json"),
228
+ claudeCodeDir: import_node_path.default.join(agentDir, "claude-code"),
229
+ claudeCodeConfigDir: import_node_path.default.join(agentDir, "claude-code", "config")
174
230
  };
175
231
  }
176
232
 
177
- // src/oauth/request-timeout.ts
178
- var OAUTH_REQUEST_TIMEOUT_MS = 3e4;
179
- function oauthRequestSignal() {
180
- return AbortSignal.timeout(OAUTH_REQUEST_TIMEOUT_MS);
181
- }
233
+ // src/claude-code/runtime.ts
234
+ var import_node_child_process = require("child_process");
235
+ var import_node_crypto = __toESM(require("crypto"), 1);
236
+ var import_node_fs2 = __toESM(require("fs"), 1);
237
+ var import_promises2 = __toESM(require("fs/promises"), 1);
238
+ var import_node_path2 = __toESM(require("path"), 1);
182
239
 
183
- // src/oauth/kimi.ts
184
- var CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098";
185
- var DEFAULT_OAUTH_HOST = "https://auth.kimi.com";
186
- var DEFAULT_CODING_BASE_URL = "https://api.kimi.com/coding/v1";
187
- var KIMI_PLATFORM = "kimi_code_cli";
188
- var DEFAULT_KIMI_VERSION = "1.0.11";
189
- var DEVICE_TIMEOUT_MS = 15 * 60 * 1e3;
190
- function oauthHost() {
191
- const host = process.env.KIMI_CODE_OAUTH_HOST ?? process.env.KIMI_OAUTH_HOST ?? DEFAULT_OAUTH_HOST;
192
- return host.replace(/\/+$/, "");
193
- }
194
- function kimiCodeBaseUrl() {
195
- return (process.env.KIMI_CODE_BASE_URL ?? DEFAULT_CODING_BASE_URL).replace(/\/+$/, "");
196
- }
197
- function kimiVersion() {
198
- const v = process.env.KIMI_CODE_VERSION ?? DEFAULT_KIMI_VERSION;
199
- return asciiHeader(v, DEFAULT_KIMI_VERSION);
200
- }
201
- function asciiHeader(value, fallback = "unknown") {
202
- const cleaned = value.replace(/[^\u0020-\u007E]/g, "").trim();
203
- return cleaned.length > 0 ? cleaned : fallback;
204
- }
205
- function macOsProductVersion() {
206
- try {
207
- const version = (0, import_node_child_process.execFileSync)("/usr/bin/sw_vers", ["-productVersion"], {
208
- encoding: "utf-8",
209
- timeout: 1e3
210
- }).trim();
211
- return version.length > 0 ? version : void 0;
212
- } catch {
213
- return void 0;
240
+ // src/claude-code/tar.ts
241
+ var import_node_fs = __toESM(require("fs"), 1);
242
+ var import_promises = __toESM(require("fs/promises"), 1);
243
+ var import_node_zlib = __toESM(require("zlib"), 1);
244
+ var BLOCK = 512;
245
+ function readString(buf, offset, length) {
246
+ const end = buf.indexOf(0, offset);
247
+ return buf.toString("utf8", offset, end === -1 || end > offset + length ? offset + length : end);
248
+ }
249
+ function readSize(buf, offset) {
250
+ if (buf[offset] & 128) {
251
+ let value = buf[offset] & 127;
252
+ for (let i = 1; i < 12; i++) value = value * 256 + buf[offset + i];
253
+ return value;
254
+ }
255
+ const text = readString(buf, offset, 12).trim();
256
+ return text ? parseInt(text, 8) : 0;
257
+ }
258
+ function paxPath(data) {
259
+ let pos = 0;
260
+ while (pos < data.length) {
261
+ const space = data.indexOf(32, pos);
262
+ if (space === -1) break;
263
+ const len = parseInt(data.toString("utf8", pos, space), 10);
264
+ if (!Number.isFinite(len) || len <= 0) break;
265
+ const record = data.toString("utf8", space + 1, pos + len - 1);
266
+ const eq = record.indexOf("=");
267
+ if (eq !== -1 && record.slice(0, eq) === "path") return record.slice(eq + 1);
268
+ pos += len;
214
269
  }
270
+ return void 0;
215
271
  }
216
- function deviceModel() {
217
- const os2 = (0, import_node_os2.type)();
218
- const version = (0, import_node_os2.release)();
219
- const osArch = (0, import_node_os2.arch)();
220
- if (os2 === "Darwin") return `macOS ${macOsProductVersion() ?? version} ${osArch}`;
221
- if (os2 === "Windows_NT") return `Windows ${version} ${osArch}`;
222
- return `${os2} ${version} ${osArch}`.trim();
272
+ async function extractFileFromTarGz(tgzPath, entryName, destPath) {
273
+ const input = import_node_fs.default.createReadStream(tgzPath).pipe(import_node_zlib.default.createGunzip());
274
+ let pending = Buffer.alloc(0);
275
+ let remaining = 0;
276
+ let padding = 0;
277
+ let kind = "skip";
278
+ let meta = [];
279
+ let overridePath;
280
+ let out;
281
+ let found = false;
282
+ try {
283
+ for await (const chunk of input) {
284
+ pending = pending.length ? Buffer.concat([pending, chunk]) : chunk;
285
+ let pos = 0;
286
+ while (pos < pending.length) {
287
+ if (remaining > 0) {
288
+ const take = Math.min(remaining, pending.length - pos);
289
+ const slice = pending.subarray(pos, pos + take);
290
+ if (kind === "target") await out.write(slice);
291
+ else if (kind === "pax" || kind === "longname") meta.push(Buffer.from(slice));
292
+ pos += take;
293
+ remaining -= take;
294
+ if (remaining === 0) {
295
+ if (kind === "target") {
296
+ await out.close();
297
+ out = void 0;
298
+ found = true;
299
+ } else if (kind === "pax") {
300
+ overridePath = paxPath(Buffer.concat(meta)) ?? overridePath;
301
+ } else if (kind === "longname") {
302
+ overridePath = readString(Buffer.concat(meta), 0, Number.MAX_SAFE_INTEGER);
303
+ }
304
+ }
305
+ continue;
306
+ }
307
+ if (padding > 0) {
308
+ const skip = Math.min(padding, pending.length - pos);
309
+ pos += skip;
310
+ padding -= skip;
311
+ continue;
312
+ }
313
+ if (found) break;
314
+ if (pending.length - pos < BLOCK) break;
315
+ const header = pending.subarray(pos, pos + BLOCK);
316
+ pos += BLOCK;
317
+ if (header.every((b) => b === 0)) continue;
318
+ const size = readSize(header, 124);
319
+ const type = String.fromCharCode(header[156] || 48);
320
+ let name = readString(header, 0, 100);
321
+ if (readString(header, 257, 6) === "ustar") {
322
+ const prefix = readString(header, 345, 155);
323
+ if (prefix) name = `${prefix}/${name}`;
324
+ }
325
+ remaining = size;
326
+ padding = (BLOCK - size % BLOCK) % BLOCK;
327
+ meta = [];
328
+ if (type === "x") kind = "pax";
329
+ else if (type === "L") kind = "longname";
330
+ else {
331
+ const path7 = overridePath ?? name;
332
+ overridePath = void 0;
333
+ if ((type === "0" || type === "\0" || type === "7") && path7 === entryName) {
334
+ kind = "target";
335
+ out = await import_promises.default.open(destPath, "w", 493);
336
+ if (size === 0) {
337
+ await out.close();
338
+ out = void 0;
339
+ found = true;
340
+ }
341
+ } else {
342
+ kind = "skip";
343
+ }
344
+ }
345
+ }
346
+ pending = pos < pending.length ? Buffer.from(pending.subarray(pos)) : Buffer.alloc(0);
347
+ if (found) break;
348
+ }
349
+ if (!found && out) throw new Error(`Archive ended inside ${entryName}`);
350
+ return found;
351
+ } finally {
352
+ if (out) await out.close().catch(() => {
353
+ });
354
+ input.destroy();
355
+ }
223
356
  }
224
- function deviceId() {
225
- const idPath = import_node_path2.default.join(getAppPaths().agentDir, "kimi_device_id");
226
- if ((0, import_node_fs.existsSync)(idPath)) {
227
- try {
228
- const text = (0, import_node_fs.readFileSync)(idPath, "utf-8").trim();
229
- if (text.length > 0) return text;
230
- } catch {
357
+
358
+ // src/claude-code/manifest.json
359
+ var manifest_default = {
360
+ sdkVersion: "0.3.318",
361
+ claudeCodeVersion: "2.1.318",
362
+ platforms: {
363
+ "darwin-arm64": {
364
+ url: "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.318.tgz",
365
+ integrity: "sha512-AWoXeDXzAMD3RaA21lWuHvCrKd8xmbTTmKjovninn4j07jheoVSMzFbCFjpp4kk5TdhT36JtWPkAxvjNMbcO8w==",
366
+ bin: "claude"
367
+ },
368
+ "darwin-x64": {
369
+ url: "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.318.tgz",
370
+ integrity: "sha512-6CkZ5PCdebSY/LePp16sf/HYRtM6zVuxtn8qYICFS5KUZFsjccJJ9swVsOdHXNHi4SWZcSmIFTwoUbfFCzisRw==",
371
+ bin: "claude"
372
+ },
373
+ "linux-arm64": {
374
+ url: "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.318.tgz",
375
+ integrity: "sha512-mBm4su17aMitLBBCQj3bHdejyUDY6z/ormGih+HG24N689RSbdKECjGjkT87tDBIR/4jyoqmuSeHqM1DI8TJlg==",
376
+ bin: "claude"
377
+ },
378
+ "linux-arm64-musl": {
379
+ url: "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.318.tgz",
380
+ integrity: "sha512-l4LSYFOrvlYCrUImGfxMTQ0wg+T3s9CBhrBvPF/0oJGKZg+QnxHHU8jUVm9e9/xYmDRTtqFIrr4id/5AfH4XGw==",
381
+ bin: "claude"
382
+ },
383
+ "linux-x64": {
384
+ url: "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.318.tgz",
385
+ integrity: "sha512-+mE+ueBD3oRFdaG16rd0h+LMjdeivGxL0pr/+FZK7JeLWVu+yPSw/f/X02b9oK36/DWykiFTu2G6HnJ9/+YO6w==",
386
+ bin: "claude"
387
+ },
388
+ "linux-x64-musl": {
389
+ url: "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.318.tgz",
390
+ integrity: "sha512-SAF3lj/2OzTU84p+OOA13QEKA9+5fYAw0M9E7AJjdnRmVFGqQfbV7Do1i1f9Sj0mIubB3rLTRphGRw8O4HZjgA==",
391
+ bin: "claude"
392
+ },
393
+ "win32-arm64": {
394
+ url: "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.318.tgz",
395
+ integrity: "sha512-19iVSZ4379QbZU1XnGUtfFvhKS1WzS8H1sbU/DbrzqbJ5Kq1eB1drj+obB1fCxTh6m26tRPj0zMJmdeu/jk3Gw==",
396
+ bin: "claude.exe"
397
+ },
398
+ "win32-x64": {
399
+ url: "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.318.tgz",
400
+ integrity: "sha512-ZdKVWpyVNVt0cXrOf8sUBaxcxLEAP+oGMdQDLBWr/BqOxTWLy7TBWOj1kzw+wVg/9WIkjkgQWeJzGXPWulR1sA==",
401
+ bin: "claude.exe"
231
402
  }
232
403
  }
233
- const id = (0, import_node_crypto.randomUUID)();
234
- try {
235
- (0, import_node_fs.mkdirSync)(getAppPaths().agentDir, { recursive: true, mode: 448 });
236
- (0, import_node_fs.writeFileSync)(idPath, id, { encoding: "utf-8", mode: 384 });
237
- } catch {
404
+ };
405
+
406
+ // src/claude-code/runtime.ts
407
+ var CLAUDE_CODE_MANIFEST = manifest_default;
408
+ var CLAUDE_CODE_AUTH_KEY = "anthropic-claude-code";
409
+ var CLAUDE_CODE_AUTH_MARKER = { via: "claude-code" };
410
+ function isClaudeCodeAuthMarker(value) {
411
+ return value?.via === CLAUDE_CODE_AUTH_MARKER.via;
412
+ }
413
+ function isLegacyClaudeOAuth(value) {
414
+ const token = value?.accessToken;
415
+ return typeof token === "string" && token.startsWith("sk-ant-oat");
416
+ }
417
+ function isMusl() {
418
+ if (process.platform !== "linux") return false;
419
+ const report = typeof process.report?.getReport === "function" ? process.report.getReport() : null;
420
+ return report != null && report.header?.glibcVersionRuntime === void 0;
421
+ }
422
+ function claudeCodePlatformKey(info = { platform: process.platform, arch: process.arch, musl: isMusl() }) {
423
+ const arch = info.arch === "arm64" || info.arch === "x64" ? info.arch : null;
424
+ if (!arch) return null;
425
+ if (info.platform === "darwin" || info.platform === "win32") return `${info.platform}-${arch}`;
426
+ if (info.platform === "linux") return `linux-${arch}${info.musl ? "-musl" : ""}`;
427
+ return null;
428
+ }
429
+ function resolveAsset(opts) {
430
+ const manifest = opts.manifest ?? CLAUDE_CODE_MANIFEST;
431
+ const key = opts.platformKey ?? claudeCodePlatformKey();
432
+ const asset = key ? manifest.platforms[key] : void 0;
433
+ if (!asset) {
434
+ throw new Error(
435
+ `Claude Code isn't available for ${process.platform}-${process.arch}. Use an Anthropic API key instead.`
436
+ );
238
437
  }
239
- return id;
438
+ return { manifest, asset };
240
439
  }
241
- function deviceHeaders() {
242
- return {
243
- "X-Msh-Platform": KIMI_PLATFORM,
244
- "X-Msh-Version": kimiVersion(),
245
- "X-Msh-Device-Name": asciiHeader((0, import_node_os2.hostname)()),
246
- "X-Msh-Device-Model": asciiHeader(deviceModel()),
247
- "X-Msh-Os-Version": asciiHeader((0, import_node_os2.release)()),
248
- "X-Msh-Device-Id": deviceId()
249
- };
440
+ function claudeCodeHome(opts = {}) {
441
+ return opts.home ?? getAppPaths().claudeCodeDir;
250
442
  }
251
- function kimiCodingHeaders() {
252
- return {
253
- "User-Agent": `kimi-code-cli/${kimiVersion()}`,
254
- ...deviceHeaders()
255
- };
443
+ function claudeCodeConfigDir(opts = {}) {
444
+ return opts.home ? import_node_path2.default.join(opts.home, "config") : getAppPaths().claudeCodeConfigDir;
256
445
  }
257
- function isKimiCodingEndpoint(baseUrl) {
258
- if (typeof baseUrl !== "string" || baseUrl.length === 0) return false;
259
- const normalized = baseUrl.replace(/\/+$/, "");
260
- return normalized === kimiCodeBaseUrl() || /(^|\.)kimi\.com/i.test(normalized);
446
+ function claudeCodeBinaryPath(opts = {}) {
447
+ const { manifest, asset } = resolveAsset(opts);
448
+ return import_node_path2.default.join(claudeCodeHome(opts), manifest.claudeCodeVersion, asset.bin);
261
449
  }
262
- async function postForm(endpoint, params) {
263
- const response = await fetch(`${oauthHost()}${endpoint}`, {
264
- method: "POST",
265
- headers: {
266
- ...deviceHeaders(),
267
- "Content-Type": "application/x-www-form-urlencoded",
268
- Accept: "application/json"
269
- },
270
- body: new URLSearchParams(params).toString(),
271
- signal: oauthRequestSignal()
272
- });
273
- let data = {};
450
+ function resolveClaudeCodeBinary(opts = {}) {
451
+ let binary;
274
452
  try {
275
- const parsed = await response.json();
276
- if (parsed && typeof parsed === "object") data = parsed;
453
+ binary = claudeCodeBinaryPath(opts);
277
454
  } catch {
455
+ return null;
456
+ }
457
+ try {
458
+ return import_node_fs2.default.statSync(binary).isFile() ? binary : null;
459
+ } catch {
460
+ return null;
278
461
  }
279
- return { status: response.status, data };
280
462
  }
281
- function errorDetail(data) {
282
- const desc = data.error_description ?? data.message ?? data.error;
283
- return typeof desc === "string" && desc.length > 0 ? desc : "unknown error";
463
+ var installs = /* @__PURE__ */ new Map();
464
+ async function ensureClaudeCodeBinary(opts = {}) {
465
+ const existing = resolveClaudeCodeBinary(opts);
466
+ if (existing) return existing;
467
+ const target = claudeCodeBinaryPath(opts);
468
+ let install = installs.get(target);
469
+ if (!install) {
470
+ install = installClaudeCode(opts).finally(() => installs.delete(target));
471
+ installs.set(target, install);
472
+ }
473
+ return install;
474
+ }
475
+ async function installClaudeCode(opts) {
476
+ const { manifest, asset } = resolveAsset(opts);
477
+ const home = claudeCodeHome(opts);
478
+ const versionDir = import_node_path2.default.join(home, manifest.claudeCodeVersion);
479
+ const binary = import_node_path2.default.join(versionDir, asset.bin);
480
+ await import_promises2.default.mkdir(home, { recursive: true });
481
+ const tag = `${process.pid}-${import_node_crypto.default.randomBytes(4).toString("hex")}`;
482
+ const tarball = import_node_path2.default.join(home, `.download-${tag}.tgz`);
483
+ const staging = import_node_path2.default.join(home, `.staging-${tag}`);
484
+ try {
485
+ await download(asset, tarball, opts);
486
+ opts.onProgress?.({ phase: "extract" });
487
+ await import_promises2.default.mkdir(staging, { recursive: true });
488
+ const found = await extractFileFromTarGz(
489
+ tarball,
490
+ `package/${asset.bin}`,
491
+ import_node_path2.default.join(staging, asset.bin)
492
+ );
493
+ if (!found) throw new Error(`The Claude Code download has no ${asset.bin} binary.`);
494
+ await import_promises2.default.chmod(import_node_path2.default.join(staging, asset.bin), 493);
495
+ try {
496
+ await import_promises2.default.rename(staging, versionDir);
497
+ } catch (err) {
498
+ if (!import_node_fs2.default.existsSync(binary)) throw err;
499
+ }
500
+ await removeOldVersions(home, manifest.claudeCodeVersion);
501
+ return binary;
502
+ } finally {
503
+ await import_promises2.default.rm(tarball, { force: true }).catch(() => {
504
+ });
505
+ await import_promises2.default.rm(staging, { recursive: true, force: true }).catch(() => {
506
+ });
507
+ }
284
508
  }
285
- function credsFromTokenResponse(data, opts) {
286
- const accessToken = data.access_token;
287
- const responseRefreshToken = data.refresh_token;
288
- const expiresIn = Number(data.expires_in);
289
- if (typeof accessToken !== "string" || accessToken.length === 0) {
290
- throw new Error("Kimi OAuth response missing access_token.");
509
+ async function download(asset, dest, opts) {
510
+ const doFetch = opts.fetch ?? fetch;
511
+ const res = await doFetch(asset.url, { signal: opts.signal });
512
+ if (!res.ok || !res.body) {
513
+ throw new Error(`Claude Code download failed (HTTP ${res.status}).`);
291
514
  }
292
- const refreshToken = typeof responseRefreshToken === "string" && responseRefreshToken.length > 0 ? responseRefreshToken : opts?.fallbackRefreshToken ?? "";
293
- if (refreshToken.length === 0) {
294
- throw new Error("Kimi OAuth response missing refresh_token.");
515
+ const totalBytes = Number(res.headers.get("content-length")) || void 0;
516
+ const hash = import_node_crypto.default.createHash("sha512");
517
+ const out = await import_promises2.default.open(dest, "w");
518
+ let receivedBytes = 0;
519
+ let lastReported = -1;
520
+ try {
521
+ opts.onProgress?.({ phase: "download", receivedBytes, totalBytes });
522
+ for await (const chunk of res.body) {
523
+ if (opts.signal?.aborted) throw opts.signal.reason ?? new Error("Download cancelled.");
524
+ hash.update(chunk);
525
+ await out.write(chunk);
526
+ receivedBytes += chunk.byteLength;
527
+ const step = totalBytes ? Math.floor(receivedBytes / totalBytes * 100) : Math.floor(receivedBytes / 4194304);
528
+ if (step !== lastReported) {
529
+ lastReported = step;
530
+ opts.onProgress?.({ phase: "download", receivedBytes, totalBytes });
531
+ }
532
+ }
533
+ } finally {
534
+ await out.close();
295
535
  }
296
- if (!Number.isFinite(expiresIn) || expiresIn <= 0) {
297
- throw new Error("Kimi OAuth response missing or invalid expires_in.");
536
+ opts.onProgress?.({ phase: "verify" });
537
+ const expected = asset.integrity.replace(/^sha512-/, "");
538
+ if (hash.digest("base64") !== expected) {
539
+ throw new Error(
540
+ "The Claude Code download didn't match its pinned checksum, so it was discarded."
541
+ );
298
542
  }
299
- return {
300
- accessToken,
301
- refreshToken,
302
- expiresAt: Date.now() + expiresIn * 1e3,
303
- expiresIn,
304
- baseUrl: kimiCodeBaseUrl()
305
- };
306
543
  }
307
- async function requestDeviceAuthorization() {
308
- const { status, data } = await postForm("/api/oauth/device_authorization", {
309
- client_id: CLIENT_ID
310
- });
311
- if (status !== 200) {
312
- throw new Error(`Kimi device authorization failed (${status}): ${errorDetail(data)}`);
544
+ async function removeOldVersions(home, keep) {
545
+ let entries;
546
+ try {
547
+ entries = await import_promises2.default.readdir(home, { withFileTypes: true });
548
+ } catch {
549
+ return;
313
550
  }
314
- const userCode = data.user_code;
315
- const deviceCode = data.device_code;
316
- const verificationUriComplete = data.verification_uri_complete;
317
- if (typeof userCode !== "string" || typeof deviceCode !== "string") {
318
- throw new Error("Kimi device authorization response missing user_code/device_code.");
551
+ for (const entry of entries) {
552
+ if (!entry.isDirectory() || entry.name === keep || entry.name === "config") continue;
553
+ if (!/^\d+\.\d+\.\d+/.test(entry.name)) continue;
554
+ await import_promises2.default.rm(import_node_path2.default.join(home, entry.name), { recursive: true, force: true }).catch(() => {
555
+ });
319
556
  }
320
- return {
321
- userCode,
322
- deviceCode,
323
- verificationUri: typeof data.verification_uri === "string" ? data.verification_uri : "",
324
- verificationUriComplete: typeof verificationUriComplete === "string" ? verificationUriComplete : "",
325
- interval: Number(data.interval ?? 5) || 5
326
- };
327
557
  }
328
- async function pollDeviceToken(deviceCode) {
329
- const { status, data } = await postForm("/api/oauth/token", {
330
- client_id: CLIENT_ID,
331
- device_code: deviceCode,
332
- grant_type: "urn:ietf:params:oauth:grant-type:device_code"
558
+ function formatClaudeCodeInstallProgress(progress) {
559
+ if (progress.phase === "verify") return "Checking the Claude Code download\u2026";
560
+ if (progress.phase === "extract") return "Unpacking Claude Code\u2026";
561
+ const mb = (bytes) => Math.round(bytes / 1048576);
562
+ if (!progress.totalBytes) return `Downloading Claude Code\u2026 ${mb(progress.receivedBytes)} MB`;
563
+ const pct = Math.floor(progress.receivedBytes / progress.totalBytes * 100);
564
+ return `Downloading Claude Code\u2026 ${pct}% (${mb(progress.receivedBytes)} of ${mb(progress.totalBytes)} MB)`;
565
+ }
566
+ var STRIPPED_ENV = [
567
+ "ANTHROPIC_API_KEY",
568
+ "ANTHROPIC_AUTH_TOKEN",
569
+ "ANTHROPIC_BASE_URL",
570
+ "CLAUDE_CODE_OAUTH_TOKEN",
571
+ "CLAUDE_CODE_OAUTH_REFRESH_TOKEN",
572
+ "CLAUDE_CODE_USE_BEDROCK",
573
+ "CLAUDE_CODE_USE_VERTEX",
574
+ "CLAUDE_CODE_USE_FOUNDRY",
575
+ "NODE_OPTIONS"
576
+ ];
577
+ function claudeCodeEnv(opts = {}, base = process.env) {
578
+ const env = {};
579
+ for (const [key, value] of Object.entries(base)) {
580
+ if (value !== void 0 && !STRIPPED_ENV.includes(key)) env[key] = value;
581
+ }
582
+ env.CLAUDE_CONFIG_DIR = claudeCodeConfigDir(opts);
583
+ env.DISABLE_AUTOUPDATER = "1";
584
+ return env;
585
+ }
586
+ function runCommand(opts) {
587
+ if (opts.binary) return { command: opts.binary, prefix: opts.binaryArgs ?? [] };
588
+ const binary = resolveClaudeCodeBinary(opts);
589
+ if (!binary) throw new Error("Claude Code isn't installed yet. Sign in with Claude first.");
590
+ return { command: binary, prefix: [] };
591
+ }
592
+ function runClaude(args, opts) {
593
+ const { command, prefix } = runCommand(opts);
594
+ return new Promise((resolve, reject) => {
595
+ const child = (0, import_node_child_process.spawn)(command, [...prefix, ...args], {
596
+ env: claudeCodeEnv(opts),
597
+ stdio: ["ignore", "pipe", "pipe"],
598
+ windowsHide: true,
599
+ signal: opts.signal
600
+ });
601
+ let stdout = "";
602
+ let stderr = "";
603
+ child.stdout.on("data", (d) => stdout += d.toString());
604
+ child.stderr.on("data", (d) => stderr += d.toString());
605
+ child.on("error", reject);
606
+ child.on("close", (code) => resolve({ code, stdout, stderr }));
333
607
  });
334
- if (status === 200 && typeof data.access_token === "string") {
335
- return { kind: "success", creds: credsFromTokenResponse(data) };
336
- }
337
- if (status >= 500) {
338
- throw new Error(`Kimi token polling server error (${status}): ${errorDetail(data)}`);
339
- }
340
- const errorCode = typeof data.error === "string" ? data.error : "unknown_error";
341
- switch (errorCode) {
342
- case "authorization_pending":
343
- return { kind: "pending" };
344
- case "slow_down":
345
- return { kind: "slow_down" };
346
- case "expired_token":
347
- return { kind: "expired" };
348
- case "access_denied":
349
- return { kind: "denied" };
350
- default:
351
- throw new Error(`Kimi token polling failed (${status}): ${errorDetail(data)}`);
608
+ }
609
+ async function claudeCodeAuthStatus(opts = {}) {
610
+ if (!opts.binary && !resolveClaudeCodeBinary(opts)) return { loggedIn: false };
611
+ const { stdout } = await runClaude(["auth", "status", "--json"], opts);
612
+ try {
613
+ const parsed = JSON.parse(stdout);
614
+ return {
615
+ loggedIn: parsed.loggedIn === true,
616
+ ...typeof parsed.authMethod === "string" ? { authMethod: parsed.authMethod } : {},
617
+ ...typeof parsed.email === "string" ? { email: parsed.email } : {},
618
+ ...typeof parsed.subscriptionType === "string" ? { subscriptionType: parsed.subscriptionType } : {}
619
+ };
620
+ } catch {
621
+ return { loggedIn: false };
352
622
  }
353
623
  }
354
- function sleep(ms) {
355
- return new Promise((resolve) => {
356
- setTimeout(resolve, ms);
357
- });
624
+ async function logoutClaudeCode(opts = {}) {
625
+ if (!opts.binary && !resolveClaudeCodeBinary(opts)) return;
626
+ const { code, stderr } = await runClaude(["auth", "logout"], opts);
627
+ if (code !== 0) throw new Error(stderr.trim() || `claude auth logout exited with ${code}`);
358
628
  }
359
- async function loginKimi(callbacks) {
360
- const auth = await requestDeviceAuthorization();
361
- callbacks.onStatus(
362
- `Visit ${auth.verificationUri || auth.verificationUriComplete} and enter code: ${auth.userCode}`
363
- );
364
- callbacks.onOpenUrl(auth.verificationUriComplete || auth.verificationUri);
365
- callbacks.onStatus("Waiting for you to authorize in the browser...");
366
- const deadline = Date.now() + DEVICE_TIMEOUT_MS;
367
- let interval = Math.max(auth.interval, 1);
368
- while (Date.now() < deadline) {
369
- await sleep(interval * 1e3);
370
- const result = await pollDeviceToken(auth.deviceCode);
371
- if (result.kind === "success") return result.creds;
372
- if (result.kind === "denied") {
373
- throw new Error("Kimi authorization was denied.");
374
- }
375
- if (result.kind === "expired") {
376
- throw new Error("Kimi device code expired. Please run login again.");
377
- }
378
- if (result.kind === "slow_down") {
379
- interval += 5;
380
- }
629
+ var OPEN_URL_HOOK = `const arg = String(process.argv[1] || "");
630
+ const file = process.env.GG_CLAUDE_CODE_URL_FILE;
631
+ if (file && /https?:/.test(arg)) {
632
+ require("node:fs").appendFileSync(file, arg + "\\n");
633
+ process.exit(0);
634
+ }
635
+ `;
636
+ var CAPTURE_WAIT_MS = 3e3;
637
+ function stripAnsi(text) {
638
+ return text.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b\[[0-9;?]*[A-Za-z]/g, "");
639
+ }
640
+ function claudeCodeCallbackUrl(manualUrl, captured) {
641
+ const port = /localhost(?::|%3A)(\d{2,5})(?:\/|%2F)callback/i.exec(captured)?.[1];
642
+ if (!port) return null;
643
+ try {
644
+ const url = new URL(manualUrl);
645
+ url.searchParams.set("redirect_uri", `http://localhost:${port}/callback`);
646
+ return url.toString();
647
+ } catch {
648
+ return null;
381
649
  }
382
- throw new Error("Kimi login timed out. Please run login again.");
383
650
  }
384
- async function refreshKimiToken(refreshToken) {
385
- const { status, data } = await postForm("/api/oauth/token", {
386
- client_id: CLIENT_ID,
387
- grant_type: "refresh_token",
388
- refresh_token: refreshToken
389
- });
390
- if (status === 200 && typeof data.access_token === "string") {
391
- return credsFromTokenResponse(data, { fallbackRefreshToken: refreshToken });
651
+ async function loginClaudeCode(callbacks, opts = {}) {
652
+ const { command, prefix } = runCommand(opts);
653
+ const home = claudeCodeHome(opts);
654
+ await import_promises2.default.mkdir(claudeCodeConfigDir(opts), { recursive: true });
655
+ const tag = `${process.pid}-${import_node_crypto.default.randomBytes(4).toString("hex")}`;
656
+ const hookFile = import_node_path2.default.join(home, `.open-url-${tag}.cjs`);
657
+ const urlFile = import_node_path2.default.join(home, `.open-url-${tag}.txt`);
658
+ await import_promises2.default.writeFile(hookFile, OPEN_URL_HOOK);
659
+ const env = {
660
+ ...claudeCodeEnv(opts),
661
+ BROWSER: process.execPath,
662
+ NODE_OPTIONS: `--require ${JSON.stringify(hookFile)}`,
663
+ GG_CLAUDE_CODE_URL_FILE: urlFile
664
+ };
665
+ const promptAbort = new AbortController();
666
+ let timer;
667
+ try {
668
+ await new Promise((resolve, reject) => {
669
+ const child = (0, import_node_child_process.spawn)(command, [...prefix, "auth", "login", "--claudeai"], {
670
+ env,
671
+ stdio: ["pipe", "pipe", "pipe"],
672
+ windowsHide: true
673
+ });
674
+ let stdout = "";
675
+ let stderr = "";
676
+ let manualUrl = null;
677
+ let opened = false;
678
+ const onAbort = () => child.kill();
679
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
680
+ const open = (url, needsCode) => {
681
+ if (opened) return;
682
+ opened = true;
683
+ clearInterval(timer);
684
+ callbacks.onOpenUrl(url);
685
+ callbacks.onStatus("Finish signing in to Claude in your browser\u2026");
686
+ if (!needsCode) return;
687
+ void callbacks.onPromptCode("Paste the code shown after signing in to Claude:", promptAbort.signal).then((code) => {
688
+ const trimmed = code.trim();
689
+ if (trimmed && !child.stdin.destroyed) child.stdin.write(`${trimmed}
690
+ `);
691
+ }).catch(() => {
692
+ });
693
+ };
694
+ child.stdout.on("data", (d) => {
695
+ stdout += d.toString();
696
+ if (manualUrl) return;
697
+ const match = /visit:\s*(https:\/\/\S+)/.exec(stripAnsi(stdout));
698
+ if (!match) return;
699
+ manualUrl = match[1];
700
+ const started = Date.now();
701
+ timer = setInterval(() => {
702
+ let captured = "";
703
+ try {
704
+ captured = import_node_fs2.default.readFileSync(urlFile, "utf8");
705
+ } catch {
706
+ }
707
+ const callbackUrl = captured ? claudeCodeCallbackUrl(manualUrl, captured) : null;
708
+ if (callbackUrl) open(callbackUrl, false);
709
+ else if (Date.now() - started > (opts.browserWaitMs ?? CAPTURE_WAIT_MS))
710
+ open(manualUrl, true);
711
+ }, 100);
712
+ });
713
+ child.stderr.on("data", (d) => stderr += d.toString());
714
+ child.on("error", reject);
715
+ child.on("close", (code) => {
716
+ opts.signal?.removeEventListener("abort", onAbort);
717
+ if (opts.signal?.aborted) reject(new Error("Sign-in cancelled."));
718
+ else if (code === 0) resolve();
719
+ else {
720
+ const detail = stripAnsi(stderr).trim().split("\n").pop()?.trim();
721
+ reject(new Error(detail || `Claude sign-in failed (exit ${code}).`));
722
+ }
723
+ });
724
+ });
725
+ } finally {
726
+ clearInterval(timer);
727
+ promptAbort.abort();
728
+ await import_promises2.default.rm(hookFile, { force: true }).catch(() => {
729
+ });
730
+ await import_promises2.default.rm(urlFile, { force: true }).catch(() => {
731
+ });
392
732
  }
393
- const errorCode = typeof data.error === "string" ? data.error : "";
394
- throw new Error(`Kimi token refresh failed (${status}): ${errorCode || errorDetail(data)}`);
395
733
  }
396
734
 
397
- // src/auth-storage.ts
398
- var import_promises3 = __toESM(require("fs/promises"), 1);
399
- var import_node_fs3 = require("fs");
400
- var import_node_crypto6 = __toESM(require("crypto"), 1);
401
-
402
- // src/oauth/anthropic.ts
735
+ // src/oauth/openai.ts
736
+ var import_node_http = __toESM(require("http"), 1);
403
737
  var import_node_crypto2 = __toESM(require("crypto"), 1);
738
+ var import_promises3 = __toESM(require("fs/promises"), 1);
739
+ var import_node_path3 = __toESM(require("path"), 1);
404
740
 
405
741
  // src/oauth/pkce.ts
406
742
  function base64urlEncode(bytes) {
@@ -420,201 +756,233 @@ async function generatePKCE() {
420
756
  return { verifier, challenge };
421
757
  }
422
758
 
423
- // src/claude-code-version.ts
424
- var import_gg_ai = require("@kenkaiiii/gg-ai");
425
-
426
- // src/oauth/anthropic.ts
427
- var CLIENT_ID2 = atob("OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl");
428
- var AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
429
- var TOKEN_URLS = [
430
- "https://platform.claude.com/v1/oauth/token",
431
- "https://console.anthropic.com/v1/oauth/token"
432
- ];
433
- var REDIRECT_URI = "https://platform.claude.com/oauth/code/callback";
434
- var SCOPES = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
435
- async function postTokenRequest(body, label) {
436
- const encoded = JSON.stringify(body);
437
- const headers = {
438
- "Content-Type": "application/json",
439
- "User-Agent": await (0, import_gg_ai.getClaudeCliUserAgent)(),
440
- "anthropic-beta": "oauth-2025-04-20"
441
- };
442
- let lastError = null;
443
- for (const url of TOKEN_URLS) {
444
- let response;
445
- try {
446
- response = await fetch(url, {
447
- method: "POST",
448
- headers,
449
- body: encoded,
450
- signal: oauthRequestSignal()
451
- });
452
- } catch (err) {
453
- lastError = err instanceof Error ? err : new Error(String(err));
454
- continue;
455
- }
456
- if (response.ok) {
457
- return await response.json();
458
- }
459
- const text = await response.text();
460
- if (response.status >= 400 && response.status < 500) {
461
- throw new Error(`Anthropic ${label} failed (${response.status}): ${text}`);
462
- }
463
- lastError = new Error(`Anthropic ${label} failed (${response.status}): ${text}`);
464
- }
465
- throw lastError ?? new Error(`Anthropic ${label} failed: all endpoints unreachable`);
466
- }
467
- function toCredentials(data) {
468
- return {
469
- accessToken: data.access_token,
470
- refreshToken: data.refresh_token,
471
- expiresAt: Date.now() + data.expires_in * 1e3 - 5 * 60 * 1e3
472
- };
473
- }
474
- async function loginAnthropic(callbacks) {
475
- const { verifier, challenge } = await generatePKCE();
476
- const state = import_node_crypto2.default.randomBytes(16).toString("hex");
477
- const params = new URLSearchParams({
478
- code: "true",
479
- client_id: CLIENT_ID2,
480
- response_type: "code",
481
- redirect_uri: REDIRECT_URI,
482
- scope: SCOPES,
483
- code_challenge: challenge,
484
- code_challenge_method: "S256",
485
- state
486
- });
487
- const authUrl = `${AUTHORIZE_URL}?${params}`;
488
- callbacks.onOpenUrl(authUrl);
489
- const raw = await callbacks.onPromptCode("Paste the code from the browser (format: code#state):");
490
- const parts = raw.trim().split("#");
491
- if (parts.length !== 2 || !parts[0] || parts[1] !== state) {
492
- throw new Error("Invalid code or state mismatch. Please try again.");
493
- }
494
- return exchangeAnthropicCode(parts[0], parts[1], verifier);
495
- }
496
- async function exchangeAnthropicCode(code, state, verifier) {
497
- const data = await postTokenRequest(
498
- {
499
- grant_type: "authorization_code",
500
- client_id: CLIENT_ID2,
501
- code,
502
- state,
503
- redirect_uri: REDIRECT_URI,
504
- code_verifier: verifier
505
- },
506
- "token exchange"
507
- );
508
- return toCredentials(data);
509
- }
510
- async function refreshAnthropicToken(refreshToken) {
511
- const data = await postTokenRequest(
512
- {
513
- grant_type: "refresh_token",
514
- client_id: CLIENT_ID2,
515
- refresh_token: refreshToken
516
- },
517
- "token refresh"
518
- );
519
- return toCredentials(data);
759
+ // src/oauth/request-timeout.ts
760
+ var OAUTH_REQUEST_TIMEOUT_MS = 3e4;
761
+ function oauthRequestSignal() {
762
+ return AbortSignal.timeout(OAUTH_REQUEST_TIMEOUT_MS);
520
763
  }
521
764
 
522
765
  // src/oauth/openai.ts
523
- var import_node_http = __toESM(require("http"), 1);
524
- var import_node_crypto3 = __toESM(require("crypto"), 1);
525
- var CLIENT_ID3 = "app_EMoamEEZ73f0CkXaXp7hrann";
526
- var AUTHORIZE_URL2 = "https://auth.openai.com/oauth/authorize";
527
- var TOKEN_URL = "https://auth.openai.com/oauth/token";
528
- var REDIRECT_URI2 = "http://localhost:1455/auth/callback";
529
- var SCOPE = "openid profile email offline_access api.connectors.read api.connectors.invoke";
530
- var JWT_CLAIM_PATH = "https://api.openai.com/auth";
766
+ var ISSUER = "https://auth.openai.com";
767
+ var AUTHORIZE_URL = `${ISSUER}/api/accounts/authorize`;
768
+ var TOKEN_URL = `${ISSUER}/api/accounts/oauth/token`;
769
+ var REVOKE_URL = `${ISSUER}/api/accounts/oauth/revoke`;
770
+ var JWKS_URL = `${ISSUER}/.well-known/jwks.json`;
771
+ var REGISTRATION_CLIENT_ID = "dynamic_agent_client";
772
+ var AGENT_NAME = "GG Coder";
773
+ var RESOURCE = "https://api.openai.com/v1";
774
+ var CHATGPT_PLAN_SCOPE = "chatgpt.tokens.use.direct";
775
+ var SCOPES = `openid profile email offline_access resource.invoke ${CHATGPT_PLAN_SCOPE}`;
776
+ var CHATGPT_PLAN_BASE_URL = "https://api.openai.com/v1";
777
+ var CHATGPT_USAGE_URL = "https://chatgpt.com/settings/usage";
778
+ var CALLBACK_PATH = "/auth/callback";
779
+ var PREFERRED_PORT = 1455;
531
780
  var CALLBACK_TIMEOUT_MS = 12e4;
532
781
  var MAX_PASTE_ATTEMPTS = 3;
533
- var MIN_RAW_CODE_LENGTH = 10;
534
- async function loginOpenAI(callbacks) {
535
- const { verifier, challenge } = await generatePKCE();
536
- const state = import_node_crypto3.default.randomBytes(16).toString("hex");
537
- const url = new URL(AUTHORIZE_URL2);
538
- url.searchParams.set("response_type", "code");
539
- url.searchParams.set("client_id", CLIENT_ID3);
540
- url.searchParams.set("redirect_uri", REDIRECT_URI2);
541
- url.searchParams.set("scope", SCOPE);
542
- url.searchParams.set("code_challenge", challenge);
543
- url.searchParams.set("code_challenge_method", "S256");
544
- url.searchParams.set("state", state);
545
- url.searchParams.set("prompt", "login");
546
- url.searchParams.set("id_token_add_organizations", "true");
547
- url.searchParams.set("codex_cli_simplified_flow", "true");
548
- url.searchParams.set("originator", "ggcoder");
549
- const code = await acquireAuthorizationCode(url.toString(), state, callbacks);
550
- const creds = await exchangeOpenAICode(code, verifier);
551
- const accountId = getAccountId(creds.accessToken);
552
- if (!accountId) {
553
- throw new Error("Failed to extract accountId from OpenAI token.");
554
- }
555
- creds.accountId = accountId;
556
- return creds;
557
- }
558
- function parseAuthorizationInput(input) {
559
- const value = input.trim();
560
- if (!value) return {};
782
+ var ID_TOKEN_CLOCK_SKEW_S = 60;
783
+ var CHATGPT_PLAN_NOT_ENABLED_MESSAGE = "ChatGPT plan use isn't enabled for GG Coder. Allow Use your ChatGPT plan, or add an OpenAI API key.";
784
+ var ChatGPTPlanNotEnabledError = class extends Error {
785
+ constructor() {
786
+ super(CHATGPT_PLAN_NOT_ENABLED_MESSAGE);
787
+ this.name = "ChatGPTPlanNotEnabledError";
788
+ }
789
+ };
790
+ function defaultHostIdFile() {
791
+ return import_node_path3.default.join(getAppPaths().agentDir, "openai-host-id");
792
+ }
793
+ function defaultRegistrationFile() {
794
+ return import_node_path3.default.join(getAppPaths().agentDir, "openai-chatgpt-client.json");
795
+ }
796
+ async function writePrivateFile(file, content) {
797
+ await import_promises3.default.mkdir(import_node_path3.default.dirname(file), { recursive: true });
798
+ const tmp = `${file}.${process.pid}.${import_node_crypto2.default.randomUUID().slice(0, 8)}.tmp`;
561
799
  try {
562
- const url = new URL(value);
563
- return {
564
- code: url.searchParams.get("code") ?? void 0,
565
- state: url.searchParams.get("state") ?? void 0
566
- };
567
- } catch {
800
+ await import_promises3.default.writeFile(tmp, content, { encoding: "utf-8", mode: 384 });
801
+ await import_promises3.default.rename(tmp, file);
802
+ } catch (err) {
803
+ await import_promises3.default.unlink(tmp).catch(() => {
804
+ });
805
+ throw err;
568
806
  }
569
- if (value.includes("#")) {
570
- const [code, state] = value.split("#", 2);
571
- return { code, state };
807
+ }
808
+ var HOST_ID_PATTERN = /^urn:uuid:[0-9a-f-]{36}$/i;
809
+ async function getChatGPTHostId(file = defaultHostIdFile()) {
810
+ const existing = await import_promises3.default.readFile(file, "utf-8").catch(() => "");
811
+ if (HOST_ID_PATTERN.test(existing.trim())) return existing.trim();
812
+ await writePrivateFile(file, `urn:uuid:${import_node_crypto2.default.randomUUID()}`);
813
+ return (await import_promises3.default.readFile(file, "utf-8")).trim();
814
+ }
815
+ async function readRegistration(file) {
816
+ try {
817
+ const value = JSON.parse(await import_promises3.default.readFile(file, "utf-8"));
818
+ if (typeof value.clientId === "string" && typeof value.subject === "string") {
819
+ return { clientId: value.clientId, subject: value.subject };
820
+ }
821
+ } catch {
572
822
  }
573
- if (value.includes("code=")) {
574
- const params = new URLSearchParams(value);
823
+ return void 0;
824
+ }
825
+ async function loginChatGPT(callbacks, options = {}) {
826
+ const hostId = await getChatGPTHostId(options.hostIdFile);
827
+ const registrationFile = options.registrationFile ?? defaultRegistrationFile();
828
+ const registration = await readRegistration(registrationFile);
829
+ const { verifier, challenge } = await generatePKCE();
830
+ const state = import_node_crypto2.default.randomBytes(16).toString("hex");
831
+ const nonce = import_node_crypto2.default.randomBytes(16).toString("hex");
832
+ const listener = await startCallbackListener(state, Boolean(registration));
833
+ try {
834
+ const redirectUri = `http://127.0.0.1:${listener.port}${CALLBACK_PATH}`;
835
+ const url = new URL(AUTHORIZE_URL);
836
+ url.searchParams.set("client_id", registration?.clientId ?? REGISTRATION_CLIENT_ID);
837
+ if (!registration) url.searchParams.set("agent_name_hint", AGENT_NAME);
838
+ url.searchParams.set("ext_agent_host_id", hostId);
839
+ if (registration && options.previous?.idToken) {
840
+ url.searchParams.set("id_token_hint", options.previous.idToken);
841
+ }
842
+ url.searchParams.set("response_type", "code");
843
+ url.searchParams.set("redirect_uri", redirectUri);
844
+ url.searchParams.set("scope", SCOPES);
845
+ url.searchParams.set("resource", RESOURCE);
846
+ url.searchParams.set("state", state);
847
+ url.searchParams.set("nonce", nonce);
848
+ url.searchParams.set("code_challenge_method", "S256");
849
+ url.searchParams.set("code_challenge", challenge);
850
+ const callback = await acquireCallback(url.toString(), state, listener, callbacks);
851
+ if (callback.get("error")) {
852
+ if (callback.get("error") === "access_denied") throw new ChatGPTPlanNotEnabledError();
853
+ throw new Error(
854
+ `ChatGPT sign-in failed: ${callback.get("error_description") ?? callback.get("error")}`
855
+ );
856
+ }
857
+ const code = callback.get("code");
858
+ if (!code) throw new Error("ChatGPT sign-in returned no authorization code.");
859
+ const issued = callback.get("client_id") ?? void 0;
860
+ if (registration && issued && issued !== registration.clientId) {
861
+ throw new Error("ChatGPT returned a different app registration. Start the sign-in again.");
862
+ }
863
+ const clientId = registration?.clientId ?? issued;
864
+ if (!clientId || clientId === REGISTRATION_CLIENT_ID) {
865
+ throw new Error(
866
+ "ChatGPT didn't finish registering GG Coder. Start the sign-in again, and paste the full callback URL if you're on another machine."
867
+ );
868
+ }
869
+ const tokens = await exchangeCode({ code, verifier, clientId, redirectUri });
870
+ const claims = await verifyIdToken(tokens.idToken, { clientId, nonce });
871
+ const subject = String(claims.sub ?? "");
872
+ if (!subject) throw new Error("ChatGPT ID token has no subject.");
873
+ if (registration && registration.subject !== subject) {
874
+ await import_promises3.default.unlink(registrationFile).catch(() => {
875
+ });
876
+ await revokeChatGPTSignIn({ refreshToken: tokens.refreshToken, clientId });
877
+ throw new Error(
878
+ "That was a different ChatGPT account. Continue with ChatGPT again to connect it."
879
+ );
880
+ }
881
+ if (!tokens.scopes.includes(CHATGPT_PLAN_SCOPE)) {
882
+ await revokeChatGPTSignIn({ refreshToken: tokens.refreshToken, clientId });
883
+ throw new ChatGPTPlanNotEnabledError();
884
+ }
885
+ if (!registration) {
886
+ await writePrivateFile(registrationFile, JSON.stringify({ clientId, subject }));
887
+ }
575
888
  return {
576
- code: params.get("code") ?? void 0,
577
- state: params.get("state") ?? void 0
889
+ accessToken: tokens.accessToken,
890
+ refreshToken: tokens.refreshToken,
891
+ expiresAt: Date.now() + tokens.expiresIn * 1e3,
892
+ expiresIn: tokens.expiresIn,
893
+ accountId: subject,
894
+ clientId,
895
+ idToken: tokens.idToken,
896
+ scopes: tokens.scopes,
897
+ baseUrl: CHATGPT_PLAN_BASE_URL
578
898
  };
899
+ } finally {
900
+ listener.close();
579
901
  }
580
- if (/\s/.test(value) || value.length < MIN_RAW_CODE_LENGTH) return {};
581
- return { code: value };
582
902
  }
583
- function decodeJwt(token) {
584
- try {
585
- const parts = token.split(".");
586
- if (parts.length !== 3) return null;
587
- const decoded = atob(parts[1]);
588
- return JSON.parse(decoded);
589
- } catch {
590
- return null;
591
- }
903
+ async function fetchChatGPTPlanModels(accessToken, fetchFn = fetch) {
904
+ const response = await fetchFn(`${CHATGPT_PLAN_BASE_URL}/models`, {
905
+ headers: { Authorization: `Bearer ${accessToken}` },
906
+ signal: oauthRequestSignal()
907
+ });
908
+ if (!response.ok) throw new Error(`ChatGPT model list failed (${response.status})`);
909
+ const data = await response.json();
910
+ return (data.models ?? []).filter((m) => typeof m.slug === "string" && (m.visibility ?? "list") === "list").map((m) => m.slug);
592
911
  }
593
- function getAccountId(accessToken) {
594
- const payload = decodeJwt(accessToken);
595
- const auth = payload?.[JWT_CLAIM_PATH];
596
- const accountId = auth?.chatgpt_account_id;
597
- return typeof accountId === "string" && accountId.length > 0 ? accountId : null;
912
+ async function startCallbackListener(expectedState, anyPort) {
913
+ let settle;
914
+ const result = new Promise((resolve, reject) => {
915
+ settle = { resolve, reject };
916
+ });
917
+ result.catch(() => {
918
+ });
919
+ const server = import_node_http.default.createServer((req, res) => {
920
+ const url = new URL(req.url || "", "http://127.0.0.1");
921
+ if (url.pathname !== CALLBACK_PATH) {
922
+ res.statusCode = 404;
923
+ res.end("Not found");
924
+ return;
925
+ }
926
+ if (url.searchParams.get("state") !== expectedState) {
927
+ res.statusCode = 400;
928
+ res.end("State mismatch");
929
+ return;
930
+ }
931
+ res.writeHead(200, { "Content-Type": "text/html" });
932
+ res.end(
933
+ "<html><body><h1>You're signed in to GG Coder.</h1><p>You can close this tab.</p></body></html>"
934
+ );
935
+ settle?.resolve(url.searchParams);
936
+ server.close();
937
+ });
938
+ const listen = (port2) => new Promise((resolve, reject) => {
939
+ const onError = (err) => reject(err);
940
+ server.once("error", onError);
941
+ server.listen(port2, "127.0.0.1", () => {
942
+ server.off("error", onError);
943
+ resolve(server.address().port);
944
+ });
945
+ });
946
+ let port;
947
+ for (const candidate of anyPort ? [PREFERRED_PORT, 0] : [PREFERRED_PORT]) {
948
+ try {
949
+ port = await listen(candidate);
950
+ break;
951
+ } catch {
952
+ }
953
+ }
954
+ if (port === void 0) {
955
+ settle?.reject(new Error("Could not start the local sign-in listener."));
956
+ return { port: PREFERRED_PORT, result, close: () => {
957
+ } };
958
+ }
959
+ const timeout = setTimeout(() => server.close(), CALLBACK_TIMEOUT_MS);
960
+ timeout.unref();
961
+ server.on("close", () => {
962
+ clearTimeout(timeout);
963
+ settle?.reject(new Error("Sign-in listener closed without a callback."));
964
+ });
965
+ return { port, result, close: () => server.close() };
598
966
  }
599
- async function acquireAuthorizationCode(authUrl, expectedState, callbacks) {
967
+ async function acquireCallback(authUrl, expectedState, listener, callbacks) {
600
968
  callbacks.onOpenUrl(authUrl);
601
- callbacks.onStatus("Waiting for browser callback...");
969
+ callbacks.onStatus("Waiting for ChatGPT sign-in in your browser...");
602
970
  const done = new AbortController();
603
971
  try {
604
972
  return await new Promise((resolve, reject) => {
605
- let serverFailed = false;
973
+ let listenerFailed = false;
606
974
  let pasteError;
607
975
  const failIfExhausted = () => {
608
- if (!serverFailed || pasteError === void 0) return;
976
+ if (!listenerFailed || pasteError === void 0) return;
609
977
  reject(
610
- pasteError instanceof Error ? pasteError : new Error("Could not obtain an authorization code.")
978
+ pasteError instanceof Error ? pasteError : new Error("Could not complete the sign-in.")
611
979
  );
612
980
  };
613
- listenForCallback(expectedState, done.signal).then(resolve, () => {
614
- serverFailed = true;
981
+ listener.result.then(resolve, () => {
982
+ listenerFailed = true;
615
983
  failIfExhausted();
616
984
  });
617
- promptForCode(expectedState, callbacks, done.signal).then(resolve, (err) => {
985
+ promptForCallback(expectedState, callbacks, done.signal).then(resolve, (err) => {
618
986
  if (err instanceof FatalLoginError) {
619
987
  reject(err);
620
988
  return;
@@ -625,687 +993,159 @@ async function acquireAuthorizationCode(authUrl, expectedState, callbacks) {
625
993
  });
626
994
  } finally {
627
995
  done.abort();
996
+ listener.close();
628
997
  }
629
998
  }
630
999
  var FatalLoginError = class extends Error {
631
1000
  };
632
- async function promptForCode(expectedState, callbacks, signal) {
633
- let message = "If the browser is on another machine (SSH/headless), paste the callback URL or code here:";
634
- for (let attempt = 0; attempt < MAX_PASTE_ATTEMPTS; attempt++) {
635
- if (signal.aborted) throw new Error("Authorization code already received.");
636
- const raw = await callbacks.onPromptCode(message, signal);
637
- if (signal.aborted) throw new Error("Authorization code already received.");
638
- const parsed = parseAuthorizationInput(raw);
639
- if (parsed.code && parsed.state && parsed.state !== expectedState) {
640
- throw new FatalLoginError("Authorization state mismatch \u2014 start the login again.");
641
- }
642
- if (parsed.code) return parsed.code;
643
- message = "That didn't contain an authorization code. Paste the full callback URL:";
644
- }
645
- throw new Error("No authorization code found in input.");
646
- }
647
- async function listenForCallback(expectedState, signal) {
648
- return new Promise((resolve, reject) => {
649
- let receivedCode = null;
650
- const server = import_node_http.default.createServer((req, res) => {
651
- const url = new URL(req.url || "", "http://localhost");
652
- if (url.pathname !== "/auth/callback") {
653
- res.statusCode = 404;
654
- res.end("Not found");
655
- return;
656
- }
657
- if (url.searchParams.get("state") !== expectedState) {
658
- res.statusCode = 400;
659
- res.end("State mismatch");
660
- return;
661
- }
662
- receivedCode = url.searchParams.get("code");
663
- res.writeHead(200, { "Content-Type": "text/html" });
664
- res.end("<html><body><h1>Login successful!</h1><p>You can close this tab.</p></body></html>");
665
- server.close();
666
- });
667
- server.on("error", (err) => {
668
- reject(err);
669
- });
670
- server.listen(1455, "127.0.0.1");
671
- const timeout = setTimeout(() => {
672
- if (!receivedCode) server.close();
673
- }, CALLBACK_TIMEOUT_MS);
674
- timeout.unref();
675
- const onAbort = () => {
676
- server.close();
677
- };
678
- signal.addEventListener("abort", onAbort, { once: true });
679
- server.on("close", () => {
680
- clearTimeout(timeout);
681
- signal.removeEventListener("abort", onAbort);
682
- if (receivedCode) {
683
- resolve(receivedCode);
684
- } else {
685
- reject(new Error("Server closed without receiving code"));
686
- }
687
- });
688
- });
689
- }
690
- async function exchangeOpenAICode(code, verifier) {
691
- const response = await fetch(TOKEN_URL, {
692
- method: "POST",
693
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
694
- body: new URLSearchParams({
695
- grant_type: "authorization_code",
696
- client_id: CLIENT_ID3,
697
- code,
698
- redirect_uri: REDIRECT_URI2,
699
- code_verifier: verifier
700
- }),
701
- signal: oauthRequestSignal()
702
- });
703
- if (!response.ok) {
704
- const text = await response.text();
705
- throw new Error(`OpenAI token exchange failed (${response.status}): ${text}`);
706
- }
707
- const data = await response.json();
708
- return {
709
- accessToken: data.access_token,
710
- refreshToken: data.refresh_token,
711
- expiresAt: Date.now() + data.expires_in * 1e3
712
- };
713
- }
714
- async function refreshOpenAIToken(refreshToken) {
715
- const response = await fetch(TOKEN_URL, {
716
- method: "POST",
717
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
718
- body: new URLSearchParams({
719
- grant_type: "refresh_token",
720
- refresh_token: refreshToken,
721
- client_id: CLIENT_ID3
722
- }),
723
- signal: oauthRequestSignal()
724
- });
725
- if (!response.ok) {
726
- const text = await response.text();
727
- throw new Error(`OpenAI token refresh failed (${response.status}): ${text}`);
728
- }
729
- const data = await response.json();
730
- const creds = {
731
- accessToken: data.access_token,
732
- refreshToken: data.refresh_token,
733
- expiresAt: Date.now() + data.expires_in * 1e3
734
- };
735
- const accountId = getAccountId(creds.accessToken);
736
- if (accountId) {
737
- creds.accountId = accountId;
738
- }
739
- return creds;
740
- }
741
-
742
- // src/oauth/gemini.ts
743
- var import_node_http2 = __toESM(require("http"), 1);
744
- var import_node_crypto4 = __toESM(require("crypto"), 1);
745
- var CLIENT_ID_ENV = "GGCODER_GEMINI_OAUTH_CLIENT_ID";
746
- var CLIENT_SECRET_ENV = "GGCODER_GEMINI_OAUTH_CLIENT_SECRET";
747
- var DEFAULT_CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com";
748
- var DEFAULT_CLIENT_SECRET = "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";
749
- var AUTHORIZE_URL3 = "https://accounts.google.com/o/oauth2/v2/auth";
750
- var TOKEN_URL2 = "https://oauth2.googleapis.com/token";
751
- var CODE_ASSIST_BASE_URL = "https://cloudcode-pa.googleapis.com";
752
- var CODE_ASSIST_API_VERSION = "v1internal";
753
- var CODE_ASSIST_POST_RETRIES = 3;
754
- var CODE_ASSIST_POST_RETRY_DELAY_MS = 100;
755
- var SCOPE2 = [
756
- "https://www.googleapis.com/auth/cloud-platform",
757
- "https://www.googleapis.com/auth/userinfo.email",
758
- "https://www.googleapis.com/auth/userinfo.profile"
759
- ].join(" ");
760
- var USER_TIER_FREE = "free-tier";
761
- var USER_TIER_LEGACY = "legacy-tier";
762
- var USER_TIER_STANDARD = "standard-tier";
763
- var VALIDATION_REQUIRED_REASON = "VALIDATION_REQUIRED";
764
- var VPC_SC_REASON = "SECURITY_POLICY_VIOLATED";
765
- var CodeAssistHttpError = class extends Error {
766
- status;
767
- body;
768
- constructor(label, status, body) {
769
- super(`Gemini Code Assist ${label} failed (${status}): ${body}`);
770
- this.name = "CodeAssistHttpError";
771
- this.status = status;
772
- this.body = body;
773
- }
774
- };
775
- async function loginGemini(callbacks) {
776
- const { clientId: clientId2, clientSecret } = getGeminiOAuthClientCredentials();
777
- const { verifier, challenge } = await generatePKCE();
778
- const state = import_node_crypto4.default.randomBytes(32).toString("hex");
779
- const redirectUri = await getLoopbackRedirectUri();
780
- const url = new URL(AUTHORIZE_URL3);
781
- url.searchParams.set("response_type", "code");
782
- url.searchParams.set("client_id", clientId2);
783
- url.searchParams.set("redirect_uri", redirectUri);
784
- url.searchParams.set("scope", SCOPE2);
785
- url.searchParams.set("access_type", "offline");
786
- url.searchParams.set("prompt", "consent");
787
- url.searchParams.set("code_challenge", challenge);
788
- url.searchParams.set("code_challenge_method", "S256");
789
- url.searchParams.set("state", state);
790
- let code;
791
- try {
792
- code = await loginWithServer(url.toString(), redirectUri, state, callbacks);
793
- } catch {
794
- callbacks.onOpenUrl(url.toString());
795
- const raw = await callbacks.onPromptCode(
796
- "Could not start local server. Paste the callback URL or code from the browser:"
797
- );
798
- const parsed = parseAuthorizationInput2(raw);
799
- if (!parsed.code) {
800
- throw new Error("No authorization code found in input.");
801
- }
802
- if (parsed.state && parsed.state !== state) {
803
- throw new Error("Invalid state. Please try again.");
804
- }
805
- code = parsed.code;
806
- }
807
- const creds = await exchangeGeminiCode(code, verifier, redirectUri, clientId2, clientSecret);
808
- callbacks.onStatus("Setting up Gemini Code Assist access...");
809
- const projectId = await setupCodeAssistProject(creds.accessToken, callbacks);
810
- return {
811
- ...creds,
812
- projectId
813
- };
814
- }
815
- async function refreshGeminiToken(refreshToken) {
816
- const { clientId: clientId2, clientSecret } = getGeminiOAuthClientCredentials();
817
- const data = await postTokenRequest2({
818
- grant_type: "refresh_token",
819
- refresh_token: refreshToken,
820
- client_id: clientId2,
821
- client_secret: clientSecret
822
- });
823
- return {
824
- accessToken: data.access_token,
825
- refreshToken: data.refresh_token ?? refreshToken,
826
- expiresAt: Date.now() + data.expires_in * 1e3 - 5 * 60 * 1e3
827
- };
828
- }
829
- function getGeminiOAuthClientCredentials() {
830
- const clientId2 = process.env[CLIENT_ID_ENV]?.trim() || DEFAULT_CLIENT_ID;
831
- const clientSecret = process.env[CLIENT_SECRET_ENV]?.trim() || DEFAULT_CLIENT_SECRET;
832
- return { clientId: clientId2, clientSecret };
833
- }
834
- async function getLoopbackRedirectUri() {
835
- return new Promise((resolve, reject) => {
836
- const server = import_node_http2.default.createServer();
837
- server.listen(0, "127.0.0.1", () => {
838
- const addr = server.address();
839
- server.close(() => {
840
- if (addr && typeof addr === "object") {
841
- resolve(`http://127.0.0.1:${addr.port}/oauth2callback`);
842
- } else {
843
- reject(new Error("Failed to allocate OAuth callback port."));
844
- }
845
- });
846
- });
847
- server.on("error", reject);
848
- });
849
- }
850
- function parseAuthorizationInput2(input) {
1001
+ function parseCallbackInput(input) {
851
1002
  const value = input.trim();
852
- if (!value) return {};
1003
+ if (!value) return void 0;
853
1004
  try {
854
- const url = new URL(value);
855
- return {
856
- code: url.searchParams.get("code") ?? void 0,
857
- state: url.searchParams.get("state") ?? void 0
858
- };
1005
+ return new URL(value).searchParams;
859
1006
  } catch {
860
1007
  }
861
- if (value.includes("code=")) {
862
- const params = new URLSearchParams(value);
863
- return {
864
- code: params.get("code") ?? void 0,
865
- state: params.get("state") ?? void 0
866
- };
1008
+ if (/[?&]?(code|error)=/.test(value)) {
1009
+ return new URLSearchParams(value.replace(/^.*\?/, ""));
867
1010
  }
868
- return { code: value };
1011
+ return void 0;
869
1012
  }
870
- async function loginWithServer(authUrl, redirectUri, expectedState, callbacks) {
871
- const redirect = new URL(redirectUri);
872
- const port = Number(redirect.port);
873
- return new Promise((resolve, reject) => {
874
- let receivedCode = null;
875
- const server = import_node_http2.default.createServer((req, res) => {
876
- const url = new URL(req.url || "", redirect.origin);
877
- if (url.pathname !== redirect.pathname) {
878
- res.statusCode = 404;
879
- res.end("Not found");
880
- return;
881
- }
882
- if (url.searchParams.get("state") !== expectedState) {
883
- res.statusCode = 400;
884
- res.end("State mismatch");
885
- return;
886
- }
887
- receivedCode = url.searchParams.get("code");
888
- res.writeHead(200, { "Content-Type": "text/html", Connection: "close" });
889
- res.end("<html><body><h1>Login successful!</h1><p>You can close this tab.</p></body></html>");
890
- server.close();
891
- });
892
- server.on("error", (err) => reject(err));
893
- server.listen(port, "127.0.0.1", () => {
894
- callbacks.onOpenUrl(authUrl);
895
- callbacks.onStatus("Waiting for browser callback...");
896
- });
897
- const timeout = setTimeout(() => {
898
- if (!receivedCode) server.close();
899
- }, 12e4);
900
- timeout.unref();
901
- server.on("close", () => {
902
- clearTimeout(timeout);
903
- if (receivedCode) {
904
- resolve(receivedCode);
905
- } else {
906
- reject(new Error("Server closed without receiving code."));
1013
+ async function promptForCallback(expectedState, callbacks, signal) {
1014
+ let message = "If the browser is on another machine (SSH/headless), paste the full callback URL here:";
1015
+ for (let attempt = 0; attempt < MAX_PASTE_ATTEMPTS; attempt++) {
1016
+ if (signal.aborted) throw new Error("Callback already received.");
1017
+ const raw = await callbacks.onPromptCode(message, signal);
1018
+ if (signal.aborted) throw new Error("Callback already received.");
1019
+ const params = parseCallbackInput(raw);
1020
+ if (params && (params.get("code") || params.get("error"))) {
1021
+ if (params.get("state") !== expectedState) {
1022
+ throw new FatalLoginError("Sign-in state mismatch \u2014 start the sign-in again.");
907
1023
  }
908
- });
909
- });
910
- }
911
- async function exchangeGeminiCode(code, verifier, redirectUri, clientId2, clientSecret) {
912
- const data = await postTokenRequest2({
913
- grant_type: "authorization_code",
914
- client_id: clientId2,
915
- client_secret: clientSecret,
916
- code,
917
- redirect_uri: redirectUri,
918
- code_verifier: verifier
919
- });
920
- if (!data.refresh_token) {
921
- throw new Error("Gemini OAuth did not return a refresh token. Please try login again.");
1024
+ return params;
1025
+ }
1026
+ message = "That wasn't a callback URL. Paste the full URL from the browser's address bar:";
922
1027
  }
923
- return {
924
- accessToken: data.access_token,
925
- refreshToken: data.refresh_token,
926
- expiresAt: Date.now() + data.expires_in * 1e3 - 5 * 60 * 1e3
927
- };
1028
+ throw new Error("No callback URL found in input.");
928
1029
  }
929
- async function postTokenRequest2(body) {
930
- const response = await fetch(TOKEN_URL2, {
1030
+ async function postForm(url, form) {
1031
+ return fetch(url, {
931
1032
  method: "POST",
932
1033
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
933
- body: new URLSearchParams(body),
1034
+ body: new URLSearchParams(form),
934
1035
  signal: oauthRequestSignal()
935
1036
  });
936
- if (!response.ok) {
937
- const text = await response.text();
938
- throw new Error(`Gemini token request failed (${response.status}): ${text}`);
939
- }
940
- return await response.json();
941
- }
942
- async function setupCodeAssistProject(accessToken, callbacks) {
943
- const envProject = process.env.GOOGLE_CLOUD_PROJECT ?? process.env.GOOGLE_CLOUD_PROJECT_ID;
944
- if (envProject && /^\d+$/.test(envProject)) {
945
- throw new Error("GOOGLE_CLOUD_PROJECT must be a project ID, not a numeric project number.");
946
- }
947
- const coreMetadata = {
948
- ideType: "IDE_UNSPECIFIED",
949
- platform: "PLATFORM_UNSPECIFIED",
950
- pluginType: "GEMINI"
951
- };
952
- const projectMetadata = {
953
- ...coreMetadata,
954
- ...envProject ? { duetProject: envProject } : {}
955
- };
956
- let loadRes;
957
- while (true) {
958
- loadRes = await loadCodeAssist(accessToken, envProject, projectMetadata);
959
- const validation = getValidationRequiredTier(loadRes);
960
- if (!validation) break;
961
- callbacks.onStatus(
962
- `Gemini Code Assist requires account validation${validation.reasonMessage ? `: ${validation.reasonMessage}` : ""}`
963
- );
964
- callbacks.onOpenUrl(validation.validationUrl);
965
- const answer = await callbacks.onPromptCode(
966
- "Complete validation in the browser, then press Enter to retry (or type cancel):"
967
- );
968
- if (answer.trim().toLowerCase() === "cancel") {
969
- throw new Error("Gemini Code Assist account validation was cancelled.");
970
- }
971
- }
972
- if (loadRes.currentTier) {
973
- const project2 = loadRes.cloudaicompanionProject ?? envProject;
974
- if (!project2) throwProjectError(loadRes);
975
- return project2;
976
- }
977
- const tier = getOnboardTier(loadRes);
978
- const onboardReq = tier.id === USER_TIER_FREE ? {
979
- tierId: tier.id,
980
- cloudaicompanionProject: void 0,
981
- metadata: coreMetadata
982
- } : {
983
- tierId: tier.id,
984
- cloudaicompanionProject: envProject,
985
- metadata: projectMetadata
986
- };
987
- let operation = await codeAssistPost(
988
- accessToken,
989
- "onboardUser",
990
- onboardReq
991
- );
992
- while (!operation.done && operation.name) {
993
- await new Promise((resolve) => setTimeout(resolve, 5e3));
994
- operation = await codeAssistGet(accessToken, operation.name);
995
- }
996
- const project = operation.response?.cloudaicompanionProject?.id ?? envProject;
997
- if (!project) throwProjectError(loadRes);
998
- return project;
999
- }
1000
- async function loadCodeAssist(accessToken, envProject, metadata) {
1001
- try {
1002
- return await codeAssistPost(accessToken, "loadCodeAssist", {
1003
- ...envProject ? { cloudaicompanionProject: envProject } : {},
1004
- metadata
1005
- });
1006
- } catch (err) {
1007
- if (err instanceof CodeAssistHttpError && isVpcScAffectedError(err)) {
1008
- return { currentTier: { id: USER_TIER_STANDARD } };
1009
- }
1010
- if (err instanceof CodeAssistHttpError && err.status === 403 && envProject === "cloudshell-gca") {
1011
- throw new Error(
1012
- "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=...",
1013
- { cause: err }
1014
- );
1015
- }
1016
- throw err;
1017
- }
1018
- }
1019
- function getValidationRequiredTier(response) {
1020
- return response.ineligibleTiers?.find(
1021
- (tier) => tier.reasonCode === VALIDATION_REQUIRED_REASON && typeof tier.validationUrl === "string"
1022
- );
1023
- }
1024
- function getOnboardTier(response) {
1025
- const defaultTier = response.allowedTiers?.find((tier) => tier.isDefault);
1026
- return defaultTier ?? { id: USER_TIER_LEGACY, name: "" };
1027
- }
1028
- function throwProjectError(response) {
1029
- const reasons = response.ineligibleTiers?.map((tier) => tier.reasonMessage ?? tier.tierName).filter((reason) => Boolean(reason));
1030
- if (reasons && reasons.length > 0) {
1031
- throw new Error(`Gemini Code Assist setup failed: ${reasons.join(", ")}`);
1032
- }
1033
- throw new Error(
1034
- "Gemini requires a Google Cloud project for this account. Set GOOGLE_CLOUD_PROJECT and try again."
1035
- );
1036
- }
1037
- async function codeAssistPost(accessToken, method, body) {
1038
- let lastError;
1039
- for (let attempt = 0; attempt <= CODE_ASSIST_POST_RETRIES; attempt++) {
1040
- try {
1041
- return await codeAssistRequest(getCodeAssistMethodUrl(method), accessToken, method, {
1042
- method: "POST",
1043
- body: JSON.stringify(body)
1044
- });
1045
- } catch (err) {
1046
- if (!(err instanceof CodeAssistHttpError) || attempt === CODE_ASSIST_POST_RETRIES || !shouldRetryCodeAssistStatus(err.status)) {
1047
- throw err;
1048
- }
1049
- lastError = err;
1050
- }
1051
- await new Promise((resolve) => setTimeout(resolve, CODE_ASSIST_POST_RETRY_DELAY_MS));
1052
- }
1053
- throw lastError ?? new Error(`Gemini Code Assist ${method} failed.`);
1054
- }
1055
- async function codeAssistGet(accessToken, operationName) {
1056
- return codeAssistRequest(getCodeAssistOperationUrl(operationName), accessToken, "operation", {
1057
- method: "GET"
1058
- });
1059
1037
  }
1060
- async function codeAssistRequest(url, accessToken, label, init) {
1061
- const response = await fetch(url, {
1062
- ...init,
1063
- headers: codeAssistHeaders(accessToken),
1064
- signal: oauthRequestSignal()
1038
+ async function exchangeCode(input) {
1039
+ const response = await postForm(TOKEN_URL, {
1040
+ grant_type: "authorization_code",
1041
+ client_id: input.clientId,
1042
+ code: input.code,
1043
+ code_verifier: input.verifier,
1044
+ redirect_uri: input.redirectUri,
1045
+ resource: RESOURCE
1065
1046
  });
1066
1047
  if (!response.ok) {
1067
- const text = await response.text();
1068
- throw new CodeAssistHttpError(label, response.status, text);
1048
+ const text = await response.text().catch(() => "");
1049
+ throw new Error(`ChatGPT token exchange failed (${response.status}): ${text.slice(0, 300)}`);
1069
1050
  }
1070
- return await response.json();
1071
- }
1072
- function getCodeAssistBaseUrl() {
1073
- const endpoint = process.env.CODE_ASSIST_ENDPOINT ?? CODE_ASSIST_BASE_URL;
1074
- const version = process.env.CODE_ASSIST_API_VERSION || CODE_ASSIST_API_VERSION;
1075
- return `${endpoint}/${version}`;
1076
- }
1077
- function getCodeAssistMethodUrl(method) {
1078
- return `${getCodeAssistBaseUrl()}:${method}`;
1079
- }
1080
- function getCodeAssistOperationUrl(operationName) {
1081
- return `${getCodeAssistBaseUrl()}/${operationName}`;
1082
- }
1083
- function shouldRetryCodeAssistStatus(status) {
1084
- return status === 429 || status === 499 || status >= 500 && status <= 599;
1085
- }
1086
- function isVpcScAffectedError(error) {
1087
- try {
1088
- const parsed = JSON.parse(error.body);
1089
- if (!parsed || typeof parsed !== "object" || !("error" in parsed)) return false;
1090
- const details = parsed.error?.details;
1091
- return Array.isArray(details) ? details.some(
1092
- (detail) => detail != null && typeof detail === "object" && "reason" in detail && detail.reason === VPC_SC_REASON
1093
- ) : false;
1094
- } catch {
1095
- return false;
1051
+ const data = await response.json();
1052
+ if (!data.access_token || !data.refresh_token || !data.id_token) {
1053
+ throw new Error("ChatGPT token response is missing tokens.");
1096
1054
  }
1097
- }
1098
- function codeAssistHeaders(accessToken) {
1099
1055
  return {
1100
- Authorization: `Bearer ${accessToken}`,
1101
- "Content-Type": "application/json",
1102
- "User-Agent": "google-gemini-cli",
1103
- "X-Goog-Api-Client": "gemini-cli/0.0.0"
1104
- };
1105
- }
1106
-
1107
- // src/oauth/xai.ts
1108
- var DEFAULT_CLIENT_ID2 = "b1a00492-073a-47ea-816f-4c329264a828";
1109
- var DEFAULT_ISSUER = "https://auth.x.ai";
1110
- var DEFAULT_CLI_BASE_URL = "https://cli-chat-proxy.grok.com/v1";
1111
- var OAUTH_SCOPE = "openid profile email offline_access grok-cli:access api:access";
1112
- var DEFAULT_GROK_CLI_VERSION = "1.0.67";
1113
- var DEVICE_TIMEOUT_FALLBACK_MS = 10 * 60 * 1e3;
1114
- var DEFAULT_TOKEN_LIFETIME_SECONDS = 3600;
1115
- function issuer() {
1116
- return (process.env.XAI_OAUTH_ISSUER ?? DEFAULT_ISSUER).replace(/\/+$/, "");
1117
- }
1118
- function clientId() {
1119
- return process.env.XAI_OAUTH_CLIENT_ID ?? DEFAULT_CLIENT_ID2;
1120
- }
1121
- function grokCliBaseUrl() {
1122
- return (process.env.XAI_CLI_BASE_URL ?? DEFAULT_CLI_BASE_URL).replace(/\/+$/, "");
1123
- }
1124
- function grokCliVersion() {
1125
- const raw = process.env.GROK_CLI_VERSION ?? DEFAULT_GROK_CLI_VERSION;
1126
- const cleaned = raw.replace(/[^\u0020-\u007E]/g, "").trim();
1127
- return cleaned.length > 0 ? cleaned : DEFAULT_GROK_CLI_VERSION;
1128
- }
1129
- function grokCliHeaders(modelId) {
1130
- return {
1131
- "X-XAI-Token-Auth": "xai-grok-cli",
1132
- "x-grok-client-version": grokCliVersion(),
1133
- "x-grok-client-identifier": "ggcoder",
1134
- ...modelId ? { "x-grok-model-override": modelId } : {}
1056
+ accessToken: data.access_token,
1057
+ refreshToken: data.refresh_token,
1058
+ idToken: data.id_token,
1059
+ expiresIn: data.expires_in ?? 3600,
1060
+ scopes: (data.scope ?? "").split(/\s+/).filter(Boolean)
1135
1061
  };
1136
1062
  }
1137
- function isGrokCliEndpoint(baseUrl) {
1138
- if (typeof baseUrl !== "string" || baseUrl.length === 0) return false;
1139
- const normalized = baseUrl.replace(/\/+$/, "");
1140
- return normalized === grokCliBaseUrl() || /(^|\.)grok\.com/i.test(normalized);
1141
- }
1142
- async function postForm2(endpoint, params) {
1143
- const response = await fetch(`${issuer()}${endpoint}`, {
1144
- method: "POST",
1145
- headers: {
1146
- "Content-Type": "application/x-www-form-urlencoded",
1147
- Accept: "application/json",
1148
- "User-Agent": `grok-cli/${grokCliVersion()}`
1149
- },
1150
- body: new URLSearchParams(params).toString(),
1151
- signal: oauthRequestSignal()
1152
- });
1153
- let data = {};
1154
- try {
1155
- const parsed = await response.json();
1156
- if (parsed && typeof parsed === "object") data = parsed;
1157
- } catch {
1158
- }
1159
- return { status: response.status, data };
1160
- }
1161
- function errorDetail2(data) {
1162
- const desc = data.error_description ?? data.message ?? data.error;
1163
- return typeof desc === "string" && desc.length > 0 ? desc : "unknown error";
1164
- }
1165
- function jwtExpirySeconds(token) {
1166
- const payload = token.split(".")[1];
1167
- if (!payload) return void 0;
1168
- try {
1169
- const json = Buffer.from(payload.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString(
1170
- "utf-8"
1171
- );
1172
- const claims = JSON.parse(json);
1173
- if (!claims || typeof claims !== "object") return void 0;
1174
- const exp = claims.exp;
1175
- return typeof exp === "number" && Number.isFinite(exp) && exp > 0 ? exp : void 0;
1176
- } catch {
1177
- return void 0;
1178
- }
1179
- }
1180
- function credsFromTokenResponse2(data, opts) {
1181
- const accessToken = data.access_token;
1182
- const responseRefreshToken = data.refresh_token;
1183
- if (typeof accessToken !== "string" || accessToken.length === 0) {
1184
- throw new Error("Grok OAuth response missing access_token.");
1185
- }
1186
- const refreshToken = typeof responseRefreshToken === "string" && responseRefreshToken.length > 0 ? responseRefreshToken : opts?.fallbackRefreshToken ?? "";
1187
- if (refreshToken.length === 0) {
1188
- throw new Error(
1189
- "Grok OAuth response missing refresh_token \u2014 the offline_access scope was not granted."
1190
- );
1191
- }
1192
- const responseExpiresIn = Number(data.expires_in);
1193
- let expiresIn;
1194
- if (Number.isFinite(responseExpiresIn) && responseExpiresIn > 0) {
1195
- expiresIn = responseExpiresIn;
1196
- } else {
1197
- const exp = jwtExpirySeconds(accessToken);
1198
- const fromJwt = exp !== void 0 ? exp - Math.floor(Date.now() / 1e3) : 0;
1199
- expiresIn = fromJwt > 0 ? fromJwt : DEFAULT_TOKEN_LIFETIME_SECONDS;
1063
+ async function refreshChatGPTToken(creds) {
1064
+ if (!creds.clientId) {
1065
+ throw new Error("ChatGPT token refresh failed (401): no issued client ID saved");
1200
1066
  }
1201
- return {
1202
- accessToken,
1203
- refreshToken,
1204
- expiresAt: Date.now() + expiresIn * 1e3,
1205
- expiresIn,
1206
- baseUrl: grokCliBaseUrl()
1207
- };
1208
- }
1209
- async function requestDeviceAuthorization2() {
1210
- const { status, data } = await postForm2("/oauth2/device/code", {
1211
- client_id: clientId(),
1212
- scope: OAUTH_SCOPE
1067
+ const response = await postForm(TOKEN_URL, {
1068
+ grant_type: "refresh_token",
1069
+ client_id: creds.clientId,
1070
+ refresh_token: creds.refreshToken,
1071
+ resource: RESOURCE
1213
1072
  });
1214
- if (status !== 200) {
1215
- throw new Error(`Grok device authorization failed (${status}): ${errorDetail2(data)}`);
1216
- }
1217
- const userCode = data.user_code;
1218
- const deviceCode = data.device_code;
1219
- const verificationUriComplete = data.verification_uri_complete;
1220
- if (typeof userCode !== "string" || typeof deviceCode !== "string") {
1221
- throw new Error("Grok device authorization response missing user_code/device_code.");
1073
+ if (!response.ok) {
1074
+ const text = await response.text().catch(() => "");
1075
+ throw new Error(`ChatGPT token refresh failed (${response.status}): ${text.slice(0, 300)}`);
1222
1076
  }
1223
- return {
1224
- userCode,
1225
- deviceCode,
1226
- verificationUri: typeof data.verification_uri === "string" ? data.verification_uri : "",
1227
- verificationUriComplete: typeof verificationUriComplete === "string" ? verificationUriComplete : "",
1228
- interval: Number(data.interval ?? 5) || 5,
1229
- // RFC 8628 §3.2: the server states how long the device code lives. Honor it
1230
- // rather than imposing our own budget — polling a code we know is dead only
1231
- // burns requests, and a longer local window would keep a user waiting past
1232
- // the point the code can ever succeed.
1233
- expiresInMs: (Number(data.expires_in) || 0) > 0 ? Number(data.expires_in) * 1e3 : DEVICE_TIMEOUT_FALLBACK_MS
1234
- };
1235
- }
1236
- async function pollDeviceToken2(deviceCode) {
1237
- const { status, data } = await postForm2("/oauth2/token", {
1238
- client_id: clientId(),
1239
- device_code: deviceCode,
1240
- grant_type: "urn:ietf:params:oauth:grant-type:device_code"
1241
- });
1242
- if (status === 200 && typeof data.access_token === "string") {
1243
- return { kind: "success", creds: credsFromTokenResponse2(data) };
1244
- }
1245
- if (status >= 500) {
1246
- throw new Error(`Grok token polling server error (${status}): ${errorDetail2(data)}`);
1247
- }
1248
- const errorCode = typeof data.error === "string" ? data.error : "unknown_error";
1249
- switch (errorCode) {
1250
- case "authorization_pending":
1251
- return { kind: "pending" };
1252
- case "slow_down":
1253
- return { kind: "slow_down" };
1254
- case "expired_token":
1255
- return { kind: "expired" };
1256
- case "access_denied":
1257
- return { kind: "denied" };
1258
- default:
1259
- throw new Error(`Grok token polling failed (${status}): ${errorDetail2(data)}`);
1077
+ const data = await response.json();
1078
+ if (!data.access_token || !data.refresh_token) {
1079
+ throw new Error("ChatGPT token refresh returned no tokens.");
1260
1080
  }
1081
+ const expiresIn = data.expires_in ?? 3600;
1082
+ return {
1083
+ accessToken: data.access_token,
1084
+ refreshToken: data.refresh_token,
1085
+ expiresAt: Date.now() + expiresIn * 1e3,
1086
+ expiresIn,
1087
+ ...data.id_token ? { idToken: data.id_token } : {},
1088
+ ...data.scope ? { scopes: data.scope.split(/\s+/).filter(Boolean) } : {}
1089
+ };
1261
1090
  }
1262
- function sleep2(ms) {
1263
- return new Promise((resolve) => {
1264
- setTimeout(resolve, ms);
1265
- });
1266
- }
1267
- async function loginXai(callbacks) {
1268
- const auth = await requestDeviceAuthorization2();
1269
- callbacks.onStatus(
1270
- `Visit ${auth.verificationUri || auth.verificationUriComplete} and enter code: ${auth.userCode}`
1271
- );
1272
- callbacks.onOpenUrl(auth.verificationUriComplete || auth.verificationUri);
1273
- callbacks.onStatus("Waiting for you to authorize in the browser...");
1274
- const deadline = Date.now() + auth.expiresInMs;
1275
- let interval = Math.max(auth.interval, 1);
1276
- while (Date.now() < deadline) {
1277
- const remaining = deadline - Date.now();
1278
- await sleep2(Math.min(interval * 1e3, remaining));
1279
- const result = await pollDeviceToken2(auth.deviceCode);
1280
- if (result.kind === "success") return result.creds;
1281
- if (result.kind === "denied") {
1282
- throw new Error("Grok authorization was denied.");
1283
- }
1284
- if (result.kind === "expired") {
1285
- throw new Error("Grok device code expired. Please run login again.");
1091
+ async function revokeChatGPTSignIn(creds, options = {}) {
1092
+ if (!creds.clientId || !creds.refreshToken) return false;
1093
+ const attempts = options.attempts ?? 3;
1094
+ for (let attempt = 0; attempt < attempts; attempt++) {
1095
+ try {
1096
+ const response = await postForm(REVOKE_URL, {
1097
+ token: creds.refreshToken,
1098
+ token_type_hint: "refresh_token",
1099
+ client_id: creds.clientId
1100
+ });
1101
+ if (response.ok) return true;
1102
+ if (response.status < 500) return false;
1103
+ } catch {
1286
1104
  }
1287
- if (result.kind === "slow_down") {
1288
- interval += 5;
1105
+ if (attempt < attempts - 1) {
1106
+ await new Promise((r) => setTimeout(r, (options.backoffMs ?? 500) * 2 ** attempt));
1289
1107
  }
1290
1108
  }
1291
- throw new Error("Grok login timed out. Please run login again.");
1292
- }
1293
- async function refreshXaiToken(refreshToken) {
1294
- const { status, data } = await postForm2("/oauth2/token", {
1295
- client_id: clientId(),
1296
- grant_type: "refresh_token",
1297
- refresh_token: refreshToken
1298
- });
1299
- if (status === 200 && typeof data.access_token === "string") {
1300
- return credsFromTokenResponse2(data, { fallbackRefreshToken: refreshToken });
1109
+ return false;
1110
+ }
1111
+ function decodeSegment(segment) {
1112
+ return JSON.parse(Buffer.from(segment, "base64url").toString("utf-8"));
1113
+ }
1114
+ async function verifyIdToken(idToken, expected) {
1115
+ const [headerSeg, payloadSeg, signatureSeg] = idToken.split(".");
1116
+ if (!headerSeg || !payloadSeg || !signatureSeg) throw new Error("Malformed ChatGPT ID token.");
1117
+ const header = decodeSegment(headerSeg);
1118
+ if (header.alg !== "RS256") throw new Error("Unexpected ChatGPT ID token algorithm.");
1119
+ const response = await fetch(JWKS_URL, { signal: oauthRequestSignal() });
1120
+ if (!response.ok) throw new Error(`Could not load OpenAI signing keys (${response.status}).`);
1121
+ const { keys = [] } = await response.json();
1122
+ const jwk = keys.find((k) => k.kid === header.kid);
1123
+ if (!jwk) throw new Error("ChatGPT ID token is signed with an unknown key.");
1124
+ const valid = import_node_crypto2.default.verify(
1125
+ "RSA-SHA256",
1126
+ Buffer.from(`${headerSeg}.${payloadSeg}`),
1127
+ import_node_crypto2.default.createPublicKey({ key: jwk, format: "jwk" }),
1128
+ Buffer.from(signatureSeg, "base64url")
1129
+ );
1130
+ if (!valid) throw new Error("ChatGPT ID token signature is invalid.");
1131
+ const claims = decodeSegment(payloadSeg);
1132
+ const now = Math.floor((expected.now ?? Date.now()) / 1e3);
1133
+ const aud = claims.aud;
1134
+ const audiences = Array.isArray(aud) ? aud : [aud];
1135
+ if (claims.iss !== ISSUER) throw new Error("ChatGPT ID token has the wrong issuer.");
1136
+ if (!audiences.includes(expected.clientId)) {
1137
+ throw new Error("ChatGPT ID token was issued to another app.");
1301
1138
  }
1302
- const errorCode = typeof data.error === "string" ? data.error : "";
1303
- throw new Error(`Grok token refresh failed (${status}): ${errorCode || errorDetail2(data)}`);
1139
+ if (typeof claims.exp !== "number" || claims.exp + ID_TOKEN_CLOCK_SKEW_S < now) {
1140
+ throw new Error("ChatGPT ID token has expired.");
1141
+ }
1142
+ if (claims.nonce !== expected.nonce) throw new Error("ChatGPT ID token nonce mismatch.");
1143
+ return claims;
1304
1144
  }
1305
1145
 
1306
1146
  // src/file-lock.ts
1307
- var import_promises = __toESM(require("fs/promises"), 1);
1308
- var import_promises2 = require("timers/promises");
1147
+ var import_promises4 = __toESM(require("fs/promises"), 1);
1148
+ var import_promises5 = require("timers/promises");
1309
1149
  var STALE_TIMEOUT_MS = 1e4;
1310
1150
  var RETRY_INTERVAL_MS = 50;
1311
1151
  var MAX_WAIT_MS = 5e3;
@@ -1325,7 +1165,7 @@ async function acquireLock(lockPath) {
1325
1165
  while (true) {
1326
1166
  try {
1327
1167
  const info = { pid: process.pid, timestamp: Date.now() };
1328
- await import_promises.default.writeFile(lockPath, JSON.stringify(info), { flag: "wx" });
1168
+ await import_promises4.default.writeFile(lockPath, JSON.stringify(info), { flag: "wx" });
1329
1169
  return;
1330
1170
  } catch (err) {
1331
1171
  const code = err.code;
@@ -1334,47 +1174,47 @@ async function acquireLock(lockPath) {
1334
1174
  const lockPresent = await lockFileExists(lockPath);
1335
1175
  if (!lockPresent && ++refusedWithoutLock > 3) throw err;
1336
1176
  if (Date.now() - startTime > MAX_WAIT_MS) throw err;
1337
- await (0, import_promises2.setTimeout)(RETRY_INTERVAL_MS);
1177
+ await (0, import_promises5.setTimeout)(RETRY_INTERVAL_MS);
1338
1178
  continue;
1339
1179
  }
1340
1180
  try {
1341
- const content = await import_promises.default.readFile(lockPath, "utf-8");
1181
+ const content = await import_promises4.default.readFile(lockPath, "utf-8");
1342
1182
  const info = JSON.parse(content);
1343
1183
  const isProcessAlive = isAlive(info.pid);
1344
1184
  const isStale = Date.now() - info.timestamp > STALE_TIMEOUT_MS;
1345
1185
  if (!isProcessAlive || isStale) {
1346
- await import_promises.default.unlink(lockPath).catch(() => {
1186
+ await import_promises4.default.unlink(lockPath).catch(() => {
1347
1187
  });
1348
1188
  continue;
1349
1189
  }
1350
1190
  } catch {
1351
- const stat = await import_promises.default.stat(lockPath).catch(() => null);
1191
+ const stat = await import_promises4.default.stat(lockPath).catch(() => null);
1352
1192
  if (!stat) continue;
1353
1193
  if (Date.now() - stat.mtimeMs > STALE_TIMEOUT_MS) {
1354
- await import_promises.default.unlink(lockPath).catch(() => {
1194
+ await import_promises4.default.unlink(lockPath).catch(() => {
1355
1195
  });
1356
1196
  continue;
1357
1197
  }
1358
1198
  }
1359
1199
  if (Date.now() - startTime > MAX_WAIT_MS) {
1360
- await import_promises.default.unlink(lockPath).catch(() => {
1200
+ await import_promises4.default.unlink(lockPath).catch(() => {
1361
1201
  });
1362
1202
  continue;
1363
1203
  }
1364
- await (0, import_promises2.setTimeout)(RETRY_INTERVAL_MS);
1204
+ await (0, import_promises5.setTimeout)(RETRY_INTERVAL_MS);
1365
1205
  }
1366
1206
  }
1367
1207
  }
1368
1208
  async function lockFileExists(lockPath) {
1369
1209
  try {
1370
- await import_promises.default.stat(lockPath);
1210
+ await import_promises4.default.stat(lockPath);
1371
1211
  return true;
1372
1212
  } catch (err) {
1373
1213
  return err.code !== "ENOENT";
1374
1214
  }
1375
1215
  }
1376
1216
  async function releaseLock(lockPath) {
1377
- await import_promises.default.unlink(lockPath).catch(() => {
1217
+ await import_promises4.default.unlink(lockPath).catch(() => {
1378
1218
  });
1379
1219
  }
1380
1220
  function isAlive(pid) {
@@ -1388,9 +1228,9 @@ function isAlive(pid) {
1388
1228
  }
1389
1229
 
1390
1230
  // src/logger.ts
1391
- var import_node_fs2 = __toESM(require("fs"), 1);
1392
- var import_node_path3 = __toESM(require("path"), 1);
1393
- var import_node_crypto5 = require("crypto");
1231
+ var import_node_fs3 = __toESM(require("fs"), 1);
1232
+ var import_node_path4 = __toESM(require("path"), 1);
1233
+ var import_node_crypto3 = require("crypto");
1394
1234
  var import_gg_ai2 = require("@kenkaiiii/gg-ai");
1395
1235
  var MAX_BYTES = 10 * 1024 * 1024;
1396
1236
  var MIN_HEADROOM_BYTES = 64 * 1024;
@@ -1406,14 +1246,14 @@ var cleanups = [];
1406
1246
  var exactSecrets = [];
1407
1247
  function rotateIfNeeded(filePath, force = false) {
1408
1248
  try {
1409
- const st = import_node_fs2.default.statSync(filePath);
1249
+ const st = import_node_fs3.default.statSync(filePath);
1410
1250
  if (!force && st.size <= MAX_BYTES - MIN_HEADROOM_BYTES) return;
1411
1251
  const rotated = `${filePath}.1`;
1412
1252
  try {
1413
- import_node_fs2.default.unlinkSync(rotated);
1253
+ import_node_fs3.default.unlinkSync(rotated);
1414
1254
  } catch {
1415
1255
  }
1416
- import_node_fs2.default.renameSync(filePath, rotated);
1256
+ import_node_fs3.default.renameSync(filePath, rotated);
1417
1257
  } catch {
1418
1258
  }
1419
1259
  }
@@ -1421,18 +1261,18 @@ function rotateOpenLog() {
1421
1261
  if (fd === null || logPath === null) return false;
1422
1262
  const sizeBefore = fileBytes;
1423
1263
  try {
1424
- import_node_fs2.default.closeSync(fd);
1264
+ import_node_fs3.default.closeSync(fd);
1425
1265
  } catch {
1426
1266
  }
1427
1267
  fd = null;
1428
1268
  rotateIfNeeded(logPath, true);
1429
1269
  try {
1430
- fd = import_node_fs2.default.openSync(logPath, "a");
1431
- fileBytes = import_node_fs2.default.fstatSync(fd).size;
1270
+ fd = import_node_fs3.default.openSync(logPath, "a");
1271
+ fileBytes = import_node_fs3.default.fstatSync(fd).size;
1432
1272
  } catch {
1433
1273
  if (fd !== null) {
1434
1274
  try {
1435
- import_node_fs2.default.closeSync(fd);
1275
+ import_node_fs3.default.closeSync(fd);
1436
1276
  } catch {
1437
1277
  }
1438
1278
  }
@@ -1448,22 +1288,22 @@ function openLog(filePath, name) {
1448
1288
  appName = name;
1449
1289
  exactSecrets = (0, import_gg_ai2.environmentSecrets)(process.env);
1450
1290
  try {
1451
- import_node_fs2.default.mkdirSync(import_node_path3.default.dirname(filePath), { recursive: true, mode: 448 });
1291
+ import_node_fs3.default.mkdirSync(import_node_path4.default.dirname(filePath), { recursive: true, mode: 448 });
1452
1292
  } catch {
1453
1293
  }
1454
1294
  rotateIfNeeded(filePath);
1455
1295
  try {
1456
- fd = import_node_fs2.default.openSync(filePath, "a");
1296
+ fd = import_node_fs3.default.openSync(filePath, "a");
1457
1297
  } catch {
1458
1298
  fd = null;
1459
1299
  fileBytes = 0;
1460
1300
  return false;
1461
1301
  }
1462
1302
  try {
1463
- fileBytes = import_node_fs2.default.fstatSync(fd).size;
1303
+ fileBytes = import_node_fs3.default.fstatSync(fd).size;
1464
1304
  } catch {
1465
1305
  try {
1466
- import_node_fs2.default.closeSync(fd);
1306
+ import_node_fs3.default.closeSync(fd);
1467
1307
  } catch {
1468
1308
  }
1469
1309
  fd = null;
@@ -1473,10 +1313,10 @@ function openLog(filePath, name) {
1473
1313
  logPath = filePath;
1474
1314
  processBytes = 0;
1475
1315
  rotationFailed = false;
1476
- sessionId = (0, import_node_crypto5.randomBytes)(4).toString("hex");
1316
+ sessionId = (0, import_node_crypto3.randomBytes)(4).toString("hex");
1477
1317
  try {
1478
1318
  if (fileBytes < MAX_BYTES) {
1479
- const written = import_node_fs2.default.writeSync(fd, "\n");
1319
+ const written = import_node_fs3.default.writeSync(fd, "\n");
1480
1320
  fileBytes += written;
1481
1321
  processBytes += written;
1482
1322
  }
@@ -1511,9 +1351,9 @@ function log(level, category, message, data) {
1511
1351
  `;
1512
1352
  try {
1513
1353
  if (fileBytes + Buffer.byteLength(capLine) <= MAX_BYTES) {
1514
- fileBytes += import_node_fs2.default.writeSync(fd, capLine);
1354
+ fileBytes += import_node_fs3.default.writeSync(fd, capLine);
1515
1355
  }
1516
- import_node_fs2.default.closeSync(fd);
1356
+ import_node_fs3.default.closeSync(fd);
1517
1357
  } catch {
1518
1358
  }
1519
1359
  fd = null;
@@ -1523,7 +1363,7 @@ function log(level, category, message, data) {
1523
1363
  if (fileBytes + lineBytes > MAX_BYTES && !rotationFailed && !rotateOpenLog()) return;
1524
1364
  if (fd === null) return;
1525
1365
  try {
1526
- const written = import_node_fs2.default.writeSync(fd, line);
1366
+ const written = import_node_fs3.default.writeSync(fd, line);
1527
1367
  fileBytes += written;
1528
1368
  processBytes += written;
1529
1369
  } catch {
@@ -1536,7 +1376,7 @@ function closeLogger(opts) {
1536
1376
  if (fd !== null) {
1537
1377
  if (opts?.shutdownLine !== false) log("INFO", "shutdown", `${appName} shutting down`);
1538
1378
  try {
1539
- if (fd !== null) import_node_fs2.default.closeSync(fd);
1379
+ if (fd !== null) import_node_fs3.default.closeSync(fd);
1540
1380
  } catch {
1541
1381
  }
1542
1382
  }
@@ -1552,7 +1392,7 @@ function closeLogger(opts) {
1552
1392
  }
1553
1393
 
1554
1394
  // src/hidden-providers.ts
1555
- var HIDDEN_PROVIDERS = ["gemini"];
1395
+ var HIDDEN_PROVIDERS = [];
1556
1396
  function isHiddenProvider(provider) {
1557
1397
  return HIDDEN_PROVIDERS.includes(provider);
1558
1398
  }
@@ -1643,22 +1483,76 @@ async function detectXiaomiTokenPlanBaseUrl(apiKey, options = {}) {
1643
1483
  }
1644
1484
 
1645
1485
  // src/auth-storage.ts
1646
- var MOONSHOT_OAUTH_KEY = "moonshot-oauth";
1647
- var XAI_OAUTH_KEY = "xai-oauth";
1648
- var DUAL_AUTH_PROVIDERS = [
1486
+ var OPENAI_CHATGPT_KEY = "openai-chatgpt";
1487
+ var KIMI_CODE_KEY = "kimi-code";
1488
+ var RETIRED_SIGN_INS = [
1489
+ {
1490
+ provider: "anthropic",
1491
+ storageKey: "anthropic",
1492
+ message: "Your old Claude sign-in no longer works. Sign in with Claude again to keep using your subscription."
1493
+ },
1494
+ {
1495
+ provider: "openai",
1496
+ storageKey: "openai",
1497
+ message: "Your old ChatGPT sign-in no longer works. Continue with ChatGPT again, or add an OpenAI API key."
1498
+ },
1649
1499
  {
1650
1500
  provider: "moonshot",
1651
- oauthKey: MOONSHOT_OAUTH_KEY,
1652
- oauthLabel: "Kimi OAuth",
1653
- apiKeyLabel: "Moonshot API key",
1654
- restoreHint: 'Run "ggcoder login" and choose Kimi OAuth to restore OAuth auth.'
1501
+ storageKey: "moonshot-oauth",
1502
+ message: "Kimi sign-in was removed. Paste a Kimi Code plan key from the Kimi Code Console, or a Moonshot API key."
1655
1503
  },
1656
1504
  {
1657
1505
  provider: "xai",
1658
- oauthKey: XAI_OAUTH_KEY,
1659
- oauthLabel: "Grok OAuth",
1660
- apiKeyLabel: "xAI API key",
1661
- restoreHint: 'Run "ggcoder login" and choose Grok OAuth to restore OAuth auth.'
1506
+ storageKey: "xai-oauth",
1507
+ message: "Grok sign-in was removed: xAI offers SuperGrok and X Premium usage only in apps it partners with. Add an xAI API key instead."
1508
+ },
1509
+ {
1510
+ provider: "gemini",
1511
+ storageKey: "gemini",
1512
+ message: "Google sign-in was removed. Add a Gemini API key from Google AI Studio instead."
1513
+ }
1514
+ ];
1515
+ function isRetiredEntry(retired, value) {
1516
+ if (!value) return false;
1517
+ switch (retired.storageKey) {
1518
+ case "anthropic":
1519
+ return isLegacyClaudeOAuth(value);
1520
+ // These slots now hold API keys; only a refreshable sign-in token is retired.
1521
+ case "openai":
1522
+ case "gemini":
1523
+ return Boolean(value.refreshToken) || Boolean(value.accountId) || Boolean(value.projectId);
1524
+ default:
1525
+ return true;
1526
+ }
1527
+ }
1528
+ function isRetiredStorageKey(data, storageKey) {
1529
+ return RETIRED_SIGN_INS.some(
1530
+ (r) => r.storageKey === storageKey && isRetiredEntry(r, data[storageKey])
1531
+ );
1532
+ }
1533
+ function usableEntry(data, storageKey) {
1534
+ return isRetiredStorageKey(data, storageKey) ? void 0 : data[storageKey];
1535
+ }
1536
+ function storageKeyProvider(storageKey) {
1537
+ if (storageKey === OPENAI_CHATGPT_KEY) return "openai";
1538
+ if (storageKey === KIMI_CODE_KEY) return "moonshot";
1539
+ if (storageKey === XIAOMI_CREDITS_KEY) return "xiaomi";
1540
+ if (storageKey === GEMINI_API_KEY) return "gemini";
1541
+ return storageKey;
1542
+ }
1543
+ function apiKeyStorageKeys(provider) {
1544
+ if (provider === "moonshot") return [KIMI_CODE_KEY, "moonshot"];
1545
+ if (provider === "xiaomi") return ["xiaomi", XIAOMI_CREDITS_KEY];
1546
+ if (provider === "gemini") return [GEMINI_API_KEY];
1547
+ return [provider];
1548
+ }
1549
+ var DUAL_AUTH_PROVIDERS = [
1550
+ {
1551
+ provider: "openai",
1552
+ oauthKey: OPENAI_CHATGPT_KEY,
1553
+ oauthLabel: "ChatGPT plan",
1554
+ apiKeyLabel: "OpenAI API key",
1555
+ restoreHint: 'Run "ggcoder login" and choose Continue with ChatGPT to use your ChatGPT plan again.'
1662
1556
  }
1663
1557
  ];
1664
1558
  function dualAuthProvider(provider) {
@@ -1683,14 +1577,20 @@ function activeBaseUrlEntry(data, provider) {
1683
1577
  const dual = dualAuthProvider(provider);
1684
1578
  if (dual) {
1685
1579
  const oauth = data[dual.oauthKey];
1580
+ const apiKey = usableEntry(data, dual.provider);
1686
1581
  if (oauth) {
1687
1582
  const exhaustedUntil = oauth.usageExhaustedUntil ?? 0;
1688
- if (Date.now() < exhaustedUntil && data[dual.provider]) return data[dual.provider];
1583
+ if (Date.now() < exhaustedUntil && apiKey) return apiKey;
1689
1584
  return oauth;
1690
1585
  }
1691
- return data[dual.provider];
1586
+ return apiKey;
1692
1587
  }
1693
- return data[provider];
1588
+ const key = apiKeyStorageKeys(provider).find((k) => usableEntry(data, k));
1589
+ const entry = key ? data[key] : void 0;
1590
+ if (key === KIMI_CODE_KEY && entry && !entry.baseUrl) {
1591
+ return { ...entry, baseUrl: KIMI_CODE_DEFAULT_BASE_URL };
1592
+ }
1593
+ return entry;
1694
1594
  }
1695
1595
  var MINIMAX_REGION_RETRY_MS = 5 * 60 * 1e3;
1696
1596
  var miniMaxRegionProbes = /* @__PURE__ */ new Map();
@@ -1701,9 +1601,17 @@ function cachedMiniMaxRegion(apiKey) {
1701
1601
  miniMaxRegionProbes.set(apiKey, { at: Date.now(), result });
1702
1602
  return result;
1703
1603
  }
1604
+ var kimiRegionProbes = /* @__PURE__ */ new Map();
1605
+ function cachedKimiRegion(apiKey) {
1606
+ const cached = kimiRegionProbes.get(apiKey);
1607
+ if (cached && Date.now() - cached.at < MINIMAX_REGION_RETRY_MS) return cached.result;
1608
+ const result = detectKimiCodeBaseUrl(apiKey).catch(() => void 0);
1609
+ kimiRegionProbes.set(apiKey, { at: Date.now(), result });
1610
+ return result;
1611
+ }
1704
1612
  function readStoredBaseUrlSync(authFile, provider) {
1705
1613
  try {
1706
- const data = JSON.parse((0, import_node_fs3.readFileSync)(authFile, "utf-8"));
1614
+ const data = JSON.parse((0, import_node_fs4.readFileSync)(authFile, "utf-8"));
1707
1615
  return activeBaseUrlEntry(data, provider)?.baseUrl;
1708
1616
  } catch {
1709
1617
  return void 0;
@@ -1716,19 +1624,7 @@ function refreshThresholdMs(creds) {
1716
1624
  return Math.max(MIN_REFRESH_THRESHOLD_MS, lifetimeMs * REFRESH_THRESHOLD_RATIO);
1717
1625
  }
1718
1626
  var USAGE_EXHAUSTED_DEFAULT_MS = 15 * 60 * 1e3;
1719
- var STATIC_API_KEY_PROVIDERS = /* @__PURE__ */ new Set([
1720
- "glm",
1721
- "moonshot",
1722
- "xiaomi",
1723
- "minimax",
1724
- "deepseek",
1725
- "openrouter",
1726
- "huggingface",
1727
- "sakana",
1728
- "xai",
1729
- // Local endpoints: a fixed (usually placeholder) key, never refreshable.
1730
- "local"
1731
- ]);
1627
+ var REFRESHABLE_STORAGE_KEYS = /* @__PURE__ */ new Set([OPENAI_CHATGPT_KEY]);
1732
1628
  var AuthStorage = class {
1733
1629
  data = {};
1734
1630
  filePath;
@@ -1749,10 +1645,12 @@ var AuthStorage = class {
1749
1645
  refreshLocks = /* @__PURE__ */ new Map();
1750
1646
  detectMiniMaxRegion;
1751
1647
  detectXiaomiCluster;
1648
+ detectKimiRegion;
1752
1649
  constructor(filePath, options) {
1753
1650
  this.filePath = filePath ?? getAppPaths().authFile;
1754
1651
  this.detectMiniMaxRegion = options?.detectMiniMaxBaseUrl ?? cachedMiniMaxRegion;
1755
1652
  this.detectXiaomiCluster = options?.detectXiaomiTokenPlanBaseUrl ?? detectXiaomiTokenPlanBaseUrl;
1653
+ this.detectKimiRegion = options?.detectKimiCodeBaseUrl ?? cachedKimiRegion;
1756
1654
  }
1757
1655
  /** Path to the on-disk auth file. Useful for status output. */
1758
1656
  get path() {
@@ -1780,27 +1678,78 @@ var AuthStorage = class {
1780
1678
  return keys.find((key) => Boolean(this.data[key]));
1781
1679
  }
1782
1680
  /**
1783
- * True if the user has any usable auth for the logical provider. For a
1784
- * dual-auth provider (Kimi/Grok) either the OAuth credential or the API key
1785
- * satisfies it.
1681
+ * True if the user has any usable auth for the logical provider: the plan
1682
+ * sign-in or any of its API keys. A retired sign-in doesn't count.
1786
1683
  */
1787
1684
  async hasProviderAuth(provider) {
1788
1685
  if (isHiddenProvider(provider)) return false;
1789
1686
  await this.ensureFresh();
1790
1687
  const dual = dualAuthProvider(provider);
1791
1688
  if (dual) {
1792
- return Boolean(this.data[dual.oauthKey] || this.data[dual.provider]);
1689
+ return Boolean(this.data[dual.oauthKey] || usableEntry(this.data, dual.provider));
1793
1690
  }
1794
- if (provider === "xiaomi") {
1795
- return Boolean(this.data["xiaomi"] || this.data[XIAOMI_CREDITS_KEY]);
1796
- }
1797
- if (provider === "gemini") {
1798
- return Boolean(this.data["gemini"] || this.data[GEMINI_API_KEY]);
1691
+ if (provider === "anthropic") {
1692
+ return Boolean(this.data.anthropic) && !isLegacyClaudeOAuth(this.data.anthropic) || isClaudeCodeAuthMarker(this.data[CLAUDE_CODE_AUTH_KEY]);
1799
1693
  }
1800
1694
  if (provider === "local") {
1801
1695
  return Object.keys(this.data).some((key) => key.startsWith(LOCAL_AUTH_KEY_PREFIX));
1802
1696
  }
1803
- return Boolean(this.data[provider]);
1697
+ return apiKeyStorageKeys(provider).some((key) => Boolean(usableEntry(this.data, key)));
1698
+ }
1699
+ /** True if any of `provider`'s API keys is stored. A retired sign-in in a key slot doesn't count. */
1700
+ async hasApiKey(provider) {
1701
+ await this.ensureFresh();
1702
+ if (provider === "anthropic" && isLegacyClaudeOAuth(this.data.anthropic)) return false;
1703
+ return apiKeyStorageKeys(provider).some((key) => Boolean(usableEntry(this.data, key)));
1704
+ }
1705
+ /**
1706
+ * `provider`'s first stored API key, never its plan sign-in. For calls that
1707
+ * only an API key may make (OpenAI's Images API isn't on the ChatGPT plan).
1708
+ */
1709
+ async resolveApiKey(provider) {
1710
+ await this.ensureFresh();
1711
+ for (const key of apiKeyStorageKeys(provider)) {
1712
+ const creds = usableEntry(this.data, key);
1713
+ if (creds && !(provider === "anthropic" && isLegacyClaudeOAuth(creds))) return creds;
1714
+ }
1715
+ throw new NotLoggedInError(provider);
1716
+ }
1717
+ /** True when the user signed in with Claude through Claude Code (marker only, no token). */
1718
+ async hasClaudeCodeLogin() {
1719
+ await this.ensureFresh();
1720
+ return isClaudeCodeAuthMarker(this.data[CLAUDE_CODE_AUTH_KEY]);
1721
+ }
1722
+ /** Record a Claude Code sign-in. Claude Code keeps the token; auth.json gets only the marker. */
1723
+ async setClaudeCodeLogin() {
1724
+ await this.mutateLatest((latest) => {
1725
+ latest[CLAUDE_CODE_AUTH_KEY] = { ...CLAUDE_CODE_AUTH_MARKER };
1726
+ if (isLegacyClaudeOAuth(latest.anthropic)) delete latest.anthropic;
1727
+ });
1728
+ }
1729
+ /** Retired sign-ins still on disk, so the UI can ask the user to reconnect. */
1730
+ async legacySignIns() {
1731
+ await this.ensureFresh();
1732
+ return RETIRED_SIGN_INS.filter((r) => isRetiredEntry(r, this.data[r.storageKey]));
1733
+ }
1734
+ /**
1735
+ * Remove `provider`'s retired sign-in, if any. True when one was removed, so
1736
+ * the caller can tell the user once to reconnect.
1737
+ */
1738
+ async takeLegacySignIn(provider) {
1739
+ const retired = RETIRED_SIGN_INS.find((r) => r.provider === provider);
1740
+ if (!retired) return false;
1741
+ await this.ensureFresh();
1742
+ if (!isRetiredEntry(retired, this.data[retired.storageKey])) return false;
1743
+ let removed = false;
1744
+ await this.mutateLatest((latest) => {
1745
+ if (!isRetiredEntry(retired, latest[retired.storageKey])) return;
1746
+ delete latest[retired.storageKey];
1747
+ removed = true;
1748
+ });
1749
+ return removed;
1750
+ }
1751
+ async clearClaudeCodeLogin() {
1752
+ await this.clearCredentials(CLAUDE_CODE_AUTH_KEY);
1804
1753
  }
1805
1754
  /** Endpoint ids that currently have a `local:<id>` credential stored. */
1806
1755
  async listLocalEndpointIds() {
@@ -1835,18 +1784,18 @@ var AuthStorage = class {
1835
1784
  const oauthCreds = dual ? this.data[dual.oauthKey] : void 0;
1836
1785
  if (dual && oauthCreds) {
1837
1786
  const exhaustedUntil = oauthCreds.usageExhaustedUntil ?? 0;
1838
- const apiKeyActive = Date.now() < exhaustedUntil && Boolean(this.data[dual.provider]);
1787
+ const apiKeyActive = Date.now() < exhaustedUntil && Boolean(usableEntry(this.data, dual.provider));
1839
1788
  if (!apiKeyActive) return false;
1840
1789
  }
1841
- return STATIC_API_KEY_PROVIDERS.has(provider);
1790
+ return !REFRESHABLE_STORAGE_KEYS.has(provider);
1842
1791
  }
1843
1792
  /**
1844
1793
  * The base URL on the credential that is active right now, if any.
1845
1794
  * Synchronous — call only after load()/resolveCredentials() populated the
1846
- * snapshot. For a dual-auth provider this is the subscription endpoint (Kimi
1847
- * For Coding, the Grok CLI proxy) whenever the OAuth entry is the one
1848
- * resolveCredentials would serve (i.e. not currently usage-exhausted with an
1849
- * API key configured).
1795
+ * snapshot. For a dual-auth provider this is the plan endpoint whenever the
1796
+ * sign-in is the one resolveCredentials would serve (i.e. not currently
1797
+ * usage-exhausted with an API key configured); for a plan/pay-per-token key
1798
+ * split it is the plan key's endpoint when one is stored.
1850
1799
  */
1851
1800
  getStoredBaseUrl(provider) {
1852
1801
  return activeBaseUrlEntry(this.data, provider)?.baseUrl;
@@ -1855,7 +1804,7 @@ var AuthStorage = class {
1855
1804
  const first = !this.loaded;
1856
1805
  await withFileLock(this.filePath, async () => {
1857
1806
  try {
1858
- const content = await import_promises3.default.readFile(this.filePath, "utf-8");
1807
+ const content = await import_promises6.default.readFile(this.filePath, "utf-8");
1859
1808
  this.data = JSON.parse(content);
1860
1809
  if (first) {
1861
1810
  log("INFO", "auth", `Loaded credentials from ${this.filePath}`, {
@@ -1901,7 +1850,7 @@ var AuthStorage = class {
1901
1850
  }
1902
1851
  let changed;
1903
1852
  try {
1904
- const stat = await import_promises3.default.stat(this.filePath);
1853
+ const stat = await import_promises6.default.stat(this.filePath);
1905
1854
  changed = stat.ino !== this.snapshotIno || stat.mtimeMs !== this.snapshotMtimeMs || stat.size !== this.snapshotSize;
1906
1855
  } catch {
1907
1856
  changed = this.snapshotSize !== -1;
@@ -1914,7 +1863,7 @@ var AuthStorage = class {
1914
1863
  */
1915
1864
  async rememberSnapshot() {
1916
1865
  try {
1917
- const stat = await import_promises3.default.stat(this.filePath);
1866
+ const stat = await import_promises6.default.stat(this.filePath);
1918
1867
  this.snapshotMtimeMs = stat.mtimeMs;
1919
1868
  this.snapshotSize = stat.size;
1920
1869
  this.snapshotIno = stat.ino;
@@ -1969,9 +1918,34 @@ var AuthStorage = class {
1969
1918
  });
1970
1919
  return { ...creds, baseUrl };
1971
1920
  }
1972
- async setCredentials(provider, creds) {
1921
+ /**
1922
+ * A Kimi Code key saved without an endpoint gets the host that issued it
1923
+ * (`api.kimi.com` or `api.kimi.ai`), saved so the check runs once.
1924
+ */
1925
+ async withKimiRegion(creds) {
1926
+ const baseUrl = await this.detectKimiRegion(creds.accessToken);
1927
+ if (!baseUrl) return { ...creds, baseUrl: KIMI_CODE_DEFAULT_BASE_URL };
1928
+ log("INFO", "auth", `Kimi Code key belongs to ${baseUrl} \u2014 saved for future requests`);
1929
+ await this.mutateLatest((latest) => {
1930
+ const current = latest[KIMI_CODE_KEY];
1931
+ if (current?.accessToken === creds.accessToken && !current.baseUrl) {
1932
+ current.baseUrl = baseUrl;
1933
+ }
1934
+ });
1935
+ return { ...creds, baseUrl };
1936
+ }
1937
+ /**
1938
+ * Store `creds` under `storageKey`. Connecting a provider again also removes
1939
+ * its retired sign-in, which is what the reconnect notice asked for.
1940
+ */
1941
+ async setCredentials(storageKey, creds) {
1942
+ const provider = storageKeyProvider(storageKey);
1973
1943
  await this.mutateLatest((latest) => {
1974
- latest[provider] = creds;
1944
+ for (const retired of RETIRED_SIGN_INS) {
1945
+ if (retired.provider !== provider || retired.storageKey === storageKey) continue;
1946
+ if (isRetiredEntry(retired, latest[retired.storageKey])) delete latest[retired.storageKey];
1947
+ }
1948
+ latest[storageKey] = creds;
1975
1949
  });
1976
1950
  }
1977
1951
  /**
@@ -2008,9 +1982,9 @@ var AuthStorage = class {
2008
1982
  * Mark the credential stored under `storageKey` as usage-exhausted until
2009
1983
  * `resetsAt` (unix SECONDS, from the provider's rate-limit response) or a
2010
1984
  * 15-minute default when no reset time is known. While the mark is in the
2011
- * future, `resolveCredentials("moonshot")` serves the Moonshot API key
2012
- * instead of the subscription OAuth credential (when both are configured) — OAuth
2013
- * stays the preferred credential and is retried automatically once the mark
1985
+ * future, `resolveCredentials("openai")` serves the OpenAI API key instead of
1986
+ * the ChatGPT plan sign-in (when both are configured) — the plan stays the
1987
+ * preferred credential and is retried automatically once the mark
2014
1988
  * lapses. Persisted to auth.json so a restart (or another gg-app window)
2015
1989
  * doesn't burn a request rediscovering the same exhausted window. No-op if
2016
1990
  * nothing is stored under `storageKey`.
@@ -2055,12 +2029,12 @@ var AuthStorage = class {
2055
2029
  await this.ensureFresh();
2056
2030
  }
2057
2031
  const directStorageKeys = opts?.storageKeys && !(opts.storageKeys.length === 1 && opts.storageKeys[0] === provider) ? opts.storageKeys : providerStorageKeys(provider);
2058
- if (!directStorageKeys.some((key) => Boolean(this.data[key]))) {
2032
+ if (!directStorageKeys.some((key) => Boolean(usableEntry(this.data, key))) && !(provider === "anthropic" && this.data[CLAUDE_CODE_AUTH_KEY])) {
2059
2033
  await this.reloadLatest();
2060
2034
  }
2061
2035
  if (opts?.storageKeys && !(opts.storageKeys.length === 1 && opts.storageKeys[0] === provider)) {
2062
2036
  for (const key of opts.storageKeys) {
2063
- const creds2 = this.data[key];
2037
+ const creds2 = usableEntry(this.data, key);
2064
2038
  if (!creds2) continue;
2065
2039
  if (key !== provider && dualAuthProviderByOAuthKey(key)) {
2066
2040
  return await this.resolveCredentials(key, {
@@ -2074,21 +2048,29 @@ var AuthStorage = class {
2074
2048
  ...opts.rejectedToken !== void 0 ? { rejectedToken: opts.rejectedToken } : {}
2075
2049
  });
2076
2050
  }
2051
+ if (key === KIMI_CODE_KEY && !creds2.baseUrl) return await this.withKimiRegion(creds2);
2077
2052
  return creds2;
2078
2053
  }
2079
2054
  throw new NotLoggedInError(provider);
2080
2055
  }
2056
+ if (provider === "anthropic" && (!this.data.anthropic || isLegacyClaudeOAuth(this.data.anthropic))) {
2057
+ if (isClaudeCodeAuthMarker(this.data[CLAUDE_CODE_AUTH_KEY])) {
2058
+ return { accessToken: "", refreshToken: "", expiresAt: Number.MAX_SAFE_INTEGER };
2059
+ }
2060
+ if (this.data.anthropic) throw new NotLoggedInError(provider);
2061
+ }
2081
2062
  const dual = dualAuthProvider(provider);
2082
2063
  const dualOAuthCreds = dual ? this.data[dual.oauthKey] : void 0;
2064
+ const dualApiKey = dual ? usableEntry(this.data, dual.provider) : void 0;
2083
2065
  if (dual && dualOAuthCreds) {
2084
2066
  const exhaustedUntil = dualOAuthCreds.usageExhaustedUntil ?? 0;
2085
- if (Date.now() < exhaustedUntil && this.data[dual.provider]) {
2067
+ if (Date.now() < exhaustedUntil && dualApiKey) {
2086
2068
  log(
2087
2069
  "WARN",
2088
2070
  "auth",
2089
2071
  `${dual.oauthLabel} usage window is exhausted \u2014 using the ${dual.apiKeyLabel} until ${new Date(exhaustedUntil).toISOString()} (OAuth resumes automatically).`
2090
2072
  );
2091
- return this.data[dual.provider];
2073
+ return dualApiKey;
2092
2074
  }
2093
2075
  try {
2094
2076
  return await this.resolveCredentials(dual.oauthKey, {
@@ -2096,23 +2078,24 @@ var AuthStorage = class {
2096
2078
  ...opts?.rejectedToken !== void 0 ? { rejectedToken: opts.rejectedToken } : {}
2097
2079
  });
2098
2080
  } catch (err) {
2099
- if (err instanceof NotLoggedInError && this.data[dual.provider]) {
2081
+ if (err instanceof NotLoggedInError && dualApiKey) {
2100
2082
  log(
2101
2083
  "WARN",
2102
2084
  "auth",
2103
2085
  `${dual.oauthLabel} credential is no longer valid \u2014 falling back to the ${dual.apiKeyLabel}. ${dual.restoreHint}`
2104
2086
  );
2105
- return this.data[dual.provider];
2087
+ return dualApiKey;
2106
2088
  }
2107
2089
  throw err;
2108
2090
  }
2109
2091
  }
2110
- const creds = this.data[provider];
2092
+ const creds = usableEntry(this.data, provider);
2111
2093
  if (!creds) {
2112
2094
  throw new NotLoggedInError(provider);
2113
2095
  }
2114
- if (STATIC_API_KEY_PROVIDERS.has(provider)) {
2096
+ if (!REFRESHABLE_STORAGE_KEYS.has(provider)) {
2115
2097
  if (provider === "minimax" && !creds.baseUrl) return await this.withMiniMaxRegion(creds);
2098
+ if (provider === KIMI_CODE_KEY && !creds.baseUrl) return await this.withKimiRegion(creds);
2116
2099
  return creds;
2117
2100
  }
2118
2101
  if (!opts?.forceRefresh && Date.now() < creds.expiresAt - refreshThresholdMs(creds)) {
@@ -2140,13 +2123,14 @@ var AuthStorage = class {
2140
2123
  this.data = latest;
2141
2124
  return latestCreds;
2142
2125
  }
2143
- const refreshFn = provider === "anthropic" ? refreshAnthropicToken : provider === "gemini" ? refreshGeminiToken : provider === MOONSHOT_OAUTH_KEY ? refreshKimiToken : provider === XAI_OAUTH_KEY ? refreshXaiToken : refreshOpenAIToken;
2144
2126
  let refreshed;
2145
2127
  try {
2146
- refreshed = await refreshFn(latestCreds.refreshToken);
2128
+ refreshed = await refreshChatGPTToken(latestCreds);
2147
2129
  } catch (err) {
2148
2130
  const msg = err instanceof Error ? err.message : String(err);
2149
- const isAuthFailure = /\((401|400)\)/.test(msg) || /invalid_grant|invalid_token|invalid.*refresh/i.test(msg) || /unauthorized/i.test(msg);
2131
+ const isAuthFailure = /\((401|400)\)/.test(msg) || /invalid_grant|invalid_token|invalid.*refresh|token_expired|refresh_token_(?:expired|invalidated|reused)/i.test(
2132
+ msg
2133
+ ) || /unauthorized/i.test(msg);
2150
2134
  if (isAuthFailure) {
2151
2135
  const current = await readAuthData(this.filePath);
2152
2136
  const currentCreds = current[provider];
@@ -2166,15 +2150,10 @@ var AuthStorage = class {
2166
2150
  }
2167
2151
  throw err;
2168
2152
  }
2169
- if (!refreshed.accountId && latestCreds.accountId) {
2170
- refreshed.accountId = latestCreds.accountId;
2171
- }
2172
- if (!refreshed.projectId && latestCreds.projectId) {
2173
- refreshed.projectId = latestCreds.projectId;
2174
- }
2175
- if (!refreshed.baseUrl && latestCreds.baseUrl) {
2176
- refreshed.baseUrl = latestCreds.baseUrl;
2177
- }
2153
+ refreshed = {
2154
+ ...pick(latestCreds, ["accountId", "baseUrl", "clientId", "idToken", "scopes"]),
2155
+ ...refreshed
2156
+ };
2178
2157
  latest[provider] = refreshed;
2179
2158
  this.data = latest;
2180
2159
  await atomicWriteFile(this.filePath, JSON.stringify(latest, null, 2));
@@ -2197,9 +2176,14 @@ var AuthStorage = class {
2197
2176
  return creds.accessToken;
2198
2177
  }
2199
2178
  };
2179
+ function pick(value, keys) {
2180
+ const out = {};
2181
+ for (const key of keys) if (value[key] !== void 0) out[key] = value[key];
2182
+ return out;
2183
+ }
2200
2184
  async function readAuthData(filePath) {
2201
2185
  try {
2202
- const content = await import_promises3.default.readFile(filePath, "utf-8");
2186
+ const content = await import_promises6.default.readFile(filePath, "utf-8");
2203
2187
  return JSON.parse(content);
2204
2188
  } catch (error) {
2205
2189
  if (error.code === "ENOENT") return {};
@@ -2207,12 +2191,12 @@ async function readAuthData(filePath) {
2207
2191
  }
2208
2192
  }
2209
2193
  async function atomicWriteFile(filePath, content) {
2210
- const tmpPath = `${filePath}.${process.pid}.${Date.now()}.${import_node_crypto6.default.randomUUID().slice(0, 8)}.tmp`;
2194
+ const tmpPath = `${filePath}.${process.pid}.${Date.now()}.${import_node_crypto4.default.randomUUID().slice(0, 8)}.tmp`;
2211
2195
  try {
2212
- await import_promises3.default.writeFile(tmpPath, content, { encoding: "utf-8", mode: 384 });
2213
- await import_promises3.default.rename(tmpPath, filePath);
2196
+ await import_promises6.default.writeFile(tmpPath, content, { encoding: "utf-8", mode: 384 });
2197
+ await import_promises6.default.rename(tmpPath, filePath);
2214
2198
  } catch (err) {
2215
- await import_promises3.default.unlink(tmpPath).catch(() => {
2199
+ await import_promises6.default.unlink(tmpPath).catch(() => {
2216
2200
  });
2217
2201
  throw err;
2218
2202
  }
@@ -2244,7 +2228,8 @@ var CODEX_FAST_BILLING = {
2244
2228
  detail: "Codex Fast mode uses your ChatGPT plan limits at 2.5x the standard rate (purchased credits at 2x).",
2245
2229
  paid: false
2246
2230
  };
2247
- var GEMINI_AUTH_KEYS = [GEMINI_API_KEY, "gemini"];
2231
+ var GEMINI_AUTH_KEYS = [GEMINI_API_KEY];
2232
+ var KIMI_AUTH_KEYS = [KIMI_CODE_KEY, "moonshot"];
2248
2233
  var MODELS = [
2249
2234
  // ── Anthropic ──────────────────────────────────────────
2250
2235
  // NOTE: Claude Mythos 5.1 (`claude-mythos-5-1`, 2026-09-01) is kept commented
@@ -2374,14 +2359,13 @@ var MODELS = [
2374
2359
  {
2375
2360
  // GPT-6 Astra — "Our most capable model for complex, demanding work."
2376
2361
  // (Codex catalog priority 2, listed for every ChatGPT plan, requires a
2377
- // Codex client >= 0.153.0 — see CODEX_CLIENT_VERSION). Same split as 5.6:
2378
- // 1.05M on the public Responses API, 272K on the ChatGPT OAuth route
2362
+ // Codex client >= 0.153.0). Same split as 5.6:
2363
+ // 1.05M on the public Responses API, 272K on the ChatGPT plan route
2379
2364
  // (openai/codex models.json, `gpt-6-astra`). Reasoning ladder low → medium
2380
2365
  // → high → xhigh → max → ultra; `ultra` is the Codex orchestration preset
2381
2366
  // (multi_agent v2) and is Codex-only — the public API tops out at `max`.
2382
- // Note: through a plain API key OpenAI requires the Responses API for tool
2383
- // calling on Astra, so the Chat Completions path is text-only; the OAuth
2384
- // Codex route is the supported way to use it as an agent.
2367
+ // OpenAI requires the Responses API for tool calling on Astra, so both
2368
+ // the ChatGPT plan and API keys ride gg-ai's Responses transport.
2385
2369
  id: "gpt-6-astra",
2386
2370
  name: "GPT-6 Astra",
2387
2371
  provider: "openai",
@@ -2415,18 +2399,16 @@ var MODELS = [
2415
2399
  billing: CODEX_FAST_BILLING
2416
2400
  },
2417
2401
  // GPT-6 Sol + Luna — released 2026-09-22 below Astra, replacing the whole
2418
- // GPT-5.6 family (Sol/Terra/Luna; there is no GPT-6 Terra — OpenAI's Codex
2402
+ // GPT-5.6 family (Sol/Terra/Luna; there was no GPT-6 Terra — OpenAI's Codex
2419
2403
  // catalog upgrades 5.6 Terra to 6 Sol). GPT-6.1 Sol (2026-09-29) then replaced
2420
- // GPT-6 Sol. Same window split as Astra: 1.05M on the public Responses API,
2404
+ // GPT-6 Sol, and GPT-6.1 Terra (2026-10-26) sits between Sol and Luna. Same window split as Astra: 1.05M on the public Responses API,
2421
2405
  // 272K on the Codex route; 128K output, text+image input, freeform
2422
2406
  // apply_patch, responses-lite transport. The GPT-5.6 ids and `gpt-6-sol` are
2423
2407
  // retired — a saved session on one falls back to the provider default on
2424
2408
  // next start.
2425
2409
  {
2426
2410
  // GPT-6.1 Sol — "Latest workhorse model for coding and everyday work."
2427
- // (Codex priority 1, default low). The catalog says Codex client >= 0.153.0,
2428
- // but the ChatGPT backend only serves it from 0.159.0 (see
2429
- // CODEX_CLIENT_VERSION). $2/$10 MTok, cached input $0.10. Ladder low →
2411
+ // (Codex priority 1, default low). $2/$10 MTok, cached input $0.10. Ladder low →
2430
2412
  // medium → high → xhigh → max → ultra; ultra is the Codex orchestration
2431
2413
  // preset (xhigh effort on the wire + proactive local subagent delegation).
2432
2414
  id: "gpt-6.1-sol",
@@ -2459,6 +2441,40 @@ var MODELS = [
2459
2441
  maxThinkingLevel: "ultra",
2460
2442
  billing: CODEX_FAST_BILLING
2461
2443
  },
2444
+ {
2445
+ // GPT-6.1 Terra (Codex priority 7, default low, client >= 0.153.0). $1/$5
2446
+ // MTok, cached input $0.05. Unlike Sol/Astra the Codex route gets a 500K
2447
+ // window (catalog `context_window`). Same ladder and Ultra preset as Sol.
2448
+ id: "gpt-6.1-terra",
2449
+ name: "GPT-6.1 Terra",
2450
+ provider: "openai",
2451
+ contextWindow: 105e4,
2452
+ codexContextWindow: 5e5,
2453
+ maxOutputTokens: 128e3,
2454
+ supportsThinking: true,
2455
+ defaultThinkingLevel: "low",
2456
+ supportsImages: true,
2457
+ supportsVideo: false,
2458
+ costTier: "medium",
2459
+ maxThinkingLevel: "ultra"
2460
+ },
2461
+ {
2462
+ // Codex Fast mode for 6.1 Terra ("1.5x speed") — see `gpt-6-astra-fast`
2463
+ // for the wire mapping and billing.
2464
+ id: "gpt-6.1-terra-fast",
2465
+ name: "GPT-6.1 Terra Fast",
2466
+ provider: "openai",
2467
+ contextWindow: 105e4,
2468
+ codexContextWindow: 5e5,
2469
+ maxOutputTokens: 128e3,
2470
+ supportsThinking: true,
2471
+ defaultThinkingLevel: "low",
2472
+ supportsImages: true,
2473
+ supportsVideo: false,
2474
+ costTier: "medium",
2475
+ maxThinkingLevel: "ultra",
2476
+ billing: CODEX_FAST_BILLING
2477
+ },
2462
2478
  {
2463
2479
  // Luna — "Fast and affordable model for easier tasks." (Codex priority 4,
2464
2480
  // default medium, needs a Codex client >= 0.155.0). $0.10/$0.50 MTok.
@@ -2579,8 +2595,8 @@ var MODELS = [
2579
2595
  maxThinkingLevel: "high",
2580
2596
  authStorageKeys: GEMINI_AUTH_KEYS
2581
2597
  },
2582
- // Keep 3.1 Flash Lite first for the working OAuth default and fast-model routing.
2583
- // New GA models are opt-in; Code Assist access varies by account. Gemini 3.7
2598
+ // Keep 3.1 Flash Lite first for the default and fast-model routing.
2599
+ // Gemini 3.7
2584
2600
  // Flash is retired: Google deprecated it on 2026-10-08 and routes every
2585
2601
  // `gemini-3.7-flash` request to 3.8 Flash (ai.google.dev/gemini-api/docs/
2586
2602
  // changelog), so a saved session on it falls back to the provider default.
@@ -2613,9 +2629,7 @@ var MODELS = [
2613
2629
  authStorageKeys: GEMINI_AUTH_KEYS
2614
2630
  },
2615
2631
  {
2616
- // Wire name `gemini-3-flash` — the Code Assist (OAuth) backend rejects the
2617
- // display string `gemini-3.5-flash` with a 404, so gemini-cli keeps this
2618
- // alternative name (SECONDARY_GEMINI_3_5_FLASH_MODEL) for that endpoint.
2632
+ // Wire name `gemini-3-flash`, the id Google serves 3.5 Flash under.
2619
2633
  id: "gemini-3-flash",
2620
2634
  name: "Gemini 3.5 Flash",
2621
2635
  provider: "gemini",
@@ -2630,9 +2644,7 @@ var MODELS = [
2630
2644
  authStorageKeys: GEMINI_AUTH_KEYS
2631
2645
  },
2632
2646
  {
2633
- // Gemini 3.1 Pro is public preview — gated behind Code Assist preview
2634
- // enablement, so free/personal OAuth accounts 404 on it (see
2635
- // ACCOUNT_GATED_MODELS in gg-ai's gemini provider).
2647
+ // Gemini 3.1 Pro is public preview.
2636
2648
  id: "gemini-3.1-pro-preview",
2637
2649
  name: "Gemini 3.1 Pro (Preview)",
2638
2650
  provider: "gemini",
@@ -2649,8 +2661,8 @@ var MODELS = [
2649
2661
  // ── Moonshot (Kimi) ────────────────────────────────────
2650
2662
  // K3 is Kimi's 2.8T-parameter flagship for long-horizon coding, knowledge
2651
2663
  // work, and deep reasoning. Its effort ladder is server-declared as
2652
- // low/high/max on both the public API (default max) and the Kimi For Coding
2653
- // OAuth endpoint (default high); thinking can also be fully disabled.
2664
+ // low/high/max on both the public API (default max) and the Kimi Code plan
2665
+ // endpoint (default high); thinking can also be fully disabled.
2654
2666
  {
2655
2667
  id: "kimi-k3",
2656
2668
  name: "Kimi K3",
@@ -2664,13 +2676,14 @@ var MODELS = [
2664
2676
  supportsVideo: true,
2665
2677
  maxVideoBytes: 100 * 1024 * 1024,
2666
2678
  costTier: "high",
2667
- maxThinkingLevel: "max"
2679
+ maxThinkingLevel: "max",
2680
+ authStorageKeys: KIMI_AUTH_KEYS
2668
2681
  },
2669
- // K2.8 Preview (2026-09-11) is served only on the Kimi For Coding OAuth
2670
- // endpoint, under its rolling `kimi-for-coding` id (live /models, 2026-09-28:
2682
+ // K2.8 Preview (2026-09-11) is served only on the Kimi Code plan endpoint,
2683
+ // under its rolling `kimi-for-coding` id (live /models, 2026-09-28:
2671
2684
  // display_name "K2.8 Preview", 1M context, image + video input, efforts
2672
- // low/high/max default max). The public API-key endpoint does not serve it,
2673
- // so it resolves from the Kimi sign-in credential only.
2685
+ // low/high/max default max). The public Moonshot API does not serve it, so
2686
+ // it resolves from a Kimi Code plan key only.
2674
2687
  {
2675
2688
  id: "kimi-for-coding",
2676
2689
  name: "Kimi K2.8 Preview",
@@ -2683,7 +2696,7 @@ var MODELS = [
2683
2696
  maxVideoBytes: 100 * 1024 * 1024,
2684
2697
  costTier: "medium",
2685
2698
  maxThinkingLevel: "max",
2686
- authStorageKeys: [MOONSHOT_OAUTH_KEY]
2699
+ authStorageKeys: [KIMI_CODE_KEY]
2687
2700
  },
2688
2701
  // K2.7 Code is requested by its pinned id (not the `kimi-for-coding` alias
2689
2702
  // that moved to K2.8), so it stays the real K2.7 on both endpoints.
@@ -2698,7 +2711,8 @@ var MODELS = [
2698
2711
  supportsVideo: true,
2699
2712
  maxVideoBytes: 100 * 1024 * 1024,
2700
2713
  costTier: "medium",
2701
- maxThinkingLevel: "high"
2714
+ maxThinkingLevel: "high",
2715
+ authStorageKeys: KIMI_AUTH_KEYS
2702
2716
  },
2703
2717
  // K2.7 Code HighSpeed — the same model as K2.7 Code served at ~180 tok/s
2704
2718
  // (up to ~260 on short contexts), 256K context (platform.kimi.ai model list,
@@ -2717,7 +2731,8 @@ var MODELS = [
2717
2731
  supportsVideo: true,
2718
2732
  maxVideoBytes: 100 * 1024 * 1024,
2719
2733
  costTier: "medium",
2720
- maxThinkingLevel: "high"
2734
+ maxThinkingLevel: "high",
2735
+ authStorageKeys: KIMI_AUTH_KEYS
2721
2736
  },
2722
2737
  // ── Z.AI (GLM) ─────────────────────────────────────────
2723
2738
  // Two GLM entries, both live on the coding endpoint (verified against its
@@ -3643,101 +3658,238 @@ function findProbedModel(probes, modelId) {
3643
3658
  return void 0;
3644
3659
  }
3645
3660
 
3661
+ // src/claude-code/usage.ts
3662
+ var import_promises7 = __toESM(require("fs/promises"), 1);
3663
+ var import_node_path5 = __toESM(require("path"), 1);
3664
+ var USAGE_FILE = "usage.json";
3665
+ var WINDOWS = {
3666
+ five_hour: { kind: "current", label: "5-hour" },
3667
+ seven_day: { kind: "weekly", label: "Weekly" }
3668
+ };
3669
+ function finite(value) {
3670
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
3671
+ }
3672
+ function toWindow(key, raw) {
3673
+ const shape = WINDOWS[key];
3674
+ const fraction = finite(raw?.utilization);
3675
+ if (!shape || fraction === void 0) return null;
3676
+ const resetsAt = finite(raw?.resetsAt);
3677
+ return {
3678
+ ...shape,
3679
+ usedPercent: Math.min(100, Math.max(0, fraction * 100)),
3680
+ ...resetsAt !== void 0 ? { resetsAt: resetsAt * 1e3 } : {}
3681
+ };
3682
+ }
3683
+ function mergeClaudeCodeRateLimit(info, previous, now = Date.now()) {
3684
+ const reported = info.unifiedWindows ? Object.entries(info.unifiedWindows).map(([key, raw]) => toWindow(key, raw)) : info.rateLimitType ? [toWindow(info.rateLimitType, info)] : [];
3685
+ const byKind = new Map(previous?.windows.map((w) => [w.kind, w]) ?? []);
3686
+ for (const window of reported) if (window) byKind.set(window.kind, window);
3687
+ const windows = [...byKind.values()].sort(
3688
+ (a, b) => a.kind === b.kind ? 0 : a.kind === "current" ? -1 : 1
3689
+ );
3690
+ return { provider: "anthropic", displayName: "Anthropic", windows, fetchedAt: now };
3691
+ }
3692
+ function usagePath(opts) {
3693
+ return import_node_path5.default.join(claudeCodeHome(opts), USAGE_FILE);
3694
+ }
3695
+ async function readClaudeCodeUsage(opts = {}) {
3696
+ let parsed;
3697
+ try {
3698
+ parsed = JSON.parse(await import_promises7.default.readFile(usagePath(opts), "utf8"));
3699
+ } catch {
3700
+ return null;
3701
+ }
3702
+ if (!Array.isArray(parsed?.windows) || typeof parsed.fetchedAt !== "number") return null;
3703
+ const now = opts.now ?? Date.now();
3704
+ const windows = parsed.windows.filter(
3705
+ (w) => (w.kind === "current" || w.kind === "weekly") && typeof w.usedPercent === "number" && (w.resetsAt === void 0 || w.resetsAt > now)
3706
+ );
3707
+ return { provider: "anthropic", displayName: "Anthropic", windows, fetchedAt: parsed.fetchedAt };
3708
+ }
3709
+ var writeQueue = Promise.resolve();
3710
+ function recordClaudeCodeRateLimit(info, opts = {}) {
3711
+ const write = async () => {
3712
+ const now = opts.now ?? Date.now();
3713
+ const next = mergeClaudeCodeRateLimit(info, await readClaudeCodeUsage({ ...opts, now }), now);
3714
+ if (next.windows.length === 0) return;
3715
+ const file = usagePath(opts);
3716
+ await import_promises7.default.mkdir(import_node_path5.default.dirname(file), { recursive: true });
3717
+ await import_promises7.default.writeFile(file, JSON.stringify(next), "utf8");
3718
+ };
3719
+ writeQueue = writeQueue.then(write).catch(() => {
3720
+ });
3721
+ return writeQueue;
3722
+ }
3723
+ async function clearClaudeCodeUsage(opts = {}) {
3724
+ await writeQueue;
3725
+ await import_promises7.default.rm(usagePath(opts), { force: true }).catch(() => {
3726
+ });
3727
+ }
3728
+
3729
+ // src/claude-code/sign-in.ts
3730
+ async function signInWithClaudeCode(auth, callbacks, opts = {}) {
3731
+ if (!opts.binary && !resolveClaudeCodeBinary(opts)) {
3732
+ callbacks.onStatus("Downloading Claude Code (one time)\u2026");
3733
+ await ensureClaudeCodeBinary({
3734
+ ...opts,
3735
+ onProgress: (progress) => {
3736
+ callbacks.onStatus(formatClaudeCodeInstallProgress(progress));
3737
+ opts.onProgress?.(progress);
3738
+ }
3739
+ });
3740
+ }
3741
+ let status = await claudeCodeAuthStatus(opts);
3742
+ if (!status.loggedIn) {
3743
+ callbacks.onStatus("Opening Claude sign-in\u2026");
3744
+ await loginClaudeCode(callbacks, opts);
3745
+ status = await claudeCodeAuthStatus(opts);
3746
+ }
3747
+ if (!status.loggedIn) throw new Error("Claude sign-in didn't finish. Try again.");
3748
+ await auth.setClaudeCodeLogin();
3749
+ return status;
3750
+ }
3751
+ async function signOutOfClaudeCode(auth, opts = {}) {
3752
+ await auth.clearClaudeCodeLogin();
3753
+ await clearClaudeCodeUsage(opts);
3754
+ try {
3755
+ await logoutClaudeCode(opts);
3756
+ } catch (err) {
3757
+ log(
3758
+ "WARN",
3759
+ "auth",
3760
+ `claude auth logout failed: ${err instanceof Error ? err.message : String(err)}`
3761
+ );
3762
+ }
3763
+ }
3764
+
3646
3765
  // src/auth-providers.ts
3766
+ function apiKeyDetails(label, billing, when = "") {
3767
+ return { apikey: { method: "apikey", label, billing, when } };
3768
+ }
3647
3769
  var ALL_AUTH_PROVIDERS = [
3648
3770
  {
3649
3771
  value: "anthropic",
3650
3772
  label: "Anthropic",
3651
3773
  description: "Claude Fable 5.1, Opus 5.5, Opus 5.5 Fast, Sonnet 5.5, Haiku 5.5",
3652
- methods: ["oauth"]
3653
- },
3654
- {
3655
- value: "openai",
3656
- label: "OpenAI",
3657
- description: "GPT-6 Astra, GPT-6 Astra Fast, GPT-6.1 Sol, GPT-6.1 Sol Fast, GPT-6 Luna, GPT-6 Luna Fast",
3658
- methods: ["oauth"]
3659
- },
3660
- {
3661
- value: "gemini",
3662
- label: "Gemini",
3663
- description: "Gemini 3.8 Flash, 3.5 Flash Lite, 3.1 Flash Lite, 3.5 Flash, 3.1 Pro (Preview)",
3664
3774
  methods: ["oauth", "apikey"],
3665
- apiKeyLabel: "Gemini",
3666
- apiKeyBaseUrl: GEMINI_API_KEY_BASE_URL,
3667
- // Stored under its own key so it never overwrites the `gemini` OAuth login.
3668
- apiKeyVariants: [
3669
- { key: GEMINI_API_KEY, label: "Google AI Studio", baseUrl: GEMINI_API_KEY_BASE_URL }
3670
- ],
3775
+ apiKeyLabel: "Anthropic",
3671
3776
  methodDetails: {
3672
3777
  oauth: {
3673
3778
  method: "oauth",
3674
- label: "Sign in with Google",
3675
- billing: "Gemini Code Assist Standard or Enterprise licence.",
3676
- when: "Personal Google accounts are no longer served here (Google ended it on June 18, 2026)."
3779
+ label: "Sign in with Claude",
3780
+ billing: "Uses your Claude Pro or Max plan.",
3781
+ when: "Runs through Anthropic's Claude Code, which GG Coder downloads once (about 140 MB) the first time you pick this.",
3782
+ requires: "A Claude Pro or Max subscription."
3677
3783
  },
3678
3784
  apikey: {
3679
3785
  method: "apikey",
3680
- label: "Gemini API key",
3681
- billing: "Pay-per-token from a Google AI Studio key.",
3682
- when: "Used first whenever it is set."
3786
+ label: "Anthropic API key",
3787
+ billing: "Pay-per-token from an Anthropic Console key.",
3788
+ when: "No subscription needed. Used only while you are not signed in with Claude."
3683
3789
  }
3684
3790
  }
3685
3791
  },
3686
3792
  {
3687
- value: "xai",
3688
- label: "xAI (Grok)",
3689
- description: "Grok 4.7 \xB7 OAuth or API key",
3793
+ value: "openai",
3794
+ label: "OpenAI",
3795
+ description: "GPT-6 Astra, GPT-6 Astra Fast, GPT-6.1 Sol, GPT-6.1 Sol Fast, GPT-6.1 Terra, GPT-6.1 Terra Fast, GPT-6 Luna, GPT-6 Luna Fast",
3690
3796
  methods: ["oauth", "apikey"],
3691
- apiKeyLabel: "xAI",
3797
+ apiKeyLabel: "OpenAI",
3692
3798
  methodDetails: {
3693
3799
  oauth: {
3694
3800
  method: "oauth",
3695
- label: "Sign in with Grok",
3696
- billing: "Included with SuperGrok or X Premium.",
3697
- when: ""
3801
+ label: "Continue with ChatGPT",
3802
+ billing: "Uses your ChatGPT plan.",
3803
+ when: "Signs in through OpenAI. Manage what GG Coder may use at chatgpt.com/settings/usage.",
3804
+ requires: "A ChatGPT plan."
3698
3805
  },
3699
3806
  apikey: {
3700
3807
  method: "apikey",
3701
- label: "xAI API key",
3702
- billing: "Pay-per-token on console.x.ai credits.",
3703
- when: ""
3808
+ label: "OpenAI API key",
3809
+ billing: "Pay-per-token from an OpenAI Platform key.",
3810
+ when: "No subscription needed. Covers you while your ChatGPT plan's usage is out."
3704
3811
  }
3705
3812
  }
3706
3813
  },
3814
+ {
3815
+ value: "gemini",
3816
+ label: "Gemini",
3817
+ description: "Gemini 3.8 Flash, 3.5 Flash Lite, 3.1 Flash Lite, 3.5 Flash, 3.1 Pro (Preview)",
3818
+ methods: ["apikey"],
3819
+ apiKeyLabel: "Gemini",
3820
+ apiKeyBaseUrl: GEMINI_API_KEY_BASE_URL,
3821
+ // Stored under its own key: `gemini` held the retired Google sign-in.
3822
+ apiKeyVariants: [
3823
+ {
3824
+ key: GEMINI_API_KEY,
3825
+ label: "Google AI Studio",
3826
+ kind: "payg",
3827
+ billing: "Pay-per-token from a Google AI Studio key.",
3828
+ baseUrl: GEMINI_API_KEY_BASE_URL
3829
+ }
3830
+ ],
3831
+ methodDetails: apiKeyDetails("Gemini API key", "Pay-per-token from a Google AI Studio key.")
3832
+ },
3833
+ {
3834
+ value: "xai",
3835
+ label: "xAI (Grok)",
3836
+ description: "Grok 4.7",
3837
+ methods: ["apikey"],
3838
+ apiKeyLabel: "xAI",
3839
+ methodDetails: apiKeyDetails(
3840
+ "xAI API key",
3841
+ "Pay-per-token from console.x.ai credits.",
3842
+ "SuperGrok and X Premium usage is only offered in apps xAI partners with."
3843
+ )
3844
+ },
3707
3845
  {
3708
3846
  value: "moonshot",
3709
- label: "Moonshot",
3710
- description: "Kimi K3, K2.8 Preview (Kimi sign-in), K2.7 Code, K2.7 Code HighSpeed \xB7 OAuth or API key",
3711
- methods: ["oauth", "apikey"],
3712
- apiKeyLabel: "Moonshot",
3713
- methodDetails: {
3714
- oauth: {
3715
- method: "oauth",
3716
- label: "Sign in with Kimi",
3717
- billing: "Included with a Kimi For Coding plan.",
3718
- when: ""
3847
+ label: "Kimi (Moonshot)",
3848
+ description: "Kimi K3, K2.8 Preview (Kimi Code plan), K2.7 Code, K2.7 Code HighSpeed",
3849
+ methods: ["apikey"],
3850
+ apiKeyLabel: "Kimi",
3851
+ apiKeyVariants: [
3852
+ {
3853
+ key: KIMI_CODE_KEY,
3854
+ label: "Kimi Code plan",
3855
+ kind: "plan",
3856
+ billing: "Uses your Kimi Code plan. Create the key in the Kimi Code Console."
3719
3857
  },
3720
- apikey: {
3721
- method: "apikey",
3722
- label: "Moonshot API key",
3723
- billing: "Pay-per-token on Moonshot credits.",
3724
- when: ""
3858
+ {
3859
+ key: "moonshot",
3860
+ label: "Moonshot API",
3861
+ kind: "payg",
3862
+ billing: "Pay-per-token from platform.moonshot.ai."
3725
3863
  }
3726
- }
3864
+ ],
3865
+ methodDetails: apiKeyDetails(
3866
+ "Kimi API key",
3867
+ "A Kimi Code plan key uses your plan; a Moonshot key is pay-per-token."
3868
+ )
3727
3869
  },
3728
3870
  {
3729
3871
  value: "glm",
3730
3872
  label: "Z.AI (GLM)",
3731
3873
  description: "GLM-5.3, GLM-5.3-Flash",
3732
3874
  methods: ["apikey"],
3733
- apiKeyLabel: "Z.AI"
3875
+ apiKeyLabel: "Z.AI",
3876
+ methodDetails: apiKeyDetails(
3877
+ "Z.AI API key",
3878
+ "Pay-per-token from your Z.AI account balance.",
3879
+ "GLM Coding Plan quota can't be used here: Z.AI limits the plan to the tools it lists."
3880
+ )
3734
3881
  },
3735
3882
  {
3736
3883
  value: "minimax",
3737
3884
  label: "MiniMax",
3738
3885
  description: "MiniMax M3.1 Flash, M3",
3739
3886
  methods: ["apikey"],
3740
- apiKeyLabel: "MiniMax"
3887
+ apiKeyLabel: "MiniMax",
3888
+ methodDetails: apiKeyDetails(
3889
+ "MiniMax API key",
3890
+ "An M Plan key uses your plan; a pay-as-you-go key is pay-per-token.",
3891
+ "The key you paste decides which one is billed."
3892
+ )
3741
3893
  },
3742
3894
  {
3743
3895
  value: "xiaomi",
@@ -3750,44 +3902,66 @@ var ALL_AUTH_PROVIDERS = [
3750
3902
  {
3751
3903
  key: "xiaomi",
3752
3904
  label: "Token Plan",
3905
+ kind: "plan",
3906
+ billing: "Uses your MiMo Token Plan.",
3753
3907
  baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1"
3754
3908
  },
3755
3909
  {
3756
3910
  key: XIAOMI_CREDITS_KEY,
3757
- label: "API Credits (required for UltraSpeed)",
3911
+ label: "API Credits",
3912
+ kind: "payg",
3913
+ billing: "Pay-per-token from MiMo API Credits. Required for UltraSpeed.",
3758
3914
  baseUrl: "https://api.xiaomimimo.com/v1"
3759
3915
  }
3760
- ]
3916
+ ],
3917
+ methodDetails: apiKeyDetails(
3918
+ "Xiaomi MiMo API key",
3919
+ "A Token Plan key uses your plan; an API Credits key is pay-per-token."
3920
+ )
3761
3921
  },
3762
3922
  {
3763
3923
  value: "deepseek",
3764
3924
  label: "DeepSeek",
3765
3925
  description: "DeepSeek V4 Pro, V4.1 Flash",
3766
3926
  methods: ["apikey"],
3767
- apiKeyLabel: "DeepSeek"
3927
+ apiKeyLabel: "DeepSeek",
3928
+ methodDetails: apiKeyDetails("DeepSeek API key", "Pay-per-token from platform.deepseek.com.")
3768
3929
  },
3769
3930
  {
3770
3931
  value: "sakana",
3771
3932
  label: "Sakana (Fugu)",
3772
3933
  description: "Fugu, Fugu Max, Fugu Ultra",
3773
3934
  methods: ["apikey"],
3774
- apiKeyLabel: "Sakana"
3935
+ apiKeyLabel: "Sakana",
3936
+ methodDetails: apiKeyDetails("Sakana API key", "Pay-per-token from your Sakana API account.")
3775
3937
  },
3776
3938
  {
3777
3939
  value: "openrouter",
3778
3940
  label: "OpenRouter",
3779
3941
  description: "Qwen3.8 Max \xB7 multi-provider gateway",
3780
3942
  methods: ["apikey"],
3781
- apiKeyLabel: "OpenRouter"
3943
+ apiKeyLabel: "OpenRouter",
3944
+ methodDetails: apiKeyDetails("OpenRouter API key", "Pay-per-token from OpenRouter credits.")
3782
3945
  },
3783
3946
  {
3784
3947
  value: "huggingface",
3785
3948
  label: "Hugging Face",
3786
3949
  description: "Kimi K2.7 Code, DeepSeek V4.1 Flash, GPT-OSS 120B",
3787
3950
  methods: ["apikey"],
3788
- apiKeyLabel: "Hugging Face"
3951
+ apiKeyLabel: "Hugging Face",
3952
+ methodDetails: apiKeyDetails(
3953
+ "Hugging Face token",
3954
+ "Pay-per-token from Hugging Face Inference Providers credits."
3955
+ )
3789
3956
  }
3790
3957
  ];
3958
+ function requiredKeyLabel(provider, storageKeys) {
3959
+ const variants = ALL_AUTH_PROVIDERS.find((p) => p.value === provider)?.apiKeyVariants ?? [];
3960
+ const needed = variants.filter((v) => storageKeys.includes(v.key));
3961
+ if (needed.length === 0 || needed.length === variants.length) return void 0;
3962
+ const names = needed.map((v) => v.label).join(" or ");
3963
+ return `Needs ${/^[aeiou]/i.test(names) ? "an" : "a"} ${names} key`;
3964
+ }
3791
3965
  var AUTH_PROVIDERS = ALL_AUTH_PROVIDERS.filter(
3792
3966
  (p) => !isHiddenProvider(p.value)
3793
3967
  );
@@ -3814,15 +3988,24 @@ function describeAuthMethods(provider) {
3814
3988
  });
3815
3989
  }
3816
3990
  function authPriorityNote(provider) {
3817
- if (provider === "gemini") {
3818
- return "Uses the Gemini API key first when one is set; otherwise the Google sign-in.";
3991
+ if (provider === "anthropic") {
3992
+ return "Uses Sign in with Claude while you are signed in; otherwise the Anthropic API key.";
3819
3993
  }
3820
3994
  const dual = dualAuthProvider(provider);
3821
- if (!dual) return void 0;
3822
- return `Uses ${dual.oauthLabel} first; the ${dual.apiKeyLabel} takes over when it runs out, then switches back.`;
3995
+ if (dual) {
3996
+ return `Uses your ${dual.oauthLabel} first; the ${dual.apiKeyLabel} takes over while its usage is out, then switches back.`;
3997
+ }
3998
+ const variants = getAuthProvider(provider)?.apiKeyVariants;
3999
+ const plan = variants?.find((v) => v.kind === "plan");
4000
+ const payg = variants?.find((v) => v.kind === "payg");
4001
+ if (plan && payg) {
4002
+ return `Uses the ${plan.label} key first when both are set; the ${payg.label} key covers models the plan doesn't include.`;
4003
+ }
4004
+ return void 0;
3823
4005
  }
3824
4006
 
3825
4007
  // src/provider-usage.ts
4008
+ var import_gg_ai5 = require("@kenkaiiii/gg-ai");
3826
4009
  var SubscriptionUsageError = class extends Error {
3827
4010
  constructor(message, status, retryAfterMs2) {
3828
4011
  super(message);
@@ -3831,52 +4014,14 @@ var SubscriptionUsageError = class extends Error {
3831
4014
  this.name = "SubscriptionUsageError";
3832
4015
  }
3833
4016
  };
3834
- var ANTHROPIC_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
3835
- var CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
3836
4017
  function finiteNumber(value) {
3837
4018
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
3838
4019
  }
3839
- function clampPercent(value) {
3840
- const number = finiteNumber(value);
3841
- return number === void 0 ? void 0 : Math.min(100, Math.max(0, number));
3842
- }
3843
4020
  function isoTimestamp(value) {
3844
4021
  if (typeof value !== "string" || !value.trim()) return void 0;
3845
4022
  const timestamp = Date.parse(value);
3846
4023
  return Number.isFinite(timestamp) ? timestamp : void 0;
3847
4024
  }
3848
- function unixTimestamp(value) {
3849
- const seconds = finiteNumber(value);
3850
- return seconds === void 0 ? void 0 : seconds * 1e3;
3851
- }
3852
- function codexResetAt(window, now) {
3853
- const absolute = unixTimestamp(window.reset_at);
3854
- if (absolute !== void 0) return absolute;
3855
- const afterSeconds = finiteNumber(window.reset_after_seconds);
3856
- return afterSeconds === void 0 ? void 0 : now + afterSeconds * 1e3;
3857
- }
3858
- function currentWindowLabel(seconds, fallbackHours) {
3859
- const duration = finiteNumber(seconds);
3860
- const hours = Math.max(1, Math.round((duration ?? fallbackHours * 3600) / 3600));
3861
- return `${hours}-hour`;
3862
- }
3863
- function codexWindowKind(window, fallback) {
3864
- const duration = finiteNumber(window.limit_window_seconds);
3865
- if (duration === void 0) return fallback;
3866
- return duration >= 6 * 24 * 60 * 60 ? "weekly" : "current";
3867
- }
3868
- function normalizedCodexWindow(window, fallbackKind, now) {
3869
- if (!window) return null;
3870
- const usedPercent = clampPercent(window.used_percent);
3871
- if (usedPercent === void 0) return null;
3872
- const kind = codexWindowKind(window, fallbackKind);
3873
- return {
3874
- kind,
3875
- label: kind === "weekly" ? "Weekly" : currentWindowLabel(window.limit_window_seconds, 5),
3876
- usedPercent,
3877
- resetsAt: codexResetAt(window, now)
3878
- };
3879
- }
3880
4025
  function retryAfterMs(response, now) {
3881
4026
  const value = response.headers.get("retry-after")?.trim();
3882
4027
  if (!value) return void 0;
@@ -3899,70 +4044,6 @@ async function readUsageResponse(response, now) {
3899
4044
  throw new SubscriptionUsageError("Subscription usage response was not valid JSON");
3900
4045
  }
3901
4046
  }
3902
- async function fetchAnthropicUsage(credentials, fetchFn, signal, now) {
3903
- const response = await fetchFn(ANTHROPIC_USAGE_URL, {
3904
- method: "GET",
3905
- signal,
3906
- headers: {
3907
- Authorization: `Bearer ${credentials.accessToken}`,
3908
- Accept: "application/json",
3909
- "anthropic-version": "2023-06-01",
3910
- "anthropic-beta": "oauth-2025-04-20",
3911
- "User-Agent": "ggcoder"
3912
- }
3913
- });
3914
- const data = await readUsageResponse(response, now());
3915
- const windows = [];
3916
- const currentPercent = clampPercent(data.five_hour?.utilization);
3917
- if (currentPercent !== void 0) {
3918
- windows.push({
3919
- kind: "current",
3920
- label: "5-hour",
3921
- usedPercent: currentPercent,
3922
- resetsAt: isoTimestamp(data.five_hour?.resets_at)
3923
- });
3924
- }
3925
- const weeklyPercent = clampPercent(data.seven_day?.utilization);
3926
- if (weeklyPercent !== void 0) {
3927
- windows.push({
3928
- kind: "weekly",
3929
- label: "Weekly",
3930
- usedPercent: weeklyPercent,
3931
- resetsAt: isoTimestamp(data.seven_day?.resets_at)
3932
- });
3933
- }
3934
- return {
3935
- provider: "anthropic",
3936
- displayName: "Anthropic",
3937
- windows,
3938
- fetchedAt: now()
3939
- };
3940
- }
3941
- async function fetchCodexUsage(credentials, fetchFn, signal, now) {
3942
- const headers = {
3943
- Authorization: `Bearer ${credentials.accessToken}`,
3944
- Accept: "application/json",
3945
- originator: "ggcoder",
3946
- "User-Agent": "ggcoder"
3947
- };
3948
- if (credentials.accountId) headers["ChatGPT-Account-Id"] = credentials.accountId;
3949
- const response = await fetchFn(CODEX_USAGE_URL, { method: "GET", signal, headers });
3950
- const data = await readUsageResponse(response, now());
3951
- const windows = [
3952
- normalizedCodexWindow(data.rate_limit?.primary_window, "current", now()),
3953
- normalizedCodexWindow(data.rate_limit?.secondary_window, "weekly", now())
3954
- ].filter((window) => window !== null);
3955
- windows.sort((left, right) => {
3956
- if (left.kind === right.kind) return 0;
3957
- return left.kind === "current" ? -1 : 1;
3958
- });
3959
- return {
3960
- provider: "openai",
3961
- displayName: "Codex",
3962
- windows,
3963
- fetchedAt: now()
3964
- };
3965
- }
3966
4047
  function kimiNumber(value) {
3967
4048
  if (typeof value === "string" && value.trim()) {
3968
4049
  const parsed = Number(value);
@@ -3993,15 +4074,14 @@ function kimiWindowMinutes(window) {
3993
4074
  }
3994
4075
  }
3995
4076
  async function fetchKimiUsage(credentials, fetchFn, signal, now) {
3996
- const response = await fetchFn(`${kimiCodeBaseUrl()}/usages`, {
4077
+ const baseUrl = (credentials.baseUrl ?? KIMI_CODE_DEFAULT_BASE_URL).replace(/\/+$/, "");
4078
+ const response = await fetchFn(`${baseUrl}/usages`, {
3997
4079
  method: "GET",
3998
4080
  signal,
3999
4081
  headers: {
4000
4082
  Authorization: "Bearer " + credentials.accessToken,
4001
4083
  Accept: "application/json",
4002
- // The managed coding endpoint gates on the kimi-code-cli client identity,
4003
- // same as model requests.
4004
- ...kimiCodingHeaders()
4084
+ "User-Agent": import_gg_ai5.GG_CODER_USER_AGENT
4005
4085
  }
4006
4086
  });
4007
4087
  const data = await readUsageResponse(response, now());
@@ -4053,7 +4133,7 @@ async function fetchSubscriptionUsage(provider, credentials, options = {}) {
4053
4133
  const now = options.now ?? Date.now;
4054
4134
  const signal = AbortSignal.timeout(options.timeoutMs ?? 8e3);
4055
4135
  try {
4056
- return provider === "anthropic" ? await fetchAnthropicUsage(credentials, fetchFn, signal, now) : provider === "openai" ? await fetchCodexUsage(credentials, fetchFn, signal, now) : await fetchKimiUsage(credentials, fetchFn, signal, now);
4136
+ return await fetchKimiUsage(credentials, fetchFn, signal, now);
4057
4137
  } catch (error) {
4058
4138
  if (error instanceof SubscriptionUsageError) throw error;
4059
4139
  const message = error instanceof Error ? error.message : String(error);
@@ -4118,7 +4198,7 @@ var TelegramBot = class {
4118
4198
  } catch (err) {
4119
4199
  if (!this.running) break;
4120
4200
  console.error(`[telegram] Poll error: ${err instanceof Error ? err.message : err}`);
4121
- await sleep3(3e3);
4201
+ await sleep(3e3);
4122
4202
  }
4123
4203
  }
4124
4204
  }
@@ -4304,7 +4384,7 @@ function splitMessage(text) {
4304
4384
  }
4305
4385
  return chunks;
4306
4386
  }
4307
- function sleep3(ms) {
4387
+ function sleep(ms) {
4308
4388
  return new Promise((r) => setTimeout(r, ms));
4309
4389
  }
4310
4390
 
@@ -4390,8 +4470,8 @@ async function transcribeVoice(fileUrl) {
4390
4470
 
4391
4471
  // src/auto-update.ts
4392
4472
  var import_node_child_process2 = require("child_process");
4393
- var import_node_fs4 = __toESM(require("fs"), 1);
4394
- var import_node_path4 = __toESM(require("path"), 1);
4473
+ var import_node_fs5 = __toESM(require("fs"), 1);
4474
+ var import_node_path6 = __toESM(require("path"), 1);
4395
4475
  var CHECK_INTERVAL_MS = 60 * 60 * 1e3;
4396
4476
  var FETCH_TIMEOUT_MS = 1e4;
4397
4477
  function compareVersions(a, b) {
@@ -4424,7 +4504,7 @@ function createAutoUpdater(config) {
4424
4504
  }
4425
4505
  function readState() {
4426
4506
  try {
4427
- const raw = import_node_fs4.default.readFileSync(stateFilePath(), "utf-8");
4507
+ const raw = import_node_fs5.default.readFileSync(stateFilePath(), "utf-8");
4428
4508
  return JSON.parse(raw);
4429
4509
  } catch {
4430
4510
  return null;
@@ -4433,8 +4513,8 @@ function createAutoUpdater(config) {
4433
4513
  function writeState(state) {
4434
4514
  try {
4435
4515
  const filePath = stateFilePath();
4436
- import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(filePath), { recursive: true, mode: 448 });
4437
- import_node_fs4.default.writeFileSync(filePath, JSON.stringify(state));
4516
+ import_node_fs5.default.mkdirSync(import_node_path6.default.dirname(filePath), { recursive: true, mode: 448 });
4517
+ import_node_fs5.default.writeFileSync(filePath, JSON.stringify(state));
4438
4518
  } catch {
4439
4519
  }
4440
4520
  }
@@ -4560,34 +4640,55 @@ function createAutoUpdater(config) {
4560
4640
  0 && (module.exports = {
4561
4641
  AUTH_PROVIDERS,
4562
4642
  AuthStorage,
4643
+ CHATGPT_PLAN_BASE_URL,
4644
+ CHATGPT_PLAN_NOT_ENABLED_MESSAGE,
4645
+ CHATGPT_PLAN_SCOPE,
4646
+ CHATGPT_USAGE_URL,
4647
+ CLAUDE_CODE_AUTH_KEY,
4648
+ CLAUDE_CODE_AUTH_MARKER,
4649
+ CLAUDE_CODE_MANIFEST,
4650
+ ChatGPTPlanNotEnabledError,
4563
4651
  DEFAULT_LOCAL_ENDPOINTS,
4564
4652
  DEFAULT_MAX_VIDEO_BYTES,
4565
4653
  FALLBACK_CONTEXT_WINDOW,
4566
4654
  GEMINI_API_KEY,
4567
4655
  GEMINI_API_KEY_BASE_URL,
4568
4656
  HIDDEN_PROVIDERS,
4657
+ KIMI_CODE_DEFAULT_BASE_URL,
4658
+ KIMI_CODE_KEY,
4659
+ KIMI_CODE_OVERSEAS_BASE_URL,
4569
4660
  LOCAL_API_KEY_PLACEHOLDER,
4570
4661
  LOCAL_AUTH_KEY_PREFIX,
4571
4662
  MINIMAX_CN_BASE_URL,
4572
4663
  MINIMAX_GLOBAL_BASE_URL,
4573
4664
  MODELS,
4574
- MOONSHOT_OAUTH_KEY,
4575
4665
  NotLoggedInError,
4666
+ OPENAI_CHATGPT_KEY,
4576
4667
  OPENROUTER_DEFAULT_BASE_URL,
4577
4668
  PLAN_MODE_THINKING_CAP,
4669
+ RETIRED_SIGN_INS,
4578
4670
  SubscriptionUsageError,
4579
4671
  TelegramBot,
4580
- XAI_OAUTH_KEY,
4581
4672
  XIAOMI_CREDITS_KEY,
4582
4673
  XIAOMI_TOKEN_PLAN_BASE_URLS,
4674
+ apiKeyStorageKeys,
4583
4675
  authPriorityNote,
4584
4676
  clampThinkingForPlanMode,
4677
+ claudeCodeAuthStatus,
4678
+ claudeCodeBinaryPath,
4679
+ claudeCodeCallbackUrl,
4680
+ claudeCodeConfigDir,
4681
+ claudeCodeEnv,
4682
+ claudeCodeHome,
4683
+ claudeCodePlatformKey,
4684
+ clearClaudeCodeUsage,
4585
4685
  clearLocalDiscoveryCache,
4586
4686
  clearRuntimeModels,
4587
4687
  closeLogger,
4588
4688
  createAutoUpdater,
4589
4689
  decodeOggOpus,
4590
4690
  describeAuthMethods,
4691
+ detectKimiCodeBaseUrl,
4591
4692
  detectMiniMaxBaseUrl,
4592
4693
  detectXiaomiTokenPlanBaseUrl,
4593
4694
  discoverLocalModels,
@@ -4595,8 +4696,11 @@ function createAutoUpdater(config) {
4595
4696
  dualAuthProvider,
4596
4697
  dualAuthProviderByOAuthKey,
4597
4698
  endpointRoot,
4699
+ ensureClaudeCodeBinary,
4700
+ fetchChatGPTPlanModels,
4598
4701
  fetchSubscriptionUsage,
4599
4702
  findProbedModel,
4703
+ formatClaudeCodeInstallProgress,
4600
4704
  formatLocalModelId,
4601
4705
  generatePKCE,
4602
4706
  getAllModels,
@@ -4604,8 +4708,7 @@ function createAutoUpdater(config) {
4604
4708
  getAuthProvider,
4605
4709
  getAuthStorageKey,
4606
4710
  getAuthStorageKeys,
4607
- getClaudeCliUserAgent,
4608
- getClaudeCodeVersion,
4711
+ getChatGPTHostId,
4609
4712
  getContextWindow,
4610
4713
  getDefaultModel,
4611
4714
  getDefaultThinkingLevel,
@@ -4621,48 +4724,49 @@ function createAutoUpdater(config) {
4621
4724
  getSupportedThinkingLevels,
4622
4725
  getToolResultCharLimit,
4623
4726
  getVideoByteLimit,
4624
- grokCliBaseUrl,
4625
- grokCliHeaders,
4626
- isGrokCliEndpoint,
4727
+ isClaudeCodeAuthMarker,
4627
4728
  isHiddenProvider,
4628
4729
  isKimiCodingEndpoint,
4730
+ isLegacyClaudeOAuth,
4629
4731
  isLocalModelId,
4630
4732
  isLoggerOpen,
4631
4733
  isModelLoaded,
4632
4734
  isPaidModel,
4633
4735
  isThinkingLevelSupported,
4634
- kimiCodeBaseUrl,
4635
- kimiCodingHeaders,
4636
4736
  localAuthStorageKey,
4637
4737
  localModelShortName,
4638
4738
  log,
4639
- loginAnthropic,
4640
- loginGemini,
4641
- loginKimi,
4642
- loginOpenAI,
4643
- loginXai,
4739
+ loginChatGPT,
4740
+ loginClaudeCode,
4741
+ logoutClaudeCode,
4742
+ mergeClaudeCodeRateLimit,
4644
4743
  oauthStorageKey,
4645
4744
  ollamaServedContext,
4646
4745
  openLog,
4746
+ parseCallbackInput,
4647
4747
  parseLocalModelId,
4648
4748
  parseOpenRouterCatalog,
4649
- pendingClaudeCodeVersionRefresh,
4650
4749
  probeEndpoint,
4651
4750
  providerStorageKeys,
4751
+ readClaudeCodeUsage,
4652
4752
  readStoredBaseUrlSync,
4653
- refreshAnthropicToken,
4654
- refreshGeminiToken,
4655
- refreshKimiToken,
4656
- refreshOpenAIToken,
4657
- refreshXaiToken,
4753
+ recordClaudeCodeRateLimit,
4754
+ refreshChatGPTToken,
4658
4755
  registerLogCleanup,
4659
4756
  registerRuntimeModels,
4757
+ requiredKeyLabel,
4660
4758
  resample,
4759
+ resolveClaudeCodeBinary,
4760
+ revokeChatGPTSignIn,
4661
4761
  setProgressCallback,
4662
4762
  setProviderCatalog,
4763
+ signInWithClaudeCode,
4764
+ signOutOfClaudeCode,
4765
+ storageKeyProvider,
4663
4766
  toModelInfo,
4664
4767
  transcribeVoice,
4665
4768
  usesOpenAICodexTransport,
4769
+ verifyIdToken,
4666
4770
  withFileLock,
4667
4771
  withoutPaidTier
4668
4772
  });