opencode-antigravity-auth 1.3.1 → 1.3.2-beta.1
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/README.md +15 -0
- package/dist/src/antigravity/oauth.d.ts.map +1 -1
- package/dist/src/antigravity/oauth.js +10 -4
- package/dist/src/antigravity/oauth.js.map +1 -1
- package/dist/src/constants.d.ts +30 -3
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +80 -3
- package/dist/src/constants.js.map +1 -1
- package/dist/src/plugin/accounts.d.ts +42 -3
- package/dist/src/plugin/accounts.d.ts.map +1 -1
- package/dist/src/plugin/accounts.js +178 -20
- package/dist/src/plugin/accounts.js.map +1 -1
- package/dist/src/plugin/cli.d.ts +17 -12
- package/dist/src/plugin/cli.d.ts.map +1 -1
- package/dist/src/plugin/cli.js +56 -15
- package/dist/src/plugin/cli.js.map +1 -1
- package/dist/src/plugin/config/loader.d.ts.map +1 -1
- package/dist/src/plugin/config/loader.js +0 -13
- package/dist/src/plugin/config/loader.js.map +1 -1
- package/dist/src/plugin/config/schema.d.ts +38 -319
- package/dist/src/plugin/config/schema.d.ts.map +1 -1
- package/dist/src/plugin/config/schema.js +66 -27
- package/dist/src/plugin/config/schema.js.map +1 -1
- package/dist/src/plugin/core/streaming/transformer.d.ts.map +1 -1
- package/dist/src/plugin/core/streaming/transformer.js +37 -6
- package/dist/src/plugin/core/streaming/transformer.js.map +1 -1
- package/dist/src/plugin/core/streaming/types.d.ts.map +1 -1
- package/dist/src/plugin/debug.d.ts.map +1 -1
- package/dist/src/plugin/debug.js +14 -1
- package/dist/src/plugin/debug.js.map +1 -1
- package/dist/src/plugin/fingerprint.d.ts +70 -0
- package/dist/src/plugin/fingerprint.d.ts.map +1 -0
- package/dist/src/plugin/fingerprint.js +155 -0
- package/dist/src/plugin/fingerprint.js.map +1 -0
- package/dist/src/plugin/quota.d.ts +25 -0
- package/dist/src/plugin/quota.d.ts.map +1 -0
- package/dist/src/plugin/quota.js +192 -0
- package/dist/src/plugin/quota.js.map +1 -0
- package/dist/src/plugin/request-helpers.d.ts.map +1 -1
- package/dist/src/plugin/request-helpers.js +61 -23
- package/dist/src/plugin/request-helpers.js.map +1 -1
- package/dist/src/plugin/request.d.ts +4 -1
- package/dist/src/plugin/request.d.ts.map +1 -1
- package/dist/src/plugin/request.js +60 -13
- package/dist/src/plugin/request.js.map +1 -1
- package/dist/src/plugin/rotation.d.ts +5 -4
- package/dist/src/plugin/rotation.d.ts.map +1 -1
- package/dist/src/plugin/rotation.js +35 -9
- package/dist/src/plugin/rotation.js.map +1 -1
- package/dist/src/plugin/search.d.ts +32 -0
- package/dist/src/plugin/search.d.ts.map +1 -0
- package/dist/src/plugin/search.js +197 -0
- package/dist/src/plugin/search.js.map +1 -0
- package/dist/src/plugin/storage.d.ts +3 -0
- package/dist/src/plugin/storage.d.ts.map +1 -1
- package/dist/src/plugin/storage.js +15 -2
- package/dist/src/plugin/storage.js.map +1 -1
- package/dist/src/plugin/transform/gemini.d.ts +1 -13
- package/dist/src/plugin/transform/gemini.d.ts.map +1 -1
- package/dist/src/plugin/transform/gemini.js +49 -12
- package/dist/src/plugin/transform/gemini.js.map +1 -1
- package/dist/src/plugin/transform/model-resolver.d.ts.map +1 -1
- package/dist/src/plugin/transform/model-resolver.js +4 -2
- package/dist/src/plugin/transform/model-resolver.js.map +1 -1
- package/dist/src/plugin/transform/types.d.ts +5 -0
- package/dist/src/plugin/transform/types.d.ts.map +1 -1
- package/dist/src/plugin/types.d.ts +1 -0
- package/dist/src/plugin/types.d.ts.map +1 -1
- package/dist/src/plugin/ui/ansi.d.ts +32 -0
- package/dist/src/plugin/ui/ansi.d.ts.map +1 -0
- package/dist/src/plugin/ui/ansi.js +52 -0
- package/dist/src/plugin/ui/ansi.js.map +1 -0
- package/dist/src/plugin/ui/auth-menu.d.ts +29 -0
- package/dist/src/plugin/ui/auth-menu.d.ts.map +1 -0
- package/dist/src/plugin/ui/auth-menu.js +97 -0
- package/dist/src/plugin/ui/auth-menu.js.map +1 -0
- package/dist/src/plugin/ui/confirm.d.ts +2 -0
- package/dist/src/plugin/ui/confirm.d.ts.map +1 -0
- package/dist/src/plugin/ui/confirm.js +15 -0
- package/dist/src/plugin/ui/confirm.js.map +1 -0
- package/dist/src/plugin/ui/select.d.ts +14 -0
- package/dist/src/plugin/ui/select.d.ts.map +1 -0
- package/dist/src/plugin/ui/select.js +174 -0
- package/dist/src/plugin/ui/select.js.map +1 -0
- package/dist/src/plugin.d.ts.map +1 -1
- package/dist/src/plugin.js +404 -78
- package/dist/src/plugin.js.map +1 -1
- package/package.json +4 -4
package/dist/src/plugin.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { exec } from "node:child_process";
|
|
2
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
3
|
import { ANTIGRAVITY_ENDPOINT_FALLBACKS, ANTIGRAVITY_PROVIDER_ID } from "./constants";
|
|
3
4
|
import { authorizeAntigravity, exchangeAntigravity } from "./antigravity/oauth";
|
|
4
5
|
import { accessTokenExpired, isOAuthAuth, parseRefreshParts } from "./plugin/auth";
|
|
@@ -16,10 +17,12 @@ import { AccountManager, parseRateLimitReason, calculateBackoffMs } from "./plug
|
|
|
16
17
|
import { createAutoUpdateCheckerHook } from "./hooks/auto-update-checker";
|
|
17
18
|
import { loadConfig, initRuntimeConfig } from "./plugin/config";
|
|
18
19
|
import { createSessionRecoveryHook, getRecoverySuccessToast } from "./plugin/recovery";
|
|
20
|
+
import { checkAccountsQuota } from "./plugin/quota";
|
|
19
21
|
import { initDiskSignatureCache } from "./plugin/cache";
|
|
20
22
|
import { createProactiveRefreshQueue } from "./plugin/refresh-queue";
|
|
21
23
|
import { initLogger, createLogger } from "./plugin/logger";
|
|
22
24
|
import { initHealthTracker, getHealthTracker, initTokenTracker, getTokenTracker } from "./plugin/rotation";
|
|
25
|
+
import { executeSearch } from "./plugin/search";
|
|
23
26
|
const MAX_OAUTH_ACCOUNTS = 10;
|
|
24
27
|
const MAX_WARMUP_SESSIONS = 1000;
|
|
25
28
|
const MAX_WARMUP_RETRIES = 2;
|
|
@@ -31,6 +34,36 @@ function getCapacityBackoffDelay(consecutiveFailures) {
|
|
|
31
34
|
const warmupAttemptedSessionIds = new Set();
|
|
32
35
|
const warmupSucceededSessionIds = new Set();
|
|
33
36
|
const log = createLogger("plugin");
|
|
37
|
+
// Module-level toast debounce to persist across requests (fixes toast spam)
|
|
38
|
+
const rateLimitToastCooldowns = new Map();
|
|
39
|
+
const RATE_LIMIT_TOAST_COOLDOWN_MS = 5000;
|
|
40
|
+
const MAX_TOAST_COOLDOWN_ENTRIES = 100;
|
|
41
|
+
// Track if "all accounts rate-limited" toast was shown to prevent spam in while loop
|
|
42
|
+
let allAccountsRateLimitedToastShown = false;
|
|
43
|
+
function cleanupToastCooldowns() {
|
|
44
|
+
if (rateLimitToastCooldowns.size > MAX_TOAST_COOLDOWN_ENTRIES) {
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
for (const [key, time] of rateLimitToastCooldowns) {
|
|
47
|
+
if (now - time > RATE_LIMIT_TOAST_COOLDOWN_MS * 2) {
|
|
48
|
+
rateLimitToastCooldowns.delete(key);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function shouldShowRateLimitToast(message) {
|
|
54
|
+
cleanupToastCooldowns();
|
|
55
|
+
const toastKey = message.replace(/\d+/g, "X");
|
|
56
|
+
const lastShown = rateLimitToastCooldowns.get(toastKey) ?? 0;
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
if (now - lastShown < RATE_LIMIT_TOAST_COOLDOWN_MS) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
rateLimitToastCooldowns.set(toastKey, now);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
function resetAllAccountsRateLimitedToast() {
|
|
65
|
+
allAccountsRateLimitedToastShown = false;
|
|
66
|
+
}
|
|
34
67
|
function trackWarmupAttempt(sessionId) {
|
|
35
68
|
if (warmupSucceededSessionIds.has(sessionId)) {
|
|
36
69
|
return false;
|
|
@@ -242,6 +275,7 @@ async function persistAccountPool(results, replaceAll = false) {
|
|
|
242
275
|
managedProjectId: parts.managedProjectId,
|
|
243
276
|
addedAt: now,
|
|
244
277
|
lastUsed: now,
|
|
278
|
+
enabled: true,
|
|
245
279
|
});
|
|
246
280
|
continue;
|
|
247
281
|
}
|
|
@@ -283,7 +317,7 @@ async function persistAccountPool(results, replaceAll = false) {
|
|
|
283
317
|
},
|
|
284
318
|
});
|
|
285
319
|
}
|
|
286
|
-
function retryAfterMsFromResponse(response) {
|
|
320
|
+
function retryAfterMsFromResponse(response, defaultRetryMs = 60_000) {
|
|
287
321
|
const retryAfterMsHeader = response.headers.get("retry-after-ms");
|
|
288
322
|
if (retryAfterMsHeader) {
|
|
289
323
|
const parsed = Number.parseInt(retryAfterMsHeader, 10);
|
|
@@ -298,20 +332,54 @@ function retryAfterMsFromResponse(response) {
|
|
|
298
332
|
return parsed * 1000;
|
|
299
333
|
}
|
|
300
334
|
}
|
|
301
|
-
return
|
|
335
|
+
return defaultRetryMs;
|
|
302
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* Parse Go-style duration strings to milliseconds.
|
|
339
|
+
* Supports compound durations: "1h16m0.667s", "1.5s", "200ms", "5m30s"
|
|
340
|
+
*
|
|
341
|
+
* @param duration - Duration string in Go format
|
|
342
|
+
* @returns Duration in milliseconds, or null if parsing fails
|
|
343
|
+
*/
|
|
303
344
|
function parseDurationToMs(duration) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
345
|
+
// Handle simple formats first for backwards compatibility
|
|
346
|
+
const simpleMatch = duration.match(/^(\d+(?:\.\d+)?)(ms|s|m|h)?$/i);
|
|
347
|
+
if (simpleMatch) {
|
|
348
|
+
const value = parseFloat(simpleMatch[1]);
|
|
349
|
+
const unit = (simpleMatch[2] || "s").toLowerCase();
|
|
350
|
+
switch (unit) {
|
|
351
|
+
case "h": return value * 3600 * 1000;
|
|
352
|
+
case "m": return value * 60 * 1000;
|
|
353
|
+
case "s": return value * 1000;
|
|
354
|
+
case "ms": return value;
|
|
355
|
+
default: return value * 1000;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// Parse compound Go-style durations: "1h16m0.667s", "5m30s", etc.
|
|
359
|
+
const compoundRegex = /(\d+(?:\.\d+)?)(h|m(?!s)|s|ms)/gi;
|
|
360
|
+
let totalMs = 0;
|
|
361
|
+
let matchFound = false;
|
|
362
|
+
let match;
|
|
363
|
+
while ((match = compoundRegex.exec(duration)) !== null) {
|
|
364
|
+
matchFound = true;
|
|
365
|
+
const value = parseFloat(match[1]);
|
|
366
|
+
const unit = match[2].toLowerCase();
|
|
367
|
+
switch (unit) {
|
|
368
|
+
case "h":
|
|
369
|
+
totalMs += value * 3600 * 1000;
|
|
370
|
+
break;
|
|
371
|
+
case "m":
|
|
372
|
+
totalMs += value * 60 * 1000;
|
|
373
|
+
break;
|
|
374
|
+
case "s":
|
|
375
|
+
totalMs += value * 1000;
|
|
376
|
+
break;
|
|
377
|
+
case "ms":
|
|
378
|
+
totalMs += value;
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
314
381
|
}
|
|
382
|
+
return matchFound ? totalMs : null;
|
|
315
383
|
}
|
|
316
384
|
function extractRateLimitBodyInfo(body) {
|
|
317
385
|
if (!body || typeof body !== "object") {
|
|
@@ -435,9 +503,10 @@ const emptyResponseAttempts = new Map();
|
|
|
435
503
|
* @param accountIndex - The account index
|
|
436
504
|
* @param quotaKey - The quota key (e.g., "gemini-cli", "gemini-antigravity", "claude")
|
|
437
505
|
* @param serverRetryAfterMs - Server-provided retry delay (if any)
|
|
506
|
+
* @param maxBackoffMs - Maximum backoff delay in milliseconds (default 60000)
|
|
438
507
|
* @returns { attempt, delayMs, isDuplicate } - isDuplicate=true if within dedup window
|
|
439
508
|
*/
|
|
440
|
-
function getRateLimitBackoff(accountIndex, quotaKey, serverRetryAfterMs) {
|
|
509
|
+
function getRateLimitBackoff(accountIndex, quotaKey, serverRetryAfterMs, maxBackoffMs = 60_000) {
|
|
441
510
|
const now = Date.now();
|
|
442
511
|
const stateKey = `${accountIndex}:${quotaKey}`;
|
|
443
512
|
const previous = rateLimitStateByAccountQuota.get(stateKey);
|
|
@@ -445,7 +514,7 @@ function getRateLimitBackoff(accountIndex, quotaKey, serverRetryAfterMs) {
|
|
|
445
514
|
if (previous && (now - previous.lastAt < RATE_LIMIT_DEDUP_WINDOW_MS)) {
|
|
446
515
|
// Same rate limit event from concurrent request - don't increment
|
|
447
516
|
const baseDelay = serverRetryAfterMs ?? 1000;
|
|
448
|
-
const backoffDelay = Math.min(baseDelay * Math.pow(2, previous.consecutive429 - 1),
|
|
517
|
+
const backoffDelay = Math.min(baseDelay * Math.pow(2, previous.consecutive429 - 1), maxBackoffMs);
|
|
449
518
|
return {
|
|
450
519
|
attempt: previous.consecutive429,
|
|
451
520
|
delayMs: Math.max(baseDelay, backoffDelay),
|
|
@@ -462,7 +531,7 @@ function getRateLimitBackoff(accountIndex, quotaKey, serverRetryAfterMs) {
|
|
|
462
531
|
quotaKey
|
|
463
532
|
});
|
|
464
533
|
const baseDelay = serverRetryAfterMs ?? 1000;
|
|
465
|
-
const backoffDelay = Math.min(baseDelay * Math.pow(2, attempt - 1),
|
|
534
|
+
const backoffDelay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxBackoffMs);
|
|
466
535
|
return { attempt, delayMs: Math.max(baseDelay, backoffDelay), isDuplicate: false };
|
|
467
536
|
}
|
|
468
537
|
/**
|
|
@@ -540,6 +609,8 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
540
609
|
// Load configuration from files and environment variables
|
|
541
610
|
const config = loadConfig(directory);
|
|
542
611
|
initRuntimeConfig(config);
|
|
612
|
+
// Cached getAuth function for tool access
|
|
613
|
+
let cachedGetAuth = null;
|
|
543
614
|
// Initialize debug with config
|
|
544
615
|
initializeDebug(config);
|
|
545
616
|
// Initialize structured logger for TUI integration
|
|
@@ -616,11 +687,55 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
616
687
|
}
|
|
617
688
|
}
|
|
618
689
|
};
|
|
690
|
+
// Create google_search tool with access to auth context
|
|
691
|
+
const googleSearchTool = tool({
|
|
692
|
+
description: "Search the web using Google Search and analyze URLs. Returns real-time information from the internet with source citations. Use this when you need up-to-date information about current events, recent developments, or any topic that may have changed. You can also provide specific URLs to analyze. IMPORTANT: If the user mentions or provides any URLs in their query, you MUST extract those URLs and pass them in the 'urls' parameter for direct analysis.",
|
|
693
|
+
args: {
|
|
694
|
+
query: tool.schema.string().describe("The search query or question to answer using web search"),
|
|
695
|
+
urls: tool.schema.array(tool.schema.string()).optional().describe("List of specific URLs to fetch and analyze. IMPORTANT: Always extract and include any URLs mentioned by the user in their query here."),
|
|
696
|
+
thinking: tool.schema.boolean().optional().default(true).describe("Enable deep thinking for more thorough analysis (default: true)"),
|
|
697
|
+
},
|
|
698
|
+
async execute(args, ctx) {
|
|
699
|
+
log.debug("Google Search tool called", { query: args.query, urlCount: args.urls?.length ?? 0 });
|
|
700
|
+
// Get current auth context
|
|
701
|
+
const auth = cachedGetAuth ? await cachedGetAuth() : null;
|
|
702
|
+
if (!auth || !isOAuthAuth(auth)) {
|
|
703
|
+
return "Error: Not authenticated with Antigravity. Please run `opencode auth login` to authenticate.";
|
|
704
|
+
}
|
|
705
|
+
// Get access token and project ID
|
|
706
|
+
const parts = parseRefreshParts(auth.refresh);
|
|
707
|
+
const projectId = parts.managedProjectId || parts.projectId || "unknown";
|
|
708
|
+
// Ensure we have a valid access token
|
|
709
|
+
let accessToken = auth.access;
|
|
710
|
+
if (!accessToken || accessTokenExpired(auth)) {
|
|
711
|
+
try {
|
|
712
|
+
const refreshed = await refreshAccessToken(auth, client, providerId);
|
|
713
|
+
accessToken = refreshed?.access;
|
|
714
|
+
}
|
|
715
|
+
catch (error) {
|
|
716
|
+
return `Error: Failed to refresh access token: ${error instanceof Error ? error.message : String(error)}`;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
if (!accessToken) {
|
|
720
|
+
return "Error: No valid access token available. Please run `opencode auth login` to re-authenticate.";
|
|
721
|
+
}
|
|
722
|
+
return executeSearch({
|
|
723
|
+
query: args.query,
|
|
724
|
+
urls: args.urls,
|
|
725
|
+
thinking: args.thinking,
|
|
726
|
+
}, accessToken, projectId, ctx.abort);
|
|
727
|
+
},
|
|
728
|
+
});
|
|
619
729
|
return {
|
|
620
730
|
event: eventHandler,
|
|
731
|
+
tool: {
|
|
732
|
+
google_search: googleSearchTool,
|
|
733
|
+
},
|
|
621
734
|
auth: {
|
|
622
735
|
provider: providerId,
|
|
623
736
|
loader: async (getAuth, provider) => {
|
|
737
|
+
// Cache getAuth for tool access
|
|
738
|
+
cachedGetAuth = getAuth;
|
|
624
739
|
const auth = await getAuth();
|
|
625
740
|
// If OpenCode has no valid OAuth auth, clear any stale account storage
|
|
626
741
|
if (!isOAuthAuth(auth)) {
|
|
@@ -704,10 +819,20 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
704
819
|
throw abortSignal.reason instanceof Error ? abortSignal.reason : new Error("Aborted");
|
|
705
820
|
}
|
|
706
821
|
};
|
|
707
|
-
//
|
|
822
|
+
// Use while(true) loop to handle rate limits with backoff
|
|
823
|
+
// This ensures we wait and retry when all accounts are rate-limited
|
|
824
|
+
const quietMode = config.quiet_mode;
|
|
825
|
+
// Helper to show toast without blocking on abort (respects quiet_mode)
|
|
708
826
|
const showToast = async (message, variant) => {
|
|
827
|
+
if (quietMode)
|
|
828
|
+
return;
|
|
709
829
|
if (abortSignal?.aborted)
|
|
710
830
|
return;
|
|
831
|
+
if (variant === "warning" && message.toLowerCase().includes("rate")) {
|
|
832
|
+
if (!shouldShowRateLimitToast(message)) {
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
711
836
|
try {
|
|
712
837
|
await client.tui.showToast({
|
|
713
838
|
body: { message, variant },
|
|
@@ -717,9 +842,6 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
717
842
|
// TUI may not be available
|
|
718
843
|
}
|
|
719
844
|
};
|
|
720
|
-
// Use while(true) loop to handle rate limits with backoff
|
|
721
|
-
// This ensures we wait and retry when all accounts are rate-limited
|
|
722
|
-
const quietMode = config.quiet_mode;
|
|
723
845
|
const hasOtherAccountWithAntigravity = (currentAccount) => {
|
|
724
846
|
if (family !== "gemini")
|
|
725
847
|
return false;
|
|
@@ -735,8 +857,10 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
735
857
|
}
|
|
736
858
|
const account = accountManager.getCurrentOrNextForFamily(family, model, config.account_selection_strategy, 'antigravity', config.pid_offset_enabled);
|
|
737
859
|
if (!account) {
|
|
860
|
+
const headerStyle = getHeaderStyleFromUrl(urlString, family);
|
|
861
|
+
const explicitQuota = isExplicitQuotaFromUrl(urlString);
|
|
738
862
|
// All accounts are rate-limited - wait and retry
|
|
739
|
-
const waitMs = accountManager.getMinWaitTimeForFamily(family, model) || 60_000;
|
|
863
|
+
const waitMs = accountManager.getMinWaitTimeForFamily(family, model, headerStyle, explicitQuota) || 60_000;
|
|
740
864
|
const waitSecValue = Math.max(1, Math.ceil(waitMs / 1000));
|
|
741
865
|
pushDebug(`all-rate-limited family=${family} accounts=${accountCount} waitMs=${waitMs}`);
|
|
742
866
|
if (isDebugEnabled()) {
|
|
@@ -758,11 +882,16 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
758
882
|
`Quota resets in ${waitTimeFormatted}. ` +
|
|
759
883
|
`Add more accounts with \`opencode auth login\` or wait and retry.`);
|
|
760
884
|
}
|
|
761
|
-
|
|
885
|
+
if (!allAccountsRateLimitedToastShown) {
|
|
886
|
+
await showToast(`All ${accountCount} account(s) rate-limited for ${family}. Waiting ${waitSecValue}s...`, "warning");
|
|
887
|
+
allAccountsRateLimitedToastShown = true;
|
|
888
|
+
}
|
|
762
889
|
// Wait for the rate-limit cooldown to expire, then retry
|
|
763
890
|
await sleep(waitMs, abortSignal);
|
|
764
891
|
continue;
|
|
765
892
|
}
|
|
893
|
+
// Account is available - reset the toast flag
|
|
894
|
+
resetAllAccountsRateLimitedToast();
|
|
766
895
|
pushDebug(`selected idx=${account.index} email=${account.email ?? ""} family=${family} accounts=${accountCount} strategy=${config.account_selection_strategy}`);
|
|
767
896
|
if (isDebugEnabled()) {
|
|
768
897
|
logAccountContext("Selected", {
|
|
@@ -773,10 +902,13 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
773
902
|
rateLimitState: account.rateLimitResetTimes,
|
|
774
903
|
});
|
|
775
904
|
}
|
|
776
|
-
// Show toast when switching to a different account (debounced,
|
|
777
|
-
if (
|
|
905
|
+
// Show toast when switching to a different account (debounced, quiet_mode handled by showToast)
|
|
906
|
+
if (accountCount > 1 && accountManager.shouldShowAccountToast(account.index)) {
|
|
778
907
|
const accountLabel = account.email || `Account ${account.index + 1}`;
|
|
779
|
-
|
|
908
|
+
// Calculate position among enabled accounts (not absolute index)
|
|
909
|
+
const enabledAccounts = accountManager.getEnabledAccounts();
|
|
910
|
+
const enabledPosition = enabledAccounts.findIndex(a => a.index === account.index) + 1;
|
|
911
|
+
await showToast(`Using ${accountLabel} (${enabledPosition}/${accountCount})`, "info");
|
|
780
912
|
accountManager.markToastShown(account.index);
|
|
781
913
|
}
|
|
782
914
|
accountManager.requestSaveToDisk();
|
|
@@ -928,6 +1060,9 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
928
1060
|
let headerStyle = getHeaderStyleFromUrl(urlString, family);
|
|
929
1061
|
const explicitQuota = isExplicitQuotaFromUrl(urlString);
|
|
930
1062
|
pushDebug(`headerStyle=${headerStyle} explicit=${explicitQuota}`);
|
|
1063
|
+
if (account.fingerprint) {
|
|
1064
|
+
pushDebug(`fingerprint: quotaUser=${account.fingerprint.quotaUser} deviceId=${account.fingerprint.deviceId.slice(0, 8)}...`);
|
|
1065
|
+
}
|
|
931
1066
|
// Check if this header style is rate-limited for this account
|
|
932
1067
|
if (accountManager.isRateLimitedForHeaderStyle(account, family, headerStyle, model)) {
|
|
933
1068
|
// Quota fallback: try alternate quota on same account (if enabled and not explicit)
|
|
@@ -936,9 +1071,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
936
1071
|
if (alternateStyle && alternateStyle !== headerStyle) {
|
|
937
1072
|
const quotaName = headerStyle === "gemini-cli" ? "Gemini CLI" : "Antigravity";
|
|
938
1073
|
const altQuotaName = alternateStyle === "gemini-cli" ? "Gemini CLI" : "Antigravity";
|
|
939
|
-
|
|
940
|
-
await showToast(`${quotaName} quota exhausted, using ${altQuotaName} quota`, "warning");
|
|
941
|
-
}
|
|
1074
|
+
await showToast(`${quotaName} quota exhausted, using ${altQuotaName} quota`, "warning");
|
|
942
1075
|
headerStyle = alternateStyle;
|
|
943
1076
|
pushDebug(`quota fallback: ${headerStyle}`);
|
|
944
1077
|
}
|
|
@@ -955,15 +1088,20 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
955
1088
|
let forceThinkingRecovery = false;
|
|
956
1089
|
// Track if token was consumed (for hybrid strategy refund on error)
|
|
957
1090
|
let tokenConsumed = false;
|
|
1091
|
+
// Track capacity retries per endpoint to prevent infinite loops
|
|
1092
|
+
let capacityRetryCount = 0;
|
|
1093
|
+
let lastEndpointIndex = -1;
|
|
958
1094
|
for (let i = 0; i < ANTIGRAVITY_ENDPOINT_FALLBACKS.length; i++) {
|
|
1095
|
+
// Reset capacity retry counter when switching to a new endpoint
|
|
1096
|
+
if (i !== lastEndpointIndex) {
|
|
1097
|
+
capacityRetryCount = 0;
|
|
1098
|
+
lastEndpointIndex = i;
|
|
1099
|
+
}
|
|
959
1100
|
const currentEndpoint = ANTIGRAVITY_ENDPOINT_FALLBACKS[i];
|
|
960
1101
|
try {
|
|
961
1102
|
const prepared = prepareAntigravityRequest(input, init, accessToken, projectContext.effectiveProjectId, currentEndpoint, headerStyle, forceThinkingRecovery, {
|
|
962
1103
|
claudeToolHardening: config.claude_tool_hardening,
|
|
963
|
-
|
|
964
|
-
mode: config.web_search.default_mode,
|
|
965
|
-
threshold: config.web_search.grounding_threshold
|
|
966
|
-
} : undefined,
|
|
1104
|
+
fingerprint: account.fingerprint,
|
|
967
1105
|
});
|
|
968
1106
|
const originalUrl = toUrlString(input);
|
|
969
1107
|
const resolvedUrl = toUrlString(prepared.request);
|
|
@@ -979,6 +1117,12 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
979
1117
|
projectId: projectContext.effectiveProjectId,
|
|
980
1118
|
});
|
|
981
1119
|
await runThinkingWarmup(prepared, projectContext.effectiveProjectId);
|
|
1120
|
+
if (config.request_jitter_max_ms > 0) {
|
|
1121
|
+
const jitterMs = Math.floor(Math.random() * config.request_jitter_max_ms);
|
|
1122
|
+
if (jitterMs > 0) {
|
|
1123
|
+
await sleep(jitterMs, abortSignal);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
982
1126
|
// Consume token for hybrid strategy
|
|
983
1127
|
// Refunded later if request fails (429 or network error)
|
|
984
1128
|
if (config.account_selection_strategy === 'hybrid') {
|
|
@@ -986,22 +1130,62 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
986
1130
|
}
|
|
987
1131
|
const response = await fetch(prepared.request, prepared.init);
|
|
988
1132
|
pushDebug(`status=${response.status} ${response.statusText}`);
|
|
989
|
-
// Handle 429 rate limit with improved logic
|
|
990
|
-
if (response.status === 429) {
|
|
1133
|
+
// Handle 429 rate limit (or Service Overloaded) with improved logic
|
|
1134
|
+
if (response.status === 429 || response.status === 503 || response.status === 529) {
|
|
991
1135
|
// Refund token on rate limit
|
|
992
1136
|
if (tokenConsumed) {
|
|
993
1137
|
getTokenTracker().refund(account.index);
|
|
994
1138
|
tokenConsumed = false;
|
|
995
1139
|
}
|
|
996
|
-
const
|
|
1140
|
+
const defaultRetryMs = (config.default_retry_after_seconds ?? 60) * 1000;
|
|
1141
|
+
const maxBackoffMs = (config.max_backoff_seconds ?? 60) * 1000;
|
|
1142
|
+
const headerRetryMs = retryAfterMsFromResponse(response, defaultRetryMs);
|
|
997
1143
|
const bodyInfo = await extractRetryInfoFromBody(response);
|
|
998
1144
|
const serverRetryMs = bodyInfo.retryDelayMs ?? headerRetryMs;
|
|
1145
|
+
// [Enhanced Parsing] Pass status to handling logic
|
|
1146
|
+
const rateLimitReason = parseRateLimitReason(bodyInfo.reason, bodyInfo.message, response.status);
|
|
1147
|
+
// STRATEGY 1: CAPACITY / SERVER ERROR (Transient)
|
|
1148
|
+
// Goal: Wait and Retry SAME Account. DO NOT LOCK.
|
|
1149
|
+
// We handle this FIRST to avoid calling getRateLimitBackoff() and polluting the global rate limit state for transient errors.
|
|
1150
|
+
if (rateLimitReason === "MODEL_CAPACITY_EXHAUSTED" || rateLimitReason === "SERVER_ERROR") {
|
|
1151
|
+
// Exponential backoff with jitter for capacity errors: 1s → 2s → 4s → 8s (max)
|
|
1152
|
+
// Matches Antigravity-Manager's ExponentialBackoff(1s, 8s)
|
|
1153
|
+
const baseDelayMs = 1000;
|
|
1154
|
+
const maxDelayMs = 8000;
|
|
1155
|
+
const exponentialDelay = Math.min(baseDelayMs * Math.pow(2, capacityRetryCount), maxDelayMs);
|
|
1156
|
+
// Add ±10% jitter to prevent thundering herd
|
|
1157
|
+
const jitter = exponentialDelay * (0.9 + Math.random() * 0.2);
|
|
1158
|
+
const waitMs = Math.round(jitter);
|
|
1159
|
+
const waitSec = Math.round(waitMs / 1000);
|
|
1160
|
+
pushDebug(`Server busy (${rateLimitReason}) on account ${account.index}, exponential backoff ${waitMs}ms (attempt ${capacityRetryCount + 1})`);
|
|
1161
|
+
await showToast(`⏳ Server busy (${response.status}). Retrying in ${waitSec}s...`, "warning");
|
|
1162
|
+
await sleep(waitMs, abortSignal);
|
|
1163
|
+
// CRITICAL FIX: Decrement i so that the loop 'continue' retries the SAME endpoint index
|
|
1164
|
+
// (i++ in the loop will bring it back to the current index)
|
|
1165
|
+
// But limit retries to prevent infinite loops (Greptile feedback)
|
|
1166
|
+
if (capacityRetryCount < 3) {
|
|
1167
|
+
capacityRetryCount++;
|
|
1168
|
+
i -= 1;
|
|
1169
|
+
continue;
|
|
1170
|
+
}
|
|
1171
|
+
else {
|
|
1172
|
+
pushDebug(`Max capacity retries (3) exhausted for endpoint ${currentEndpoint}, regenerating fingerprint...`);
|
|
1173
|
+
// Regenerate fingerprint to get fresh device identity before trying next endpoint
|
|
1174
|
+
const newFingerprint = accountManager.regenerateAccountFingerprint(account.index);
|
|
1175
|
+
if (newFingerprint) {
|
|
1176
|
+
pushDebug(`Fingerprint regenerated for account ${account.index}`);
|
|
1177
|
+
}
|
|
1178
|
+
continue;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
// STRATEGY 2: RATE LIMIT EXCEEDED (RPM) / QUOTA EXHAUSTED / UNKNOWN
|
|
1182
|
+
// Goal: Lock and Rotate (Standard Logic)
|
|
1183
|
+
// Only now do we call getRateLimitBackoff, which increments the global failure tracker
|
|
999
1184
|
const quotaKey = headerStyleToQuotaKey(headerStyle, family);
|
|
1000
1185
|
const { attempt, delayMs, isDuplicate } = getRateLimitBackoff(account.index, quotaKey, serverRetryMs);
|
|
1001
|
-
|
|
1186
|
+
// Calculate potential backoffs
|
|
1002
1187
|
const smartBackoffMs = calculateBackoffMs(rateLimitReason, account.consecutiveFailures ?? 0, serverRetryMs);
|
|
1003
1188
|
const effectiveDelayMs = Math.max(delayMs, smartBackoffMs);
|
|
1004
|
-
const isCapacityExhausted = rateLimitReason === "MODEL_CAPACITY_EXHAUSTED";
|
|
1005
1189
|
pushDebug(`429 idx=${account.index} email=${account.email ?? ""} family=${family} delayMs=${effectiveDelayMs} attempt=${attempt} reason=${rateLimitReason}`);
|
|
1006
1190
|
if (bodyInfo.message) {
|
|
1007
1191
|
pushDebug(`429 message=${bodyInfo.message}`);
|
|
@@ -1015,36 +1199,37 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
1015
1199
|
logRateLimitEvent(account.index, account.email, family, response.status, effectiveDelayMs, bodyInfo);
|
|
1016
1200
|
await logResponseBody(debugContext, response, 429);
|
|
1017
1201
|
getHealthTracker().recordRateLimit(account.index);
|
|
1018
|
-
if (isCapacityExhausted) {
|
|
1019
|
-
const capacityBackoffMs = calculateBackoffMs(rateLimitReason, account.consecutiveFailures ?? 0, serverRetryMs);
|
|
1020
|
-
accountManager.markRateLimitedWithReason(account, family, headerStyle, model, rateLimitReason, serverRetryMs);
|
|
1021
|
-
const backoffFormatted = formatWaitTime(capacityBackoffMs);
|
|
1022
|
-
const failures = account.consecutiveFailures ?? 0;
|
|
1023
|
-
pushDebug(`capacity exhausted on account ${account.index}, backoff=${capacityBackoffMs}ms (failure #${failures})`);
|
|
1024
|
-
// Check if we can switch to another account (respects switch_on_first_rate_limit config)
|
|
1025
|
-
if (config.switch_on_first_rate_limit && accountCount > 1) {
|
|
1026
|
-
await showToast(`Server at capacity. Switching account in 1s...`, "warning");
|
|
1027
|
-
await sleep(FIRST_RETRY_DELAY_MS, abortSignal);
|
|
1028
|
-
shouldSwitchAccount = true;
|
|
1029
|
-
break;
|
|
1030
|
-
}
|
|
1031
|
-
// No other accounts available or config disabled - wait the backoff
|
|
1032
|
-
await showToast(`Server at capacity. Waiting ${backoffFormatted}... (attempt ${failures})`, "warning");
|
|
1033
|
-
await sleep(capacityBackoffMs, abortSignal);
|
|
1034
|
-
continue;
|
|
1035
|
-
}
|
|
1036
1202
|
const accountLabel = account.email || `Account ${account.index + 1}`;
|
|
1037
|
-
|
|
1203
|
+
// Progressive retry for standard 429s: 1st 429 → 1s then switch (if enabled) or retry same
|
|
1204
|
+
if (attempt === 1 && rateLimitReason !== "QUOTA_EXHAUSTED") {
|
|
1038
1205
|
await showToast(`Rate limited. Quick retry in 1s...`, "warning");
|
|
1039
1206
|
await sleep(FIRST_RETRY_DELAY_MS, abortSignal);
|
|
1207
|
+
// CacheFirst mode: wait for same account if within threshold (preserves prompt cache)
|
|
1208
|
+
if (config.scheduling_mode === 'cache_first') {
|
|
1209
|
+
const maxCacheFirstWaitMs = config.max_cache_first_wait_seconds * 1000;
|
|
1210
|
+
// effectiveDelayMs is the backoff calculated for this account
|
|
1211
|
+
if (effectiveDelayMs <= maxCacheFirstWaitMs) {
|
|
1212
|
+
pushDebug(`cache_first: waiting ${effectiveDelayMs}ms for same account to recover`);
|
|
1213
|
+
await showToast(`⏳ Waiting ${Math.ceil(effectiveDelayMs / 1000)}s for same account (prompt cache preserved)...`, "info");
|
|
1214
|
+
accountManager.markRateLimitedWithReason(account, family, headerStyle, model, rateLimitReason, serverRetryMs);
|
|
1215
|
+
await sleep(effectiveDelayMs, abortSignal);
|
|
1216
|
+
// Retry same endpoint after wait
|
|
1217
|
+
i -= 1;
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
// Wait time exceeds threshold, fall through to switch
|
|
1221
|
+
pushDebug(`cache_first: wait ${effectiveDelayMs}ms exceeds max ${maxCacheFirstWaitMs}ms, switching account`);
|
|
1222
|
+
}
|
|
1040
1223
|
if (config.switch_on_first_rate_limit && accountCount > 1) {
|
|
1041
|
-
accountManager.markRateLimitedWithReason(account, family, headerStyle, model, rateLimitReason, serverRetryMs);
|
|
1224
|
+
accountManager.markRateLimitedWithReason(account, family, headerStyle, model, rateLimitReason, serverRetryMs, config.failure_ttl_seconds * 1000);
|
|
1042
1225
|
shouldSwitchAccount = true;
|
|
1043
1226
|
break;
|
|
1044
1227
|
}
|
|
1228
|
+
// Same endpoint retry for first RPM hit
|
|
1229
|
+
i -= 1;
|
|
1045
1230
|
continue;
|
|
1046
1231
|
}
|
|
1047
|
-
accountManager.markRateLimitedWithReason(account, family, headerStyle, model, rateLimitReason, serverRetryMs);
|
|
1232
|
+
accountManager.markRateLimitedWithReason(account, family, headerStyle, model, rateLimitReason, serverRetryMs, config.failure_ttl_seconds * 1000);
|
|
1048
1233
|
accountManager.requestSaveToDisk();
|
|
1049
1234
|
// For Gemini, try prioritized Antigravity across ALL accounts first
|
|
1050
1235
|
if (family === "gemini") {
|
|
@@ -1147,6 +1332,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
1147
1332
|
if (response.ok) {
|
|
1148
1333
|
account.consecutiveFailures = 0;
|
|
1149
1334
|
getHealthTracker().recordSuccess(account.index);
|
|
1335
|
+
accountManager.markAccountUsed(account.index);
|
|
1150
1336
|
}
|
|
1151
1337
|
logAntigravityDebugResponse(debugContext, response, {
|
|
1152
1338
|
note: response.ok ? "Success" : `Error ${response.status}`,
|
|
@@ -1158,9 +1344,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
1158
1344
|
const cloned = response.clone();
|
|
1159
1345
|
const bodyText = await cloned.text();
|
|
1160
1346
|
if (bodyText.includes("Prompt is too long") || bodyText.includes("prompt_too_long")) {
|
|
1161
|
-
|
|
1162
|
-
await showToast("Context too long - use /compact to reduce size", "warning");
|
|
1163
|
-
}
|
|
1347
|
+
await showToast("Context too long - use /compact to reduce size", "warning");
|
|
1164
1348
|
const errorMessage = `[Antigravity Error] Context is too long for this model.\n\nPlease use /compact to reduce context size, then retry your request.\n\nAlternatively, you can:\n- Use /clear to start fresh\n- Use /undo to remove recent messages\n- Switch to a model with larger context window`;
|
|
1165
1349
|
return createSyntheticErrorResponse(errorMessage, prepared.requestedModel);
|
|
1166
1350
|
}
|
|
@@ -1197,7 +1381,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
1197
1381
|
const transformedResponse = await transformAntigravityResponse(response, prepared.streaming, debugContext, prepared.requestedModel, prepared.projectId, prepared.endpoint, prepared.effectiveModel, prepared.sessionId, prepared.toolDebugMissing, prepared.toolDebugSummary, prepared.toolDebugPayload, debugLines);
|
|
1198
1382
|
// Check for context errors and show appropriate toast
|
|
1199
1383
|
const contextError = transformedResponse.headers.get("x-antigravity-context-error");
|
|
1200
|
-
if (contextError
|
|
1384
|
+
if (contextError) {
|
|
1201
1385
|
if (contextError === "prompt_too_long") {
|
|
1202
1386
|
await showToast("Context too long - use /compact to reduce size, or trim your request", "warning");
|
|
1203
1387
|
}
|
|
@@ -1289,18 +1473,135 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
1289
1473
|
const useManualMode = noBrowser || shouldSkipLocalServer();
|
|
1290
1474
|
// Check for existing accounts and prompt user for login mode
|
|
1291
1475
|
let startFresh = true;
|
|
1476
|
+
let refreshAccountIndex;
|
|
1292
1477
|
const existingStorage = await loadAccounts();
|
|
1293
1478
|
if (existingStorage && existingStorage.accounts.length > 0) {
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1479
|
+
let menuResult;
|
|
1480
|
+
while (true) {
|
|
1481
|
+
const now = Date.now();
|
|
1482
|
+
const existingAccounts = existingStorage.accounts.map((acc, idx) => {
|
|
1483
|
+
let status = 'unknown';
|
|
1484
|
+
const rateLimits = acc.rateLimitResetTimes;
|
|
1485
|
+
if (rateLimits) {
|
|
1486
|
+
const isRateLimited = Object.values(rateLimits).some((resetTime) => typeof resetTime === 'number' && resetTime > now);
|
|
1487
|
+
if (isRateLimited) {
|
|
1488
|
+
status = 'rate-limited';
|
|
1489
|
+
}
|
|
1490
|
+
else {
|
|
1491
|
+
status = 'active';
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
else {
|
|
1495
|
+
status = 'active';
|
|
1496
|
+
}
|
|
1497
|
+
if (acc.coolingDownUntil && acc.coolingDownUntil > now) {
|
|
1498
|
+
status = 'rate-limited';
|
|
1499
|
+
}
|
|
1500
|
+
return {
|
|
1501
|
+
email: acc.email,
|
|
1502
|
+
index: idx,
|
|
1503
|
+
addedAt: acc.addedAt,
|
|
1504
|
+
lastUsed: acc.lastUsed,
|
|
1505
|
+
status,
|
|
1506
|
+
isCurrentAccount: idx === (existingStorage.activeIndex ?? 0),
|
|
1507
|
+
enabled: acc.enabled !== false,
|
|
1508
|
+
};
|
|
1509
|
+
});
|
|
1510
|
+
menuResult = await promptLoginMode(existingAccounts);
|
|
1511
|
+
if (menuResult.mode === "check") {
|
|
1512
|
+
console.log("\nChecking quotas for all accounts...");
|
|
1513
|
+
const results = await checkAccountsQuota(existingStorage.accounts, client, providerId);
|
|
1514
|
+
for (const res of results) {
|
|
1515
|
+
const label = res.email || `Account ${res.index + 1}`;
|
|
1516
|
+
const disabledStr = res.disabled ? " (disabled)" : "";
|
|
1517
|
+
console.log(`\n${res.index + 1}. ${label}${disabledStr}`);
|
|
1518
|
+
if (res.status === "error") {
|
|
1519
|
+
console.log(` Error: ${res.error}`);
|
|
1520
|
+
continue;
|
|
1521
|
+
}
|
|
1522
|
+
if (!res.quota || Object.keys(res.quota.groups).length === 0) {
|
|
1523
|
+
console.log(" No quota information available.");
|
|
1524
|
+
if (res.quota?.error)
|
|
1525
|
+
console.log(` Error: ${res.quota.error}`);
|
|
1526
|
+
continue;
|
|
1527
|
+
}
|
|
1528
|
+
const printGrp = (name, group) => {
|
|
1529
|
+
if (!group)
|
|
1530
|
+
return;
|
|
1531
|
+
const remaining = typeof group.remainingFraction === 'number'
|
|
1532
|
+
? `${Math.round(group.remainingFraction * 100)}%`
|
|
1533
|
+
: 'UNKNOWN';
|
|
1534
|
+
const resetStr = group.resetTime ? `, resets in ${formatWaitTime(Date.parse(group.resetTime) - Date.now())}` : '';
|
|
1535
|
+
console.log(` ${name}: ${remaining}${resetStr}`);
|
|
1536
|
+
};
|
|
1537
|
+
printGrp("Claude", res.quota.groups.claude);
|
|
1538
|
+
printGrp("Gemini 3 Pro", res.quota.groups["gemini-pro"]);
|
|
1539
|
+
printGrp("Gemini 3 Flash", res.quota.groups["gemini-flash"]);
|
|
1540
|
+
if (res.updatedAccount) {
|
|
1541
|
+
existingStorage.accounts[res.index] = res.updatedAccount;
|
|
1542
|
+
await saveAccounts(existingStorage);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
console.log("");
|
|
1546
|
+
continue;
|
|
1547
|
+
}
|
|
1548
|
+
if (menuResult.mode === "manage") {
|
|
1549
|
+
if (menuResult.toggleAccountIndex !== undefined) {
|
|
1550
|
+
const acc = existingStorage.accounts[menuResult.toggleAccountIndex];
|
|
1551
|
+
if (acc) {
|
|
1552
|
+
acc.enabled = acc.enabled === false;
|
|
1553
|
+
await saveAccounts(existingStorage);
|
|
1554
|
+
console.log(`\nAccount ${acc.email || menuResult.toggleAccountIndex + 1} ${acc.enabled ? 'enabled' : 'disabled'}.\n`);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
break;
|
|
1560
|
+
}
|
|
1561
|
+
if (menuResult.mode === "cancel") {
|
|
1562
|
+
return {
|
|
1563
|
+
url: "",
|
|
1564
|
+
instructions: "Authentication cancelled",
|
|
1565
|
+
method: "auto",
|
|
1566
|
+
callback: async () => ({ type: "failed", error: "Authentication cancelled" }),
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
if (menuResult.deleteAccountIndex !== undefined) {
|
|
1570
|
+
const updatedAccounts = existingStorage.accounts.filter((_, idx) => idx !== menuResult.deleteAccountIndex);
|
|
1571
|
+
await saveAccounts({
|
|
1572
|
+
version: 3,
|
|
1573
|
+
accounts: updatedAccounts,
|
|
1574
|
+
activeIndex: 0,
|
|
1575
|
+
activeIndexByFamily: { claude: 0, gemini: 0 },
|
|
1576
|
+
});
|
|
1577
|
+
console.log("\nAccount deleted.\n");
|
|
1578
|
+
if (updatedAccounts.length > 0) {
|
|
1579
|
+
return {
|
|
1580
|
+
url: "",
|
|
1581
|
+
instructions: "Account deleted. Please run `opencode auth login` again to continue.",
|
|
1582
|
+
method: "auto",
|
|
1583
|
+
callback: async () => ({ type: "failed", error: "Account deleted - please re-run auth" }),
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
if (menuResult.refreshAccountIndex !== undefined) {
|
|
1588
|
+
refreshAccountIndex = menuResult.refreshAccountIndex;
|
|
1589
|
+
const refreshEmail = existingStorage.accounts[refreshAccountIndex]?.email;
|
|
1590
|
+
console.log(`\nRe-authenticating ${refreshEmail || 'account'}...\n`);
|
|
1591
|
+
startFresh = false;
|
|
1592
|
+
}
|
|
1593
|
+
if (menuResult.deleteAll) {
|
|
1594
|
+
await clearAccounts();
|
|
1595
|
+
console.log("\nAll accounts deleted.\n");
|
|
1596
|
+
startFresh = true;
|
|
1302
1597
|
}
|
|
1303
1598
|
else {
|
|
1599
|
+
startFresh = menuResult.mode === "fresh";
|
|
1600
|
+
}
|
|
1601
|
+
if (startFresh && !menuResult.deleteAll) {
|
|
1602
|
+
console.log("\nStarting fresh - existing accounts will be replaced.\n");
|
|
1603
|
+
}
|
|
1604
|
+
else if (!startFresh) {
|
|
1304
1605
|
console.log("\nAdding to existing accounts.\n");
|
|
1305
1606
|
}
|
|
1306
1607
|
}
|
|
@@ -1394,7 +1695,6 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
1394
1695
|
break;
|
|
1395
1696
|
}
|
|
1396
1697
|
accounts.push(result);
|
|
1397
|
-
// Show toast for successful account authentication
|
|
1398
1698
|
try {
|
|
1399
1699
|
await client.tui.showToast({
|
|
1400
1700
|
body: {
|
|
@@ -1404,15 +1704,40 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
1404
1704
|
});
|
|
1405
1705
|
}
|
|
1406
1706
|
catch {
|
|
1407
|
-
// TUI may not be available in CLI mode
|
|
1408
1707
|
}
|
|
1409
1708
|
try {
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1709
|
+
if (refreshAccountIndex !== undefined) {
|
|
1710
|
+
const currentStorage = await loadAccounts();
|
|
1711
|
+
if (currentStorage) {
|
|
1712
|
+
const updatedAccounts = [...currentStorage.accounts];
|
|
1713
|
+
const parts = parseRefreshParts(result.refresh);
|
|
1714
|
+
if (parts.refreshToken) {
|
|
1715
|
+
updatedAccounts[refreshAccountIndex] = {
|
|
1716
|
+
email: result.email ?? updatedAccounts[refreshAccountIndex]?.email,
|
|
1717
|
+
refreshToken: parts.refreshToken,
|
|
1718
|
+
projectId: parts.projectId ?? updatedAccounts[refreshAccountIndex]?.projectId,
|
|
1719
|
+
managedProjectId: parts.managedProjectId ?? updatedAccounts[refreshAccountIndex]?.managedProjectId,
|
|
1720
|
+
addedAt: updatedAccounts[refreshAccountIndex]?.addedAt ?? Date.now(),
|
|
1721
|
+
lastUsed: Date.now(),
|
|
1722
|
+
};
|
|
1723
|
+
await saveAccounts({
|
|
1724
|
+
version: 3,
|
|
1725
|
+
accounts: updatedAccounts,
|
|
1726
|
+
activeIndex: currentStorage.activeIndex,
|
|
1727
|
+
activeIndexByFamily: currentStorage.activeIndexByFamily,
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
else {
|
|
1733
|
+
const isFirstAccount = accounts.length === 1;
|
|
1734
|
+
await persistAccountPool([result], isFirstAccount && startFresh);
|
|
1735
|
+
}
|
|
1413
1736
|
}
|
|
1414
1737
|
catch {
|
|
1415
|
-
|
|
1738
|
+
}
|
|
1739
|
+
if (refreshAccountIndex !== undefined) {
|
|
1740
|
+
break;
|
|
1416
1741
|
}
|
|
1417
1742
|
if (accounts.length >= MAX_OAUTH_ACCOUNTS) {
|
|
1418
1743
|
break;
|
|
@@ -1442,7 +1767,6 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
1442
1767
|
callback: async () => ({ type: "failed", error: "Authentication cancelled" }),
|
|
1443
1768
|
};
|
|
1444
1769
|
}
|
|
1445
|
-
// Get the actual deduplicated account count from storage
|
|
1446
1770
|
let actualAccountCount = accounts.length;
|
|
1447
1771
|
try {
|
|
1448
1772
|
const finalStorage = await loadAccounts();
|
|
@@ -1451,11 +1775,13 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
|
|
|
1451
1775
|
}
|
|
1452
1776
|
}
|
|
1453
1777
|
catch {
|
|
1454
|
-
// Fall back to accounts.length if we can't read storage
|
|
1455
1778
|
}
|
|
1779
|
+
const successMessage = refreshAccountIndex !== undefined
|
|
1780
|
+
? `Token refreshed successfully.`
|
|
1781
|
+
: `Multi-account setup complete (${actualAccountCount} account(s)).`;
|
|
1456
1782
|
return {
|
|
1457
1783
|
url: "",
|
|
1458
|
-
instructions:
|
|
1784
|
+
instructions: successMessage,
|
|
1459
1785
|
method: "auto",
|
|
1460
1786
|
callback: async () => primary,
|
|
1461
1787
|
};
|