claudish 6.5.1 → 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.
- package/dist/index.js +624 -586
- 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.
|
|
24570
|
-
|
|
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
|
|
24908
|
-
|
|
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
|
|
24914
|
-
|
|
24915
|
-
|
|
24916
|
-
|
|
24917
|
-
|
|
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
|
|
25173
|
-
import { join as
|
|
25174
|
-
import { homedir as
|
|
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 =
|
|
25177
|
-
if (!
|
|
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(
|
|
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
|
|
25270
|
-
import { join as
|
|
25271
|
-
import { homedir as
|
|
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 =
|
|
25317
|
-
if (
|
|
24811
|
+
const diskPath = join10(homedir9(), ".claudish", "all-models.json");
|
|
24812
|
+
if (existsSync8(diskPath)) {
|
|
25318
24813
|
try {
|
|
25319
|
-
const data = JSON.parse(
|
|
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
|
|
25337
|
-
import { join as
|
|
25338
|
-
import { homedir as
|
|
25339
|
-
import { createHash as
|
|
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 =
|
|
25345
|
-
return
|
|
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 || !
|
|
24868
|
+
if (!path || !existsSync9(path))
|
|
25374
24869
|
return;
|
|
25375
24870
|
try {
|
|
25376
|
-
const data = JSON.parse(
|
|
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 || !
|
|
24884
|
+
if (!path || !existsSync9(path))
|
|
25390
24885
|
return null;
|
|
25391
24886
|
try {
|
|
25392
|
-
const data = JSON.parse(
|
|
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
|
|
25452
|
-
import { join as
|
|
25453
|
-
import { homedir as
|
|
25454
|
-
import { createHash as
|
|
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 =
|
|
25457
|
-
const cachePath =
|
|
25458
|
-
if (!
|
|
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(
|
|
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 =
|
|
25583
|
-
const { mkdirSync: mkdirSync8, writeFileSync:
|
|
25077
|
+
const cacheDir = join12(homedir11(), ".claudish");
|
|
25078
|
+
const { mkdirSync: mkdirSync8, writeFileSync: writeSync } = await import("fs");
|
|
25584
25079
|
mkdirSync8(cacheDir, { recursive: true });
|
|
25585
|
-
|
|
25080
|
+
writeSync(join12(cacheDir, "zen-models.json"), JSON.stringify({ models, fetchedAt: new Date().toISOString() }));
|
|
25586
25081
|
}
|
|
25587
25082
|
function readZenGoModelCacheSync() {
|
|
25588
|
-
const cachePath =
|
|
25589
|
-
if (!
|
|
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(
|
|
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 =
|
|
25620
|
-
const { mkdirSync: mkdirSync8, writeFileSync:
|
|
25114
|
+
const cacheDir = join12(homedir11(), ".claudish");
|
|
25115
|
+
const { mkdirSync: mkdirSync8, writeFileSync: writeSync } = await import("fs");
|
|
25621
25116
|
mkdirSync8(cacheDir, { recursive: true });
|
|
25622
|
-
|
|
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
|
|
25759
|
-
import { join as
|
|
25760
|
-
import { homedir as
|
|
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 =
|
|
25796
|
-
if (
|
|
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
|
|
26098
|
-
import { homedir as
|
|
26099
|
-
import { join as
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
26217
|
-
CACHE_FILE =
|
|
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.
|
|
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();
|
|
@@ -96521,6 +96536,8 @@ class MtmDiagRunner {
|
|
|
96521
96536
|
modelName = "";
|
|
96522
96537
|
provider = "";
|
|
96523
96538
|
port = "";
|
|
96539
|
+
quotaRemaining;
|
|
96540
|
+
tokenPollTimer = null;
|
|
96524
96541
|
lastError = "";
|
|
96525
96542
|
errorCount = 0;
|
|
96526
96543
|
requestCount = 0;
|
|
@@ -96530,6 +96547,11 @@ class MtmDiagRunner {
|
|
|
96530
96547
|
adapters = "";
|
|
96531
96548
|
setPort(port) {
|
|
96532
96549
|
this.port = String(port);
|
|
96550
|
+
this.tokenPollTimer = setInterval(() => {
|
|
96551
|
+
const changed = this.readTokenFile();
|
|
96552
|
+
if (changed)
|
|
96553
|
+
this.refreshStatusBar();
|
|
96554
|
+
}, 3000);
|
|
96533
96555
|
}
|
|
96534
96556
|
setModel(name) {
|
|
96535
96557
|
this.modelName = name.includes("/") ? name.split("/").pop() : name;
|
|
@@ -96539,16 +96561,28 @@ class MtmDiagRunner {
|
|
|
96539
96561
|
this.provider = name.split("/")[0];
|
|
96540
96562
|
}
|
|
96541
96563
|
}
|
|
96542
|
-
|
|
96543
|
-
|
|
96564
|
+
readTokenFile() {
|
|
96565
|
+
if (!this.port)
|
|
96566
|
+
return false;
|
|
96544
96567
|
try {
|
|
96545
96568
|
const tokPath = join31(homedir27(), ".claudish", `tokens-${this.port}.json`);
|
|
96546
96569
|
const tok = JSON.parse(readFileSync24(tokPath, "utf-8"));
|
|
96547
|
-
|
|
96548
|
-
|
|
96549
|
-
|
|
96570
|
+
let changed = false;
|
|
96571
|
+
if (typeof tok.quota_remaining === "number" && tok.quota_remaining !== this.quotaRemaining) {
|
|
96572
|
+
this.quotaRemaining = tok.quota_remaining;
|
|
96573
|
+
changed = true;
|
|
96574
|
+
}
|
|
96575
|
+
if (tok.provider_name && tok.provider_name !== this.provider) {
|
|
96550
96576
|
this.provider = tok.provider_name;
|
|
96551
|
-
|
|
96577
|
+
changed = true;
|
|
96578
|
+
}
|
|
96579
|
+
return changed;
|
|
96580
|
+
} catch {
|
|
96581
|
+
return false;
|
|
96582
|
+
}
|
|
96583
|
+
}
|
|
96584
|
+
refreshStatusBar() {
|
|
96585
|
+
this.readTokenFile();
|
|
96552
96586
|
const bar = renderStatusBar({
|
|
96553
96587
|
model: this.modelName,
|
|
96554
96588
|
provider: this.provider,
|
|
@@ -96556,7 +96590,7 @@ class MtmDiagRunner {
|
|
|
96556
96590
|
lastError: this.lastError,
|
|
96557
96591
|
requestCount: this.requestCount,
|
|
96558
96592
|
avgRoundtripMs: this.avgRoundtripMs,
|
|
96559
|
-
quotaRemaining
|
|
96593
|
+
quotaRemaining: this.quotaRemaining
|
|
96560
96594
|
});
|
|
96561
96595
|
try {
|
|
96562
96596
|
appendFileSync2(this.statusPath, bar + `
|
|
@@ -96567,6 +96601,10 @@ class MtmDiagRunner {
|
|
|
96567
96601
|
return this.logPath;
|
|
96568
96602
|
}
|
|
96569
96603
|
cleanup() {
|
|
96604
|
+
if (this.tokenPollTimer) {
|
|
96605
|
+
clearInterval(this.tokenPollTimer);
|
|
96606
|
+
this.tokenPollTimer = null;
|
|
96607
|
+
}
|
|
96570
96608
|
if (this.logStream) {
|
|
96571
96609
|
try {
|
|
96572
96610
|
this.logStream.end();
|
|
@@ -96624,15 +96662,10 @@ function renderStatusBar(state) {
|
|
|
96624
96662
|
const { model, provider, errorCount, lastError, requestCount, avgRoundtripMs, quotaRemaining } = state;
|
|
96625
96663
|
const parts = [];
|
|
96626
96664
|
parts.push("M: claudish ");
|
|
96665
|
+
if (provider)
|
|
96666
|
+
parts.push(`D: ${provider} `);
|
|
96627
96667
|
if (model)
|
|
96628
96668
|
parts.push(`C: ${model} `);
|
|
96629
|
-
if (provider)
|
|
96630
|
-
parts.push(`W: ${provider} `);
|
|
96631
|
-
if (typeof quotaRemaining === "number") {
|
|
96632
|
-
const pct = Math.round(quotaRemaining * 100);
|
|
96633
|
-
const color = pct > 50 ? "G" : pct > 20 ? "Y" : "R";
|
|
96634
|
-
parts.push(`${color}: ${pct}% quota `);
|
|
96635
|
-
}
|
|
96636
96669
|
if (errorCount > 0) {
|
|
96637
96670
|
const errLabel = errorCount === 1 ? " \u26A0 1 error " : ` \u26A0 ${errorCount} errors `;
|
|
96638
96671
|
parts.push(`R:${errLabel}`);
|
|
@@ -96641,6 +96674,11 @@ function renderStatusBar(state) {
|
|
|
96641
96674
|
} else {
|
|
96642
96675
|
parts.push("G: \u25CF ok ");
|
|
96643
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
|
+
}
|
|
96644
96682
|
return parts.join("\t");
|
|
96645
96683
|
}
|
|
96646
96684
|
function parseLogMessage(msg) {
|