mirror of
https://github.com/NoeFabris/opencode-antigravity-auth.git
synced 2026-05-13 23:53:18 +00:00
- Add enabled field to AccountMetadataV3 for account enable/disable - Add getEnabledAccounts/getTotalAccountCount methods to AccountManager - Add quota.ts with quota checking logic via fetchAvailableModels API - Integrate 'Check quotas' and 'Manage accounts' options into auth menu - Display quota status for Claude, Gemini 3 Pro, and Gemini 3 Flash - Add standalone check-quota.mjs script for manual verification - Update tsconfig to exclude temp_research folder
219 lines
6.9 KiB
JavaScript
219 lines
6.9 KiB
JavaScript
import { readFileSync } from "node:fs";
|
|
import { homedir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
const CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
|
|
const CLIENT_SECRET = "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";
|
|
const CLOUD_CODE_BASE = "https://cloudcode-pa.googleapis.com";
|
|
const USER_AGENT = "antigravity/windows/amd64";
|
|
const FALLBACK_PROJECT_ID = "bamboo-precept-lgxtn";
|
|
|
|
function getDefaultAccountsPath() {
|
|
if (process.platform === "win32") {
|
|
const appData = process.env.APPDATA || join(homedir(), "AppData", "Roaming");
|
|
return join(appData, "opencode", "antigravity-accounts.json");
|
|
}
|
|
const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
return join(xdgConfig, "opencode", "antigravity-accounts.json");
|
|
}
|
|
|
|
function parseArgs() {
|
|
const args = process.argv.slice(2);
|
|
let path = getDefaultAccountsPath();
|
|
let accountIndex = null;
|
|
for (let i = 0; i < args.length; i += 1) {
|
|
const arg = args[i];
|
|
if (arg === "--path" && args[i + 1]) {
|
|
path = args[i + 1];
|
|
i += 1;
|
|
continue;
|
|
}
|
|
if (arg === "--account" && args[i + 1]) {
|
|
const parsed = Number.parseInt(args[i + 1], 10);
|
|
if (!Number.isNaN(parsed)) {
|
|
accountIndex = parsed - 1;
|
|
}
|
|
i += 1;
|
|
}
|
|
}
|
|
return { path, accountIndex };
|
|
}
|
|
|
|
async function postJson(url, token, body, extraHeaders = {}) {
|
|
return fetch(url, {
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
"Content-Type": "application/json",
|
|
"User-Agent": USER_AGENT,
|
|
...extraHeaders,
|
|
},
|
|
body: JSON.stringify(body),
|
|
});
|
|
}
|
|
|
|
async function refreshAccessToken(refreshToken) {
|
|
const response = await fetch("https://oauth2.googleapis.com/token", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
},
|
|
body: new URLSearchParams({
|
|
grant_type: "refresh_token",
|
|
refresh_token: refreshToken,
|
|
client_id: CLIENT_ID,
|
|
client_secret: CLIENT_SECRET,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const text = await response.text().catch(() => "");
|
|
throw new Error(`Token refresh failed (${response.status}): ${text.slice(0, 200)}`);
|
|
}
|
|
|
|
const payload = await response.json();
|
|
return payload.access_token;
|
|
}
|
|
|
|
async function loadProjectId(accessToken) {
|
|
const body = { metadata: { ideType: "ANTIGRAVITY" } };
|
|
const response = await postJson(`${CLOUD_CODE_BASE}/v1internal:loadCodeAssist`, accessToken, body);
|
|
if (!response.ok) {
|
|
return "";
|
|
}
|
|
const payload = await response.json();
|
|
if (typeof payload.cloudaicompanionProject === "string") {
|
|
return payload.cloudaicompanionProject;
|
|
}
|
|
if (payload.cloudaicompanionProject && typeof payload.cloudaicompanionProject.id === "string") {
|
|
return payload.cloudaicompanionProject.id;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function classifyGroup(modelName) {
|
|
const lower = modelName.toLowerCase();
|
|
if (lower.includes("claude")) return "claude";
|
|
if (!lower.includes("gemini-3")) return null;
|
|
if (lower.includes("flash")) return "gemini-flash";
|
|
return "gemini-pro";
|
|
}
|
|
|
|
function updateGroup(groups, group, remainingFraction, resetTime) {
|
|
const entry = groups[group] || { count: 0 };
|
|
entry.count += 1;
|
|
if (typeof remainingFraction === "number") {
|
|
if (entry.remaining === undefined) {
|
|
entry.remaining = remainingFraction;
|
|
} else {
|
|
entry.remaining = Math.min(entry.remaining, remainingFraction);
|
|
}
|
|
}
|
|
if (resetTime) {
|
|
const timestamp = Date.parse(resetTime);
|
|
if (Number.isFinite(timestamp)) {
|
|
if (!entry.resetTime) {
|
|
entry.resetTime = resetTime;
|
|
} else {
|
|
const existing = Date.parse(entry.resetTime);
|
|
if (!Number.isFinite(existing) || timestamp < existing) {
|
|
entry.resetTime = resetTime;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
groups[group] = entry;
|
|
}
|
|
|
|
function formatDuration(targetTime) {
|
|
const delta = targetTime - Date.now();
|
|
if (delta <= 0) return "now";
|
|
const totalSeconds = Math.round(delta / 1000);
|
|
const hours = Math.floor(totalSeconds / 3600);
|
|
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
if (hours > 0) return `${hours}h ${minutes}m`;
|
|
return `${minutes}m`;
|
|
}
|
|
|
|
function printGroup(label, entry) {
|
|
if (!entry || entry.count === 0) return;
|
|
const remaining = typeof entry.remaining === "number" ? Math.round(entry.remaining * 100) : null;
|
|
const status = remaining === null ? "UNKNOWN" : remaining <= 0 ? "LIMITED" : "OK";
|
|
const details = [];
|
|
if (remaining !== null) details.push(`remaining ${remaining}%`);
|
|
if (entry.resetTime) {
|
|
const time = formatDuration(Date.parse(entry.resetTime));
|
|
details.push(`resets in ${time}`);
|
|
}
|
|
const suffix = details.length ? ` (${details.join(", ")})` : "";
|
|
console.log(` ${label}: ${status}${suffix}`);
|
|
}
|
|
|
|
async function run() {
|
|
const { path, accountIndex } = parseArgs();
|
|
const payload = JSON.parse(readFileSync(path, "utf8"));
|
|
const accounts = payload.accounts || [];
|
|
|
|
if (accounts.length === 0) {
|
|
console.log("No accounts found.");
|
|
return;
|
|
}
|
|
|
|
const selected = accountIndex === null
|
|
? accounts.map((account, index) => ({ account, index }))
|
|
: accounts
|
|
.map((account, index) => ({ account, index }))
|
|
.filter((item) => item.index === accountIndex);
|
|
|
|
for (const { account, index } of selected) {
|
|
const label = account.email || `Account ${index + 1}`;
|
|
const disabled = account.enabled === false ? " (disabled)" : "";
|
|
console.log(`\n${index + 1}. ${label}${disabled}`);
|
|
|
|
try {
|
|
const accessToken = await refreshAccessToken(account.refreshToken);
|
|
let projectId = await loadProjectId(accessToken);
|
|
if (!projectId) {
|
|
projectId = account.managedProjectId || account.projectId || FALLBACK_PROJECT_ID;
|
|
}
|
|
console.log(` project: ${projectId}`);
|
|
|
|
const body = projectId ? { project: projectId } : {};
|
|
const response = await postJson(
|
|
`${CLOUD_CODE_BASE}/v1internal:fetchAvailableModels`,
|
|
accessToken,
|
|
body,
|
|
);
|
|
console.log(` fetchAvailableModels: ${response.status}`);
|
|
|
|
if (!response.ok) {
|
|
const text = await response.text().catch(() => "");
|
|
console.log(` error: ${text.trim().slice(0, 200)}`);
|
|
continue;
|
|
}
|
|
|
|
const data = await response.json();
|
|
const groups = {};
|
|
const models = data.models || {};
|
|
for (const [modelName, info] of Object.entries(models)) {
|
|
const group = classifyGroup(modelName);
|
|
if (!group) continue;
|
|
if (!info || !info.quotaInfo) continue;
|
|
const remaining = info.quotaInfo.remainingFraction ?? 0;
|
|
updateGroup(groups, group, remaining, info.quotaInfo.resetTime);
|
|
}
|
|
|
|
printGroup("Claude", groups["claude"]);
|
|
printGroup("Gemini 3 Pro", groups["gemini-pro"]);
|
|
printGroup("Gemini 3 Flash", groups["gemini-flash"]);
|
|
} catch (error) {
|
|
console.log(` error: ${error instanceof Error ? error.message : String(error)}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
run().catch((error) => {
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
});
|