claudish 6.5.2 → 6.5.3

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.
Files changed (2) hide show
  1. package/dist/index.js +594 -579
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -20811,6 +20811,9 @@ class TokenTracker {
20811
20811
  setQuotaRemaining(fraction) {
20812
20812
  this.quotaRemaining = fraction;
20813
20813
  }
20814
+ rewrite() {
20815
+ this.writeFile(this.sessionInputTokens, this.sessionOutputTokens);
20816
+ }
20814
20817
  update(inputTokens, outputTokens) {
20815
20818
  this.sessionInputTokens = inputTokens;
20816
20819
  this.sessionOutputTokens += outputTokens;
@@ -23896,512 +23899,6 @@ var init_stats = __esm(() => {
23896
23899
  });
23897
23900
  });
23898
23901
 
23899
- // src/auth/gemini-oauth.ts
23900
- var exports_gemini_oauth = {};
23901
- __export(exports_gemini_oauth, {
23902
- setupGeminiUser: () => setupGeminiUser,
23903
- retrieveUserQuota: () => retrieveUserQuota,
23904
- getValidAccessToken: () => getValidAccessToken,
23905
- getGeminiTierFullName: () => getGeminiTierFullName,
23906
- getGeminiTierDisplayName: () => getGeminiTierDisplayName,
23907
- getGeminiOAuth: () => getGeminiOAuth,
23908
- GeminiOAuth: () => GeminiOAuth
23909
- });
23910
- import { createServer } from "http";
23911
- import { randomBytes as randomBytes2, createHash as createHash2 } from "crypto";
23912
- import { readFileSync as readFileSync5, existsSync as existsSync7, unlinkSync as unlinkSync3, openSync, writeSync, closeSync } from "fs";
23913
- import { homedir as homedir8 } from "os";
23914
- import { join as join9 } from "path";
23915
- import { exec } from "child_process";
23916
- import { promisify } from "util";
23917
-
23918
- class GeminiOAuth {
23919
- static instance = null;
23920
- credentials = null;
23921
- refreshPromise = null;
23922
- tokenRefreshMargin = 5 * 60 * 1000;
23923
- oauthState = null;
23924
- static getInstance() {
23925
- if (!GeminiOAuth.instance) {
23926
- GeminiOAuth.instance = new GeminiOAuth;
23927
- }
23928
- return GeminiOAuth.instance;
23929
- }
23930
- constructor() {
23931
- this.credentials = this.loadCredentials();
23932
- }
23933
- hasCredentials() {
23934
- return this.credentials !== null && !!this.credentials.refresh_token;
23935
- }
23936
- getCredentialsPath() {
23937
- const claudishDir = join9(homedir8(), ".claudish");
23938
- return join9(claudishDir, "gemini-oauth.json");
23939
- }
23940
- async login() {
23941
- log("[GeminiOAuth] Starting OAuth login flow");
23942
- const codeVerifier = this.generateCodeVerifier();
23943
- const codeChallenge = await this.generateCodeChallenge(codeVerifier);
23944
- this.oauthState = randomBytes2(32).toString("base64url");
23945
- const { authCode, redirectUri } = await this.startCallbackServer(codeChallenge, this.oauthState);
23946
- const tokens = await this.exchangeCodeForTokens(authCode, codeVerifier, redirectUri);
23947
- const credentials = {
23948
- access_token: tokens.access_token,
23949
- refresh_token: tokens.refresh_token,
23950
- expires_at: Date.now() + tokens.expires_in * 1000
23951
- };
23952
- this.saveCredentials(credentials);
23953
- this.credentials = credentials;
23954
- this.oauthState = null;
23955
- log("[GeminiOAuth] Login successful");
23956
- }
23957
- async logout() {
23958
- const credPath = this.getCredentialsPath();
23959
- if (existsSync7(credPath)) {
23960
- unlinkSync3(credPath);
23961
- log("[GeminiOAuth] Credentials deleted");
23962
- }
23963
- this.credentials = null;
23964
- }
23965
- async getAccessToken() {
23966
- if (this.refreshPromise) {
23967
- log("[GeminiOAuth] Waiting for in-progress refresh");
23968
- return this.refreshPromise;
23969
- }
23970
- if (!this.credentials) {
23971
- throw new Error("No Gemini OAuth credentials found. Please run `claudish login gemini` first.");
23972
- }
23973
- if (this.isTokenValid()) {
23974
- return this.credentials.access_token;
23975
- }
23976
- this.refreshPromise = this.doRefreshToken();
23977
- try {
23978
- const token = await this.refreshPromise;
23979
- return token;
23980
- } finally {
23981
- this.refreshPromise = null;
23982
- }
23983
- }
23984
- async refreshToken() {
23985
- if (!this.credentials) {
23986
- throw new Error("No Gemini OAuth credentials found. Please run `claudish login gemini` first.");
23987
- }
23988
- await this.doRefreshToken();
23989
- }
23990
- isTokenValid() {
23991
- if (!this.credentials)
23992
- return false;
23993
- return Date.now() < this.credentials.expires_at - this.tokenRefreshMargin;
23994
- }
23995
- async doRefreshToken() {
23996
- if (!this.credentials) {
23997
- throw new Error("No Gemini OAuth credentials found. Please run `claudish login gemini` first.");
23998
- }
23999
- log("[GeminiOAuth] Refreshing access token");
24000
- try {
24001
- const response = await fetch(OAUTH_CONFIG.tokenUrl, {
24002
- method: "POST",
24003
- headers: {
24004
- "Content-Type": "application/x-www-form-urlencoded"
24005
- },
24006
- body: new URLSearchParams({
24007
- grant_type: "refresh_token",
24008
- refresh_token: this.credentials.refresh_token,
24009
- client_id: OAUTH_CONFIG.clientId,
24010
- client_secret: OAUTH_CONFIG.clientSecret
24011
- })
24012
- });
24013
- if (!response.ok) {
24014
- const errorText = await response.text();
24015
- throw new Error(`Token refresh failed: ${response.status} - ${errorText}`);
24016
- }
24017
- const tokens = await response.json();
24018
- const updatedCredentials = {
24019
- access_token: tokens.access_token,
24020
- refresh_token: tokens.refresh_token || this.credentials.refresh_token,
24021
- expires_at: Date.now() + tokens.expires_in * 1000
24022
- };
24023
- this.saveCredentials(updatedCredentials);
24024
- this.credentials = updatedCredentials;
24025
- log(`[GeminiOAuth] Token refreshed, valid until ${new Date(updatedCredentials.expires_at).toISOString()}`);
24026
- return updatedCredentials.access_token;
24027
- } catch (e) {
24028
- log(`[GeminiOAuth] Refresh failed: ${e.message}`);
24029
- throw new Error(`OAuth credentials invalid. Please run \`claudish login gemini\` again.
24030
-
24031
- Details: ${e.message}`);
24032
- }
24033
- }
24034
- loadCredentials() {
24035
- const credPath = this.getCredentialsPath();
24036
- if (!existsSync7(credPath)) {
24037
- return null;
24038
- }
24039
- try {
24040
- const data = readFileSync5(credPath, "utf-8");
24041
- const credentials = JSON.parse(data);
24042
- if (!credentials.access_token || !credentials.refresh_token || !credentials.expires_at) {
24043
- log("[GeminiOAuth] Invalid credentials file structure");
24044
- return null;
24045
- }
24046
- log("[GeminiOAuth] Loaded credentials from file");
24047
- return credentials;
24048
- } catch (e) {
24049
- log(`[GeminiOAuth] Failed to load credentials: ${e.message}`);
24050
- return null;
24051
- }
24052
- }
24053
- saveCredentials(credentials) {
24054
- const credPath = this.getCredentialsPath();
24055
- const claudishDir = join9(homedir8(), ".claudish");
24056
- if (!existsSync7(claudishDir)) {
24057
- const { mkdirSync: mkdirSync8 } = __require("fs");
24058
- mkdirSync8(claudishDir, { recursive: true });
24059
- }
24060
- const fd = openSync(credPath, "w", 384);
24061
- try {
24062
- const data = JSON.stringify(credentials, null, 2);
24063
- writeSync(fd, data, 0, "utf-8");
24064
- } finally {
24065
- closeSync(fd);
24066
- }
24067
- log(`[GeminiOAuth] Credentials saved to ${credPath}`);
24068
- }
24069
- generateCodeVerifier() {
24070
- return randomBytes2(64).toString("base64url");
24071
- }
24072
- async generateCodeChallenge(verifier) {
24073
- const hash = createHash2("sha256").update(verifier).digest("base64url");
24074
- return hash;
24075
- }
24076
- buildAuthUrl(codeChallenge, state, redirectUri) {
24077
- const params = new URLSearchParams({
24078
- client_id: OAUTH_CONFIG.clientId,
24079
- redirect_uri: redirectUri,
24080
- response_type: "code",
24081
- scope: OAUTH_CONFIG.scopes.join(" "),
24082
- code_challenge: codeChallenge,
24083
- code_challenge_method: "S256",
24084
- access_type: "offline",
24085
- prompt: "consent",
24086
- state
24087
- });
24088
- return `${OAUTH_CONFIG.authUrl}?${params.toString()}`;
24089
- }
24090
- async startCallbackServer(codeChallenge, state) {
24091
- return new Promise((resolve2, reject) => {
24092
- let redirectUri = "";
24093
- const server = createServer((req, res) => {
24094
- const url = new URL(req.url, redirectUri.replace("/callback", ""));
24095
- if (url.pathname === "/callback") {
24096
- const code = url.searchParams.get("code");
24097
- const callbackState = url.searchParams.get("state");
24098
- const error2 = url.searchParams.get("error");
24099
- if (error2) {
24100
- res.writeHead(400, { "Content-Type": "text/html" });
24101
- res.end(`
24102
- <html>
24103
- <body>
24104
- <h1>Authentication Failed</h1>
24105
- <p>Error: ${error2}</p>
24106
- <p>You can close this window.</p>
24107
- </body>
24108
- </html>
24109
- `);
24110
- server.close();
24111
- reject(new Error(`OAuth error: ${error2}`));
24112
- return;
24113
- }
24114
- if (!callbackState || callbackState !== this.oauthState) {
24115
- res.writeHead(400, { "Content-Type": "text/html" });
24116
- res.end(`
24117
- <html>
24118
- <body>
24119
- <h1>Authentication Failed</h1>
24120
- <p>Invalid state parameter. Possible CSRF attack.</p>
24121
- <p>You can close this window.</p>
24122
- </body>
24123
- </html>
24124
- `);
24125
- server.close();
24126
- reject(new Error("Invalid OAuth state parameter (CSRF protection)"));
24127
- return;
24128
- }
24129
- if (!code) {
24130
- res.writeHead(400, { "Content-Type": "text/html" });
24131
- res.end(`
24132
- <html>
24133
- <body>
24134
- <h1>Authentication Failed</h1>
24135
- <p>No authorization code received.</p>
24136
- <p>You can close this window.</p>
24137
- </body>
24138
- </html>
24139
- `);
24140
- server.close();
24141
- reject(new Error("No authorization code received"));
24142
- return;
24143
- }
24144
- res.writeHead(200, { "Content-Type": "text/html" });
24145
- res.end(`
24146
- <html>
24147
- <body>
24148
- <h1>Authentication Successful!</h1>
24149
- <p>You can now close this window and return to your terminal.</p>
24150
- </body>
24151
- </html>
24152
- `);
24153
- server.close();
24154
- resolve2({ authCode: code, redirectUri });
24155
- } else {
24156
- res.writeHead(404, { "Content-Type": "text/plain" });
24157
- res.end("Not found");
24158
- }
24159
- });
24160
- server.listen(0, () => {
24161
- const address = server.address();
24162
- if (!address || typeof address === "string") {
24163
- reject(new Error("Failed to get server port"));
24164
- return;
24165
- }
24166
- const port = address.port;
24167
- redirectUri = `http://localhost:${port}/callback`;
24168
- log(`[GeminiOAuth] Callback server started on http://localhost:${port}`);
24169
- const authUrl = this.buildAuthUrl(codeChallenge, state, redirectUri);
24170
- this.openBrowser(authUrl);
24171
- });
24172
- server.on("error", (err) => {
24173
- reject(new Error(`Failed to start callback server: ${err.message}`));
24174
- });
24175
- setTimeout(() => {
24176
- server.close();
24177
- reject(new Error("OAuth login timed out after 5 minutes"));
24178
- }, 5 * 60 * 1000);
24179
- });
24180
- }
24181
- async exchangeCodeForTokens(code, verifier, redirectUri) {
24182
- log("[GeminiOAuth] Exchanging auth code for tokens");
24183
- try {
24184
- const response = await fetch(OAUTH_CONFIG.tokenUrl, {
24185
- method: "POST",
24186
- headers: {
24187
- "Content-Type": "application/x-www-form-urlencoded"
24188
- },
24189
- body: new URLSearchParams({
24190
- grant_type: "authorization_code",
24191
- code,
24192
- redirect_uri: redirectUri,
24193
- client_id: OAUTH_CONFIG.clientId,
24194
- client_secret: OAUTH_CONFIG.clientSecret,
24195
- code_verifier: verifier
24196
- })
24197
- });
24198
- if (!response.ok) {
24199
- const errorText = await response.text();
24200
- throw new Error(`Token exchange failed: ${response.status} - ${errorText}`);
24201
- }
24202
- const tokens = await response.json();
24203
- if (!tokens.access_token || !tokens.refresh_token) {
24204
- throw new Error("Token response missing access_token or refresh_token");
24205
- }
24206
- return tokens;
24207
- } catch (e) {
24208
- throw new Error(`Failed to authenticate with Google OAuth: ${e.message}`);
24209
- }
24210
- }
24211
- async openBrowser(url) {
24212
- const platform = process.platform;
24213
- try {
24214
- if (platform === "darwin") {
24215
- await execAsync(`open "${url}"`);
24216
- } else if (platform === "win32") {
24217
- await execAsync(`start "${url}"`);
24218
- } else {
24219
- await execAsync(`xdg-open "${url}"`);
24220
- }
24221
- console.log(`
24222
- Opening browser for authentication...`);
24223
- console.log(`If the browser doesn't open, visit this URL:
24224
- ${url}
24225
- `);
24226
- } catch (e) {
24227
- console.log(`
24228
- Please open this URL in your browser to authenticate:`);
24229
- console.log(url);
24230
- console.log("");
24231
- }
24232
- }
24233
- }
24234
- function getGeminiOAuth() {
24235
- return GeminiOAuth.getInstance();
24236
- }
24237
- async function getValidAccessToken() {
24238
- const oauth = GeminiOAuth.getInstance();
24239
- return oauth.getAccessToken();
24240
- }
24241
- function getGeminiTierDisplayName() {
24242
- if (!cachedTierId)
24243
- return "Gemini Free";
24244
- return TIER_SHORT_NAMES[cachedTierId] || cachedTierId.replace(/-tier$/, "");
24245
- }
24246
- function getGeminiTierFullName() {
24247
- if (cachedTierName)
24248
- return cachedTierName;
24249
- return getGeminiTierDisplayName();
24250
- }
24251
- async function setupGeminiUser(accessToken) {
24252
- if (cachedProjectId && cachedTierId) {
24253
- log(`[GeminiOAuth] Using cached project ID: ${cachedProjectId}, tier: ${cachedTierId}`);
24254
- return { projectId: cachedProjectId, tierId: cachedTierId };
24255
- }
24256
- const envProject = process.env.GOOGLE_CLOUD_PROJECT || process.env.GOOGLE_CLOUD_PROJECT_ID;
24257
- log("[GeminiOAuth] Calling loadCodeAssist...");
24258
- const loadRes = await callLoadCodeAssist(accessToken, envProject);
24259
- log(`[GeminiOAuth] loadCodeAssist response: ${JSON.stringify(loadRes)}`);
24260
- const resolvedTier = loadRes.paidTier?.id || (typeof loadRes.currentTier === "object" ? loadRes.currentTier?.id : loadRes.currentTier) || null;
24261
- if ((loadRes.currentTier || loadRes.paidTier) && loadRes.cloudaicompanionProject) {
24262
- const projectId2 = envProject || loadRes.cloudaicompanionProject;
24263
- if (projectId2) {
24264
- cachedProjectId = projectId2;
24265
- cachedTierId = resolvedTier || "free-tier";
24266
- cachedTierName = loadRes.paidTier?.name || null;
24267
- log(`[GeminiOAuth] User already set up, project: ${projectId2}, tier: ${cachedTierId}`);
24268
- return { projectId: projectId2, tierId: cachedTierId };
24269
- }
24270
- }
24271
- const tierId = resolvedTier || loadRes.allowedTiers?.[0]?.id || "free-tier";
24272
- const isFree = tierId === "free-tier";
24273
- const onboardProject = isFree ? undefined : envProject;
24274
- const MAX_POLL_ATTEMPTS = 30;
24275
- log(`[GeminiOAuth] Onboarding user to ${tierId}...`);
24276
- let lro = await callOnboardUser(accessToken, tierId, onboardProject);
24277
- log(`[GeminiOAuth] Initial onboardUser response: done=${lro.done}`);
24278
- let attempts = 0;
24279
- while (!lro.done && attempts < MAX_POLL_ATTEMPTS) {
24280
- attempts++;
24281
- log(`[GeminiOAuth] Polling onboardUser (attempt ${attempts}/${MAX_POLL_ATTEMPTS})...`);
24282
- await new Promise((r) => setTimeout(r, 2000));
24283
- lro = await callOnboardUser(accessToken, tierId, onboardProject);
24284
- }
24285
- if (!lro.done) {
24286
- throw new Error(`Gemini onboarding timed out after ${MAX_POLL_ATTEMPTS * 2} seconds`);
24287
- }
24288
- if (lro.error) {
24289
- throw new Error(`Gemini onboarding failed: ${JSON.stringify(lro.error)}`);
24290
- }
24291
- const projectId = lro.response?.cloudaicompanionProject?.id;
24292
- if (!projectId) {
24293
- if (envProject) {
24294
- cachedProjectId = envProject;
24295
- cachedTierId = tierId;
24296
- return { projectId: envProject, tierId };
24297
- }
24298
- throw new Error("Gemini onboarding completed but no project ID returned.");
24299
- }
24300
- cachedProjectId = projectId;
24301
- cachedTierId = tierId;
24302
- log(`[GeminiOAuth] Onboarding complete, project: ${projectId}, tier: ${tierId}`);
24303
- return { projectId, tierId };
24304
- }
24305
- async function callLoadCodeAssist(accessToken, projectId) {
24306
- const metadata = {
24307
- pluginType: "GEMINI",
24308
- ideType: "IDE_UNSPECIFIED",
24309
- platform: "PLATFORM_UNSPECIFIED",
24310
- duetProject: projectId
24311
- };
24312
- const res = await fetch(`${CODE_ASSIST_API_BASE}:loadCodeAssist`, {
24313
- method: "POST",
24314
- headers: {
24315
- Authorization: `Bearer ${accessToken}`,
24316
- "Content-Type": "application/json"
24317
- },
24318
- body: JSON.stringify({ metadata, cloudaicompanionProject: projectId })
24319
- });
24320
- if (!res.ok) {
24321
- throw new Error(`loadCodeAssist failed: ${res.status} ${await res.text()}`);
24322
- }
24323
- return await res.json();
24324
- }
24325
- async function callOnboardUser(accessToken, tierId, projectId) {
24326
- const metadata = {
24327
- pluginType: "GEMINI",
24328
- ideType: "IDE_UNSPECIFIED",
24329
- platform: "PLATFORM_UNSPECIFIED",
24330
- duetProject: projectId
24331
- };
24332
- const res = await fetch(`${CODE_ASSIST_API_BASE}:onboardUser`, {
24333
- method: "POST",
24334
- headers: {
24335
- Authorization: `Bearer ${accessToken}`,
24336
- "Content-Type": "application/json"
24337
- },
24338
- body: JSON.stringify({
24339
- tierId,
24340
- metadata,
24341
- cloudaicompanionProject: projectId
24342
- })
24343
- });
24344
- if (!res.ok) {
24345
- throw new Error(`onboardUser failed: ${res.status} ${await res.text()}`);
24346
- }
24347
- return await res.json();
24348
- }
24349
- async function retrieveUserQuota(accessToken, projectId) {
24350
- try {
24351
- const res = await fetch(`${CODE_ASSIST_API_BASE}:retrieveUserQuota`, {
24352
- method: "POST",
24353
- headers: {
24354
- Authorization: `Bearer ${accessToken}`,
24355
- "Content-Type": "application/json",
24356
- "User-Agent": `GeminiCLI/0.5.6/gemini-code-assist (${process.platform}; ${process.arch})`
24357
- },
24358
- body: JSON.stringify({ project: projectId })
24359
- });
24360
- if (!res.ok) {
24361
- log(`[GeminiOAuth] retrieveUserQuota failed: ${res.status}`);
24362
- return null;
24363
- }
24364
- return await res.json();
24365
- } catch (err) {
24366
- log(`[GeminiOAuth] retrieveUserQuota error: ${err}`);
24367
- return null;
24368
- }
24369
- }
24370
- var execAsync, getDefaultClientId = () => {
24371
- const parts = [
24372
- "681255809395",
24373
- "oo8ft2oprdrnp9e3aqf6av3hmdib135j",
24374
- "apps",
24375
- "googleusercontent",
24376
- "com"
24377
- ];
24378
- return `${parts[0]}-${parts[1]}.${parts[2]}.${parts[3]}.${parts[4]}`;
24379
- }, getDefaultClientSecret = () => {
24380
- const p = ["GOCSPX", "4uHgMPm", "1o7Sk", "geV6Cu5clXFsxl"];
24381
- return `${p[0]}-${p[1]}-${p[2]}-${p[3]}`;
24382
- }, OAUTH_CONFIG, CODE_ASSIST_API_BASE = "https://cloudcode-pa.googleapis.com/v1internal", cachedProjectId = null, cachedTierId = null, cachedTierName = null, TIER_SHORT_NAMES;
24383
- var init_gemini_oauth = __esm(() => {
24384
- init_logger();
24385
- execAsync = promisify(exec);
24386
- OAUTH_CONFIG = {
24387
- clientId: process.env.GEMINI_CLIENT_ID || getDefaultClientId(),
24388
- clientSecret: process.env.GEMINI_CLIENT_SECRET || getDefaultClientSecret(),
24389
- authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
24390
- tokenUrl: "https://oauth2.googleapis.com/token",
24391
- scopes: [
24392
- "https://www.googleapis.com/auth/cloud-platform",
24393
- "https://www.googleapis.com/auth/userinfo.email",
24394
- "https://www.googleapis.com/auth/userinfo.profile"
24395
- ]
24396
- };
24397
- TIER_SHORT_NAMES = {
24398
- "free-tier": "GeminiCA Free",
24399
- "standard-tier": "GeminiCA Std",
24400
- "g1-pro-tier": "GeminiCA Pro",
24401
- "legacy-tier": "GeminiCA Legacy"
24402
- };
24403
- });
24404
-
24405
23902
  // src/handlers/composed-handler.ts
24406
23903
  function extractAuthHeaders(c) {
24407
23904
  const headers = c.req.header();
@@ -24566,8 +24063,11 @@ class ComposedHandler {
24566
24063
  if (this.provider.displayName) {
24567
24064
  this.tokenTracker.setProviderDisplayName(this.provider.displayName);
24568
24065
  }
24569
- if (this.provider.name === "gemini-codeassist") {
24570
- this.fetchQuotaForStatusLine().catch(() => {});
24066
+ if (typeof this.provider.getQuotaRemaining === "function") {
24067
+ await Promise.race([
24068
+ this.fetchQuotaForStatusLine(),
24069
+ new Promise((r) => setTimeout(r, 2000))
24070
+ ]).catch(() => {});
24571
24071
  }
24572
24072
  } catch (err) {
24573
24073
  log(`[${this.provider.displayName}] Auth/health check failed: ${err.message}`);
@@ -24904,19 +24404,14 @@ class ComposedHandler {
24904
24404
  }
24905
24405
  async fetchQuotaForStatusLine() {
24906
24406
  try {
24907
- const { retrieveUserQuota: retrieveUserQuota2, getValidAccessToken: getValidAccessToken2 } = await Promise.resolve().then(() => (init_gemini_oauth(), exports_gemini_oauth));
24908
- const token = await getValidAccessToken2();
24909
- const transport = this.provider;
24910
- const projectId = transport.projectId;
24911
- if (!projectId)
24407
+ const fn = this.provider.getQuotaRemaining;
24408
+ if (typeof fn !== "function")
24912
24409
  return;
24913
- const quota = await retrieveUserQuota2(token, projectId);
24914
- if (!quota?.buckets?.length)
24915
- return;
24916
- const modelName = this.targetModel;
24917
- const bucket = quota.buckets.find((b) => b.modelId === modelName);
24918
- if (bucket && typeof bucket.remainingFraction === "number") {
24919
- this.tokenTracker.setQuotaRemaining(bucket.remainingFraction);
24410
+ const bareModel = this.targetModel.includes("@") ? this.targetModel.split("@")[1] : this.targetModel;
24411
+ const remaining = await fn.call(this.provider, bareModel);
24412
+ if (typeof remaining === "number") {
24413
+ this.tokenTracker.setQuotaRemaining(remaining);
24414
+ this.tokenTracker.rewrite();
24920
24415
  }
24921
24416
  } catch {}
24922
24417
  }
@@ -25169,18 +24664,18 @@ var init_remote_provider_registry = __esm(() => {
25169
24664
  });
25170
24665
 
25171
24666
  // src/auth/oauth-registry.ts
25172
- import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
25173
- import { join as join10 } from "path";
25174
- import { homedir as homedir9 } from "os";
24667
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
24668
+ import { join as join9 } from "path";
24669
+ import { homedir as homedir8 } from "os";
25175
24670
  function hasValidOAuthCredentials(descriptor) {
25176
- const credPath = join10(homedir9(), ".claudish", descriptor.credentialFile);
25177
- if (!existsSync8(credPath))
24671
+ const credPath = join9(homedir8(), ".claudish", descriptor.credentialFile);
24672
+ if (!existsSync7(credPath))
25178
24673
  return false;
25179
24674
  if (descriptor.validationMode === "file-exists") {
25180
24675
  return true;
25181
24676
  }
25182
24677
  try {
25183
- const data = JSON.parse(readFileSync6(credPath, "utf-8"));
24678
+ const data = JSON.parse(readFileSync5(credPath, "utf-8"));
25184
24679
  if (!data.access_token)
25185
24680
  return false;
25186
24681
  if (data.refresh_token)
@@ -25266,9 +24761,9 @@ var init_static_fallback = __esm(() => {
25266
24761
  });
25267
24762
 
25268
24763
  // src/providers/catalog-resolvers/openrouter.ts
25269
- import { readFileSync as readFileSync7, existsSync as existsSync9 } from "fs";
25270
- import { join as join11 } from "path";
25271
- import { homedir as homedir10 } from "os";
24764
+ import { readFileSync as readFileSync6, existsSync as existsSync8 } from "fs";
24765
+ import { join as join10 } from "path";
24766
+ import { homedir as homedir9 } from "os";
25272
24767
 
25273
24768
  class OpenRouterCatalogResolver {
25274
24769
  provider = "openrouter";
@@ -25313,10 +24808,10 @@ class OpenRouterCatalogResolver {
25313
24808
  _getModels() {
25314
24809
  if (_memCache)
25315
24810
  return _memCache;
25316
- const diskPath = join11(homedir10(), ".claudish", "all-models.json");
25317
- if (existsSync9(diskPath)) {
24811
+ const diskPath = join10(homedir9(), ".claudish", "all-models.json");
24812
+ if (existsSync8(diskPath)) {
25318
24813
  try {
25319
- const data = JSON.parse(readFileSync7(diskPath, "utf-8"));
24814
+ const data = JSON.parse(readFileSync6(diskPath, "utf-8"));
25320
24815
  if (Array.isArray(data.models) && data.models.length > 0) {
25321
24816
  _memCache = data.models;
25322
24817
  return _memCache;
@@ -25333,16 +24828,16 @@ var init_openrouter2 = __esm(() => {
25333
24828
  });
25334
24829
 
25335
24830
  // src/providers/catalog-resolvers/litellm.ts
25336
- import { readFileSync as readFileSync8, existsSync as existsSync10 } from "fs";
25337
- import { join as join12 } from "path";
25338
- import { homedir as homedir11 } from "os";
25339
- import { createHash as createHash3 } from "crypto";
24831
+ import { readFileSync as readFileSync7, existsSync as existsSync9 } from "fs";
24832
+ import { join as join11 } from "path";
24833
+ import { homedir as homedir10 } from "os";
24834
+ import { createHash as createHash2 } from "crypto";
25340
24835
  function getCachePath() {
25341
24836
  const baseUrl = process.env.LITELLM_BASE_URL;
25342
24837
  if (!baseUrl)
25343
24838
  return null;
25344
- const hash = createHash3("sha256").update(baseUrl).digest("hex").substring(0, 16);
25345
- return join12(homedir11(), ".claudish", `litellm-models-${hash}.json`);
24839
+ const hash = createHash2("sha256").update(baseUrl).digest("hex").substring(0, 16);
24840
+ return join11(homedir10(), ".claudish", `litellm-models-${hash}.json`);
25346
24841
  }
25347
24842
 
25348
24843
  class LiteLLMCatalogResolver {
@@ -25370,10 +24865,10 @@ class LiteLLMCatalogResolver {
25370
24865
  }
25371
24866
  async warmCache() {
25372
24867
  const path = getCachePath();
25373
- if (!path || !existsSync10(path))
24868
+ if (!path || !existsSync9(path))
25374
24869
  return;
25375
24870
  try {
25376
- const data = JSON.parse(readFileSync8(path, "utf-8"));
24871
+ const data = JSON.parse(readFileSync7(path, "utf-8"));
25377
24872
  if (Array.isArray(data.models)) {
25378
24873
  _memCache2 = data.models.map((m) => m.name ?? m.id?.replace("litellm@", "") ?? "");
25379
24874
  }
@@ -25386,10 +24881,10 @@ class LiteLLMCatalogResolver {
25386
24881
  if (_memCache2)
25387
24882
  return _memCache2;
25388
24883
  const path = getCachePath();
25389
- if (!path || !existsSync10(path))
24884
+ if (!path || !existsSync9(path))
25390
24885
  return null;
25391
24886
  try {
25392
- const data = JSON.parse(readFileSync8(path, "utf-8"));
24887
+ const data = JSON.parse(readFileSync7(path, "utf-8"));
25393
24888
  if (Array.isArray(data.models)) {
25394
24889
  _memCache2 = data.models.map((m) => m.name ?? m.id?.replace("litellm@", "") ?? "");
25395
24890
  return _memCache2;
@@ -25448,17 +24943,17 @@ var init_model_catalog_resolver = __esm(() => {
25448
24943
  });
25449
24944
 
25450
24945
  // src/providers/auto-route.ts
25451
- import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
25452
- import { join as join13 } from "path";
25453
- import { homedir as homedir12 } from "os";
25454
- import { createHash as createHash4 } from "crypto";
24946
+ import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
24947
+ import { join as join12 } from "path";
24948
+ import { homedir as homedir11 } from "os";
24949
+ import { createHash as createHash3 } from "crypto";
25455
24950
  function readLiteLLMCacheSync(baseUrl) {
25456
- const hash = createHash4("sha256").update(baseUrl).digest("hex").substring(0, 16);
25457
- const cachePath = join13(homedir12(), ".claudish", `litellm-models-${hash}.json`);
25458
- if (!existsSync11(cachePath))
24951
+ const hash = createHash3("sha256").update(baseUrl).digest("hex").substring(0, 16);
24952
+ const cachePath = join12(homedir11(), ".claudish", `litellm-models-${hash}.json`);
24953
+ if (!existsSync10(cachePath))
25459
24954
  return null;
25460
24955
  try {
25461
- const data = JSON.parse(readFileSync9(cachePath, "utf-8"));
24956
+ const data = JSON.parse(readFileSync8(cachePath, "utf-8"));
25462
24957
  if (!Array.isArray(data.models))
25463
24958
  return null;
25464
24959
  return data.models;
@@ -25579,17 +25074,17 @@ async function warmZenModelCache() {
25579
25074
  const models = (data.data ?? []).map((m) => ({ id: m.id }));
25580
25075
  if (models.length === 0)
25581
25076
  return;
25582
- const cacheDir = join13(homedir12(), ".claudish");
25583
- const { mkdirSync: mkdirSync8, writeFileSync: writeSync2 } = await import("fs");
25077
+ const cacheDir = join12(homedir11(), ".claudish");
25078
+ const { mkdirSync: mkdirSync8, writeFileSync: writeSync } = await import("fs");
25584
25079
  mkdirSync8(cacheDir, { recursive: true });
25585
- writeSync2(join13(cacheDir, "zen-models.json"), JSON.stringify({ models, fetchedAt: new Date().toISOString() }));
25080
+ writeSync(join12(cacheDir, "zen-models.json"), JSON.stringify({ models, fetchedAt: new Date().toISOString() }));
25586
25081
  }
25587
25082
  function readZenGoModelCacheSync() {
25588
- const cachePath = join13(homedir12(), ".claudish", "zen-go-models.json");
25589
- if (!existsSync11(cachePath))
25083
+ const cachePath = join12(homedir11(), ".claudish", "zen-go-models.json");
25084
+ if (!existsSync10(cachePath))
25590
25085
  return null;
25591
25086
  try {
25592
- const data = JSON.parse(readFileSync9(cachePath, "utf-8"));
25087
+ const data = JSON.parse(readFileSync8(cachePath, "utf-8"));
25593
25088
  if (!Array.isArray(data.models))
25594
25089
  return null;
25595
25090
  return new Set(data.models.map((m) => m.id));
@@ -25616,10 +25111,10 @@ async function warmZenGoModelCache() {
25616
25111
  const models = (data.data ?? []).map((m) => ({ id: m.id }));
25617
25112
  if (models.length === 0)
25618
25113
  return;
25619
- const cacheDir = join13(homedir12(), ".claudish");
25620
- const { mkdirSync: mkdirSync8, writeFileSync: writeSync2 } = await import("fs");
25114
+ const cacheDir = join12(homedir11(), ".claudish");
25115
+ const { mkdirSync: mkdirSync8, writeFileSync: writeSync } = await import("fs");
25621
25116
  mkdirSync8(cacheDir, { recursive: true });
25622
- writeSync2(join13(cacheDir, "zen-go-models.json"), JSON.stringify({ models, fetchedAt: new Date().toISOString() }));
25117
+ writeSync(join12(cacheDir, "zen-go-models.json"), JSON.stringify({ models, fetchedAt: new Date().toISOString() }));
25623
25118
  }
25624
25119
  function hasProviderCredentials(provider) {
25625
25120
  const keyInfo = getApiKeyEnvVars(provider);
@@ -25755,9 +25250,9 @@ __export(exports_provider_resolver, {
25755
25250
  getMissingKeyResolutions: () => getMissingKeyResolutions,
25756
25251
  getMissingKeyError: () => getMissingKeyError
25757
25252
  });
25758
- import { existsSync as existsSync12 } from "fs";
25759
- import { join as join14 } from "path";
25760
- import { homedir as homedir13 } from "os";
25253
+ import { existsSync as existsSync11 } from "fs";
25254
+ import { join as join13 } from "path";
25255
+ import { homedir as homedir12 } from "os";
25761
25256
  function getApiKeyInfoForProvider(providerName) {
25762
25257
  const lookupName = providerName === "gemini" ? "google" : providerName;
25763
25258
  const info = getApiKeyInfo(lookupName);
@@ -25792,8 +25287,8 @@ function isApiKeyAvailable(info) {
25792
25287
  }
25793
25288
  if (info.oauthFallback) {
25794
25289
  try {
25795
- const credPath = join14(homedir13(), ".claudish", info.oauthFallback);
25796
- if (existsSync12(credPath)) {
25290
+ const credPath = join13(homedir12(), ".claudish", info.oauthFallback);
25291
+ if (existsSync11(credPath)) {
25797
25292
  return true;
25798
25293
  }
25799
25294
  } catch {}
@@ -26094,9 +25589,9 @@ var init_provider_resolver = __esm(() => {
26094
25589
  });
26095
25590
 
26096
25591
  // src/services/pricing-cache.ts
26097
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync8, existsSync as existsSync13, mkdirSync as mkdirSync8, statSync as statSync2 } from "fs";
26098
- import { homedir as homedir14 } from "os";
26099
- import { join as join15 } from "path";
25592
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync8, existsSync as existsSync12, mkdirSync as mkdirSync8, statSync as statSync2 } from "fs";
25593
+ import { homedir as homedir13 } from "os";
25594
+ import { join as join14 } from "path";
26100
25595
  function getDynamicPricingSync(provider, modelName) {
26101
25596
  if (provider === "openrouter") {
26102
25597
  const direct = pricingMap.get(modelName);
@@ -26161,12 +25656,12 @@ async function warmPricingCache() {
26161
25656
  }
26162
25657
  function loadDiskCache() {
26163
25658
  try {
26164
- if (!existsSync13(CACHE_FILE))
25659
+ if (!existsSync12(CACHE_FILE))
26165
25660
  return false;
26166
25661
  const stat = statSync2(CACHE_FILE);
26167
25662
  const age = Date.now() - stat.mtimeMs;
26168
25663
  const isFresh = age < CACHE_TTL_MS;
26169
- const raw2 = readFileSync10(CACHE_FILE, "utf-8");
25664
+ const raw2 = readFileSync9(CACHE_FILE, "utf-8");
26170
25665
  const data = JSON.parse(raw2);
26171
25666
  for (const [key, pricing] of Object.entries(data)) {
26172
25667
  pricingMap.set(key, pricing);
@@ -26213,8 +25708,8 @@ var init_pricing_cache = __esm(() => {
26213
25708
  init_model_loader();
26214
25709
  init_remote_provider_types();
26215
25710
  pricingMap = new Map;
26216
- CACHE_DIR = join15(homedir14(), ".claudish");
26217
- CACHE_FILE = join15(CACHE_DIR, "pricing-cache.json");
25711
+ CACHE_DIR = join14(homedir13(), ".claudish");
25712
+ CACHE_FILE = join14(CACHE_DIR, "pricing-cache.json");
26218
25713
  CACHE_TTL_MS = 24 * 60 * 60 * 1000;
26219
25714
  PROVIDER_TO_OR_PREFIX = {
26220
25715
  openai: ["openai/"],
@@ -26602,6 +26097,512 @@ var init_gemini_apikey = __esm(() => {
26602
26097
  init_gemini_queue();
26603
26098
  });
26604
26099
 
26100
+ // src/auth/gemini-oauth.ts
26101
+ var exports_gemini_oauth = {};
26102
+ __export(exports_gemini_oauth, {
26103
+ setupGeminiUser: () => setupGeminiUser,
26104
+ retrieveUserQuota: () => retrieveUserQuota,
26105
+ getValidAccessToken: () => getValidAccessToken,
26106
+ getGeminiTierFullName: () => getGeminiTierFullName,
26107
+ getGeminiTierDisplayName: () => getGeminiTierDisplayName,
26108
+ getGeminiOAuth: () => getGeminiOAuth,
26109
+ GeminiOAuth: () => GeminiOAuth
26110
+ });
26111
+ import { createServer } from "http";
26112
+ import { randomBytes as randomBytes2, createHash as createHash4 } from "crypto";
26113
+ import { readFileSync as readFileSync10, existsSync as existsSync13, unlinkSync as unlinkSync3, openSync, writeSync, closeSync } from "fs";
26114
+ import { homedir as homedir14 } from "os";
26115
+ import { join as join15 } from "path";
26116
+ import { exec } from "child_process";
26117
+ import { promisify } from "util";
26118
+
26119
+ class GeminiOAuth {
26120
+ static instance = null;
26121
+ credentials = null;
26122
+ refreshPromise = null;
26123
+ tokenRefreshMargin = 5 * 60 * 1000;
26124
+ oauthState = null;
26125
+ static getInstance() {
26126
+ if (!GeminiOAuth.instance) {
26127
+ GeminiOAuth.instance = new GeminiOAuth;
26128
+ }
26129
+ return GeminiOAuth.instance;
26130
+ }
26131
+ constructor() {
26132
+ this.credentials = this.loadCredentials();
26133
+ }
26134
+ hasCredentials() {
26135
+ return this.credentials !== null && !!this.credentials.refresh_token;
26136
+ }
26137
+ getCredentialsPath() {
26138
+ const claudishDir = join15(homedir14(), ".claudish");
26139
+ return join15(claudishDir, "gemini-oauth.json");
26140
+ }
26141
+ async login() {
26142
+ log("[GeminiOAuth] Starting OAuth login flow");
26143
+ const codeVerifier = this.generateCodeVerifier();
26144
+ const codeChallenge = await this.generateCodeChallenge(codeVerifier);
26145
+ this.oauthState = randomBytes2(32).toString("base64url");
26146
+ const { authCode, redirectUri } = await this.startCallbackServer(codeChallenge, this.oauthState);
26147
+ const tokens = await this.exchangeCodeForTokens(authCode, codeVerifier, redirectUri);
26148
+ const credentials = {
26149
+ access_token: tokens.access_token,
26150
+ refresh_token: tokens.refresh_token,
26151
+ expires_at: Date.now() + tokens.expires_in * 1000
26152
+ };
26153
+ this.saveCredentials(credentials);
26154
+ this.credentials = credentials;
26155
+ this.oauthState = null;
26156
+ log("[GeminiOAuth] Login successful");
26157
+ }
26158
+ async logout() {
26159
+ const credPath = this.getCredentialsPath();
26160
+ if (existsSync13(credPath)) {
26161
+ unlinkSync3(credPath);
26162
+ log("[GeminiOAuth] Credentials deleted");
26163
+ }
26164
+ this.credentials = null;
26165
+ }
26166
+ async getAccessToken() {
26167
+ if (this.refreshPromise) {
26168
+ log("[GeminiOAuth] Waiting for in-progress refresh");
26169
+ return this.refreshPromise;
26170
+ }
26171
+ if (!this.credentials) {
26172
+ throw new Error("No Gemini OAuth credentials found. Please run `claudish login gemini` first.");
26173
+ }
26174
+ if (this.isTokenValid()) {
26175
+ return this.credentials.access_token;
26176
+ }
26177
+ this.refreshPromise = this.doRefreshToken();
26178
+ try {
26179
+ const token = await this.refreshPromise;
26180
+ return token;
26181
+ } finally {
26182
+ this.refreshPromise = null;
26183
+ }
26184
+ }
26185
+ async refreshToken() {
26186
+ if (!this.credentials) {
26187
+ throw new Error("No Gemini OAuth credentials found. Please run `claudish login gemini` first.");
26188
+ }
26189
+ await this.doRefreshToken();
26190
+ }
26191
+ isTokenValid() {
26192
+ if (!this.credentials)
26193
+ return false;
26194
+ return Date.now() < this.credentials.expires_at - this.tokenRefreshMargin;
26195
+ }
26196
+ async doRefreshToken() {
26197
+ if (!this.credentials) {
26198
+ throw new Error("No Gemini OAuth credentials found. Please run `claudish login gemini` first.");
26199
+ }
26200
+ log("[GeminiOAuth] Refreshing access token");
26201
+ try {
26202
+ const response = await fetch(OAUTH_CONFIG.tokenUrl, {
26203
+ method: "POST",
26204
+ headers: {
26205
+ "Content-Type": "application/x-www-form-urlencoded"
26206
+ },
26207
+ body: new URLSearchParams({
26208
+ grant_type: "refresh_token",
26209
+ refresh_token: this.credentials.refresh_token,
26210
+ client_id: OAUTH_CONFIG.clientId,
26211
+ client_secret: OAUTH_CONFIG.clientSecret
26212
+ })
26213
+ });
26214
+ if (!response.ok) {
26215
+ const errorText = await response.text();
26216
+ throw new Error(`Token refresh failed: ${response.status} - ${errorText}`);
26217
+ }
26218
+ const tokens = await response.json();
26219
+ const updatedCredentials = {
26220
+ access_token: tokens.access_token,
26221
+ refresh_token: tokens.refresh_token || this.credentials.refresh_token,
26222
+ expires_at: Date.now() + tokens.expires_in * 1000
26223
+ };
26224
+ this.saveCredentials(updatedCredentials);
26225
+ this.credentials = updatedCredentials;
26226
+ log(`[GeminiOAuth] Token refreshed, valid until ${new Date(updatedCredentials.expires_at).toISOString()}`);
26227
+ return updatedCredentials.access_token;
26228
+ } catch (e) {
26229
+ log(`[GeminiOAuth] Refresh failed: ${e.message}`);
26230
+ throw new Error(`OAuth credentials invalid. Please run \`claudish login gemini\` again.
26231
+
26232
+ Details: ${e.message}`);
26233
+ }
26234
+ }
26235
+ loadCredentials() {
26236
+ const credPath = this.getCredentialsPath();
26237
+ if (!existsSync13(credPath)) {
26238
+ return null;
26239
+ }
26240
+ try {
26241
+ const data = readFileSync10(credPath, "utf-8");
26242
+ const credentials = JSON.parse(data);
26243
+ if (!credentials.access_token || !credentials.refresh_token || !credentials.expires_at) {
26244
+ log("[GeminiOAuth] Invalid credentials file structure");
26245
+ return null;
26246
+ }
26247
+ log("[GeminiOAuth] Loaded credentials from file");
26248
+ return credentials;
26249
+ } catch (e) {
26250
+ log(`[GeminiOAuth] Failed to load credentials: ${e.message}`);
26251
+ return null;
26252
+ }
26253
+ }
26254
+ saveCredentials(credentials) {
26255
+ const credPath = this.getCredentialsPath();
26256
+ const claudishDir = join15(homedir14(), ".claudish");
26257
+ if (!existsSync13(claudishDir)) {
26258
+ const { mkdirSync: mkdirSync9 } = __require("fs");
26259
+ mkdirSync9(claudishDir, { recursive: true });
26260
+ }
26261
+ const fd = openSync(credPath, "w", 384);
26262
+ try {
26263
+ const data = JSON.stringify(credentials, null, 2);
26264
+ writeSync(fd, data, 0, "utf-8");
26265
+ } finally {
26266
+ closeSync(fd);
26267
+ }
26268
+ log(`[GeminiOAuth] Credentials saved to ${credPath}`);
26269
+ }
26270
+ generateCodeVerifier() {
26271
+ return randomBytes2(64).toString("base64url");
26272
+ }
26273
+ async generateCodeChallenge(verifier) {
26274
+ const hash = createHash4("sha256").update(verifier).digest("base64url");
26275
+ return hash;
26276
+ }
26277
+ buildAuthUrl(codeChallenge, state, redirectUri) {
26278
+ const params = new URLSearchParams({
26279
+ client_id: OAUTH_CONFIG.clientId,
26280
+ redirect_uri: redirectUri,
26281
+ response_type: "code",
26282
+ scope: OAUTH_CONFIG.scopes.join(" "),
26283
+ code_challenge: codeChallenge,
26284
+ code_challenge_method: "S256",
26285
+ access_type: "offline",
26286
+ prompt: "consent",
26287
+ state
26288
+ });
26289
+ return `${OAUTH_CONFIG.authUrl}?${params.toString()}`;
26290
+ }
26291
+ async startCallbackServer(codeChallenge, state) {
26292
+ return new Promise((resolve2, reject) => {
26293
+ let redirectUri = "";
26294
+ const server = createServer((req, res) => {
26295
+ const url = new URL(req.url, redirectUri.replace("/callback", ""));
26296
+ if (url.pathname === "/callback") {
26297
+ const code = url.searchParams.get("code");
26298
+ const callbackState = url.searchParams.get("state");
26299
+ const error2 = url.searchParams.get("error");
26300
+ if (error2) {
26301
+ res.writeHead(400, { "Content-Type": "text/html" });
26302
+ res.end(`
26303
+ <html>
26304
+ <body>
26305
+ <h1>Authentication Failed</h1>
26306
+ <p>Error: ${error2}</p>
26307
+ <p>You can close this window.</p>
26308
+ </body>
26309
+ </html>
26310
+ `);
26311
+ server.close();
26312
+ reject(new Error(`OAuth error: ${error2}`));
26313
+ return;
26314
+ }
26315
+ if (!callbackState || callbackState !== this.oauthState) {
26316
+ res.writeHead(400, { "Content-Type": "text/html" });
26317
+ res.end(`
26318
+ <html>
26319
+ <body>
26320
+ <h1>Authentication Failed</h1>
26321
+ <p>Invalid state parameter. Possible CSRF attack.</p>
26322
+ <p>You can close this window.</p>
26323
+ </body>
26324
+ </html>
26325
+ `);
26326
+ server.close();
26327
+ reject(new Error("Invalid OAuth state parameter (CSRF protection)"));
26328
+ return;
26329
+ }
26330
+ if (!code) {
26331
+ res.writeHead(400, { "Content-Type": "text/html" });
26332
+ res.end(`
26333
+ <html>
26334
+ <body>
26335
+ <h1>Authentication Failed</h1>
26336
+ <p>No authorization code received.</p>
26337
+ <p>You can close this window.</p>
26338
+ </body>
26339
+ </html>
26340
+ `);
26341
+ server.close();
26342
+ reject(new Error("No authorization code received"));
26343
+ return;
26344
+ }
26345
+ res.writeHead(200, { "Content-Type": "text/html" });
26346
+ res.end(`
26347
+ <html>
26348
+ <body>
26349
+ <h1>Authentication Successful!</h1>
26350
+ <p>You can now close this window and return to your terminal.</p>
26351
+ </body>
26352
+ </html>
26353
+ `);
26354
+ server.close();
26355
+ resolve2({ authCode: code, redirectUri });
26356
+ } else {
26357
+ res.writeHead(404, { "Content-Type": "text/plain" });
26358
+ res.end("Not found");
26359
+ }
26360
+ });
26361
+ server.listen(0, () => {
26362
+ const address = server.address();
26363
+ if (!address || typeof address === "string") {
26364
+ reject(new Error("Failed to get server port"));
26365
+ return;
26366
+ }
26367
+ const port = address.port;
26368
+ redirectUri = `http://localhost:${port}/callback`;
26369
+ log(`[GeminiOAuth] Callback server started on http://localhost:${port}`);
26370
+ const authUrl = this.buildAuthUrl(codeChallenge, state, redirectUri);
26371
+ this.openBrowser(authUrl);
26372
+ });
26373
+ server.on("error", (err) => {
26374
+ reject(new Error(`Failed to start callback server: ${err.message}`));
26375
+ });
26376
+ setTimeout(() => {
26377
+ server.close();
26378
+ reject(new Error("OAuth login timed out after 5 minutes"));
26379
+ }, 5 * 60 * 1000);
26380
+ });
26381
+ }
26382
+ async exchangeCodeForTokens(code, verifier, redirectUri) {
26383
+ log("[GeminiOAuth] Exchanging auth code for tokens");
26384
+ try {
26385
+ const response = await fetch(OAUTH_CONFIG.tokenUrl, {
26386
+ method: "POST",
26387
+ headers: {
26388
+ "Content-Type": "application/x-www-form-urlencoded"
26389
+ },
26390
+ body: new URLSearchParams({
26391
+ grant_type: "authorization_code",
26392
+ code,
26393
+ redirect_uri: redirectUri,
26394
+ client_id: OAUTH_CONFIG.clientId,
26395
+ client_secret: OAUTH_CONFIG.clientSecret,
26396
+ code_verifier: verifier
26397
+ })
26398
+ });
26399
+ if (!response.ok) {
26400
+ const errorText = await response.text();
26401
+ throw new Error(`Token exchange failed: ${response.status} - ${errorText}`);
26402
+ }
26403
+ const tokens = await response.json();
26404
+ if (!tokens.access_token || !tokens.refresh_token) {
26405
+ throw new Error("Token response missing access_token or refresh_token");
26406
+ }
26407
+ return tokens;
26408
+ } catch (e) {
26409
+ throw new Error(`Failed to authenticate with Google OAuth: ${e.message}`);
26410
+ }
26411
+ }
26412
+ async openBrowser(url) {
26413
+ const platform = process.platform;
26414
+ try {
26415
+ if (platform === "darwin") {
26416
+ await execAsync(`open "${url}"`);
26417
+ } else if (platform === "win32") {
26418
+ await execAsync(`start "${url}"`);
26419
+ } else {
26420
+ await execAsync(`xdg-open "${url}"`);
26421
+ }
26422
+ console.log(`
26423
+ Opening browser for authentication...`);
26424
+ console.log(`If the browser doesn't open, visit this URL:
26425
+ ${url}
26426
+ `);
26427
+ } catch (e) {
26428
+ console.log(`
26429
+ Please open this URL in your browser to authenticate:`);
26430
+ console.log(url);
26431
+ console.log("");
26432
+ }
26433
+ }
26434
+ }
26435
+ function getGeminiOAuth() {
26436
+ return GeminiOAuth.getInstance();
26437
+ }
26438
+ async function getValidAccessToken() {
26439
+ const oauth = GeminiOAuth.getInstance();
26440
+ return oauth.getAccessToken();
26441
+ }
26442
+ function getGeminiTierDisplayName() {
26443
+ if (!cachedTierId)
26444
+ return "Gemini Free";
26445
+ return TIER_SHORT_NAMES[cachedTierId] || cachedTierId.replace(/-tier$/, "");
26446
+ }
26447
+ function getGeminiTierFullName() {
26448
+ if (cachedTierName)
26449
+ return cachedTierName;
26450
+ return getGeminiTierDisplayName();
26451
+ }
26452
+ async function setupGeminiUser(accessToken) {
26453
+ if (cachedProjectId && cachedTierId) {
26454
+ log(`[GeminiOAuth] Using cached project ID: ${cachedProjectId}, tier: ${cachedTierId}`);
26455
+ return { projectId: cachedProjectId, tierId: cachedTierId };
26456
+ }
26457
+ const envProject = process.env.GOOGLE_CLOUD_PROJECT || process.env.GOOGLE_CLOUD_PROJECT_ID;
26458
+ log("[GeminiOAuth] Calling loadCodeAssist...");
26459
+ const loadRes = await callLoadCodeAssist(accessToken, envProject);
26460
+ log(`[GeminiOAuth] loadCodeAssist response: ${JSON.stringify(loadRes)}`);
26461
+ const resolvedTier = loadRes.paidTier?.id || (typeof loadRes.currentTier === "object" ? loadRes.currentTier?.id : loadRes.currentTier) || null;
26462
+ if ((loadRes.currentTier || loadRes.paidTier) && loadRes.cloudaicompanionProject) {
26463
+ const projectId2 = envProject || loadRes.cloudaicompanionProject;
26464
+ if (projectId2) {
26465
+ cachedProjectId = projectId2;
26466
+ cachedTierId = resolvedTier || "free-tier";
26467
+ cachedTierName = loadRes.paidTier?.name || null;
26468
+ log(`[GeminiOAuth] User already set up, project: ${projectId2}, tier: ${cachedTierId}`);
26469
+ return { projectId: projectId2, tierId: cachedTierId };
26470
+ }
26471
+ }
26472
+ const tierId = resolvedTier || loadRes.allowedTiers?.[0]?.id || "free-tier";
26473
+ const isFree = tierId === "free-tier";
26474
+ const onboardProject = isFree ? undefined : envProject;
26475
+ const MAX_POLL_ATTEMPTS = 30;
26476
+ log(`[GeminiOAuth] Onboarding user to ${tierId}...`);
26477
+ let lro = await callOnboardUser(accessToken, tierId, onboardProject);
26478
+ log(`[GeminiOAuth] Initial onboardUser response: done=${lro.done}`);
26479
+ let attempts = 0;
26480
+ while (!lro.done && attempts < MAX_POLL_ATTEMPTS) {
26481
+ attempts++;
26482
+ log(`[GeminiOAuth] Polling onboardUser (attempt ${attempts}/${MAX_POLL_ATTEMPTS})...`);
26483
+ await new Promise((r) => setTimeout(r, 2000));
26484
+ lro = await callOnboardUser(accessToken, tierId, onboardProject);
26485
+ }
26486
+ if (!lro.done) {
26487
+ throw new Error(`Gemini onboarding timed out after ${MAX_POLL_ATTEMPTS * 2} seconds`);
26488
+ }
26489
+ if (lro.error) {
26490
+ throw new Error(`Gemini onboarding failed: ${JSON.stringify(lro.error)}`);
26491
+ }
26492
+ const projectId = lro.response?.cloudaicompanionProject?.id;
26493
+ if (!projectId) {
26494
+ if (envProject) {
26495
+ cachedProjectId = envProject;
26496
+ cachedTierId = tierId;
26497
+ return { projectId: envProject, tierId };
26498
+ }
26499
+ throw new Error("Gemini onboarding completed but no project ID returned.");
26500
+ }
26501
+ cachedProjectId = projectId;
26502
+ cachedTierId = tierId;
26503
+ log(`[GeminiOAuth] Onboarding complete, project: ${projectId}, tier: ${tierId}`);
26504
+ return { projectId, tierId };
26505
+ }
26506
+ async function callLoadCodeAssist(accessToken, projectId) {
26507
+ const metadata = {
26508
+ pluginType: "GEMINI",
26509
+ ideType: "IDE_UNSPECIFIED",
26510
+ platform: "PLATFORM_UNSPECIFIED",
26511
+ duetProject: projectId
26512
+ };
26513
+ const res = await fetch(`${CODE_ASSIST_API_BASE}:loadCodeAssist`, {
26514
+ method: "POST",
26515
+ headers: {
26516
+ Authorization: `Bearer ${accessToken}`,
26517
+ "Content-Type": "application/json"
26518
+ },
26519
+ body: JSON.stringify({ metadata, cloudaicompanionProject: projectId })
26520
+ });
26521
+ if (!res.ok) {
26522
+ throw new Error(`loadCodeAssist failed: ${res.status} ${await res.text()}`);
26523
+ }
26524
+ return await res.json();
26525
+ }
26526
+ async function callOnboardUser(accessToken, tierId, projectId) {
26527
+ const metadata = {
26528
+ pluginType: "GEMINI",
26529
+ ideType: "IDE_UNSPECIFIED",
26530
+ platform: "PLATFORM_UNSPECIFIED",
26531
+ duetProject: projectId
26532
+ };
26533
+ const res = await fetch(`${CODE_ASSIST_API_BASE}:onboardUser`, {
26534
+ method: "POST",
26535
+ headers: {
26536
+ Authorization: `Bearer ${accessToken}`,
26537
+ "Content-Type": "application/json"
26538
+ },
26539
+ body: JSON.stringify({
26540
+ tierId,
26541
+ metadata,
26542
+ cloudaicompanionProject: projectId
26543
+ })
26544
+ });
26545
+ if (!res.ok) {
26546
+ throw new Error(`onboardUser failed: ${res.status} ${await res.text()}`);
26547
+ }
26548
+ return await res.json();
26549
+ }
26550
+ async function retrieveUserQuota(accessToken, projectId) {
26551
+ try {
26552
+ const res = await fetch(`${CODE_ASSIST_API_BASE}:retrieveUserQuota`, {
26553
+ method: "POST",
26554
+ headers: {
26555
+ Authorization: `Bearer ${accessToken}`,
26556
+ "Content-Type": "application/json",
26557
+ "User-Agent": `GeminiCLI/0.5.6/gemini-code-assist (${process.platform}; ${process.arch})`
26558
+ },
26559
+ body: JSON.stringify({ project: projectId })
26560
+ });
26561
+ if (!res.ok) {
26562
+ log(`[GeminiOAuth] retrieveUserQuota failed: ${res.status}`);
26563
+ return null;
26564
+ }
26565
+ return await res.json();
26566
+ } catch (err) {
26567
+ log(`[GeminiOAuth] retrieveUserQuota error: ${err}`);
26568
+ return null;
26569
+ }
26570
+ }
26571
+ var execAsync, getDefaultClientId = () => {
26572
+ const parts = [
26573
+ "681255809395",
26574
+ "oo8ft2oprdrnp9e3aqf6av3hmdib135j",
26575
+ "apps",
26576
+ "googleusercontent",
26577
+ "com"
26578
+ ];
26579
+ return `${parts[0]}-${parts[1]}.${parts[2]}.${parts[3]}.${parts[4]}`;
26580
+ }, getDefaultClientSecret = () => {
26581
+ const p = ["GOCSPX", "4uHgMPm", "1o7Sk", "geV6Cu5clXFsxl"];
26582
+ return `${p[0]}-${p[1]}-${p[2]}-${p[3]}`;
26583
+ }, OAUTH_CONFIG, CODE_ASSIST_API_BASE = "https://cloudcode-pa.googleapis.com/v1internal", cachedProjectId = null, cachedTierId = null, cachedTierName = null, TIER_SHORT_NAMES;
26584
+ var init_gemini_oauth = __esm(() => {
26585
+ init_logger();
26586
+ execAsync = promisify(exec);
26587
+ OAUTH_CONFIG = {
26588
+ clientId: process.env.GEMINI_CLIENT_ID || getDefaultClientId(),
26589
+ clientSecret: process.env.GEMINI_CLIENT_SECRET || getDefaultClientSecret(),
26590
+ authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
26591
+ tokenUrl: "https://oauth2.googleapis.com/token",
26592
+ scopes: [
26593
+ "https://www.googleapis.com/auth/cloud-platform",
26594
+ "https://www.googleapis.com/auth/userinfo.email",
26595
+ "https://www.googleapis.com/auth/userinfo.profile"
26596
+ ]
26597
+ };
26598
+ TIER_SHORT_NAMES = {
26599
+ "free-tier": "GeminiCA Free",
26600
+ "standard-tier": "GeminiCA Std",
26601
+ "g1-pro-tier": "GeminiCA Pro",
26602
+ "legacy-tier": "GeminiCA Legacy"
26603
+ };
26604
+ });
26605
+
26605
26606
  // src/providers/transport/gemini-codeassist.ts
26606
26607
  import { randomUUID as randomUUID2 } from "crypto";
26607
26608
  function buildGeminiCliUserAgent(model) {
@@ -26829,6 +26830,20 @@ ${lines.join(`
26829
26830
  }
26830
26831
  } catch {}
26831
26832
  }
26833
+ async getQuotaRemaining(modelName) {
26834
+ if (!this.accessToken || !this.projectId)
26835
+ return;
26836
+ try {
26837
+ const { retrieveUserQuota: retrieveUserQuota2 } = await Promise.resolve().then(() => (init_gemini_oauth(), exports_gemini_oauth));
26838
+ const data = await retrieveUserQuota2(this.accessToken, this.projectId);
26839
+ if (!data?.buckets?.length)
26840
+ return;
26841
+ const bucket = data.buckets.find((b) => b.modelId === modelName);
26842
+ return typeof bucket?.remainingFraction === "number" ? bucket.remainingFraction : undefined;
26843
+ } catch {
26844
+ return;
26845
+ }
26846
+ }
26832
26847
  }
26833
26848
  var CODE_ASSIST_BASE = "https://cloudcode-pa.googleapis.com", CODE_ASSIST_ENDPOINT, CODE_ASSIST_FALLBACK_CHAIN, MAX_RETRY_ATTEMPTS = 3, DEFAULT_RATE_LIMIT_DELAY_MS = 1e4;
26834
26849
  var init_gemini_codeassist = __esm(() => {
@@ -34296,7 +34311,7 @@ async function fetchGLMCodingModels() {
34296
34311
  return [];
34297
34312
  }
34298
34313
  }
34299
- var __filename4, __dirname4, VERSION = "6.5.2", CACHE_MAX_AGE_DAYS2 = 2, CLAUDISH_CACHE_DIR2, BUNDLED_MODELS_PATH, CACHED_MODELS_PATH, ALL_MODELS_JSON_PATH;
34314
+ var __filename4, __dirname4, VERSION = "6.5.3", CACHE_MAX_AGE_DAYS2 = 2, CLAUDISH_CACHE_DIR2, BUNDLED_MODELS_PATH, CACHED_MODELS_PATH, ALL_MODELS_JSON_PATH;
34300
34315
  var init_cli = __esm(() => {
34301
34316
  init_config();
34302
34317
  init_model_loader();
@@ -96647,15 +96662,10 @@ function renderStatusBar(state) {
96647
96662
  const { model, provider, errorCount, lastError, requestCount, avgRoundtripMs, quotaRemaining } = state;
96648
96663
  const parts = [];
96649
96664
  parts.push("M: claudish ");
96665
+ if (provider)
96666
+ parts.push(`D: ${provider} `);
96650
96667
  if (model)
96651
96668
  parts.push(`C: ${model} `);
96652
- if (provider)
96653
- parts.push(`W: ${provider} `);
96654
- if (typeof quotaRemaining === "number") {
96655
- const pct = Math.round(quotaRemaining * 100);
96656
- const color = pct > 50 ? "G" : pct > 20 ? "Y" : "R";
96657
- parts.push(`${color}: ${pct}% quota `);
96658
- }
96659
96669
  if (errorCount > 0) {
96660
96670
  const errLabel = errorCount === 1 ? " \u26A0 1 error " : ` \u26A0 ${errorCount} errors `;
96661
96671
  parts.push(`R:${errLabel}`);
@@ -96664,6 +96674,11 @@ function renderStatusBar(state) {
96664
96674
  } else {
96665
96675
  parts.push("G: \u25CF ok ");
96666
96676
  }
96677
+ if (typeof quotaRemaining === "number") {
96678
+ const pct = Math.round(quotaRemaining * 100);
96679
+ const color = pct > 50 ? "G" : pct > 20 ? "Y" : "R";
96680
+ parts.push(`${color}: ${pct}% quota `);
96681
+ }
96667
96682
  return parts.join("\t");
96668
96683
  }
96669
96684
  function parseLogMessage(msg) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudish",
3
- "version": "6.5.2",
3
+ "version": "6.5.3",
4
4
  "description": "Run Claude Code with any model - OpenRouter, Ollama, LM Studio & local models",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",