antigravity-claude-proxy 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +289 -0
- package/bin/cli.js +109 -0
- package/package.json +54 -0
- package/src/account-manager.js +633 -0
- package/src/accounts-cli.js +437 -0
- package/src/cloudcode-client.js +1018 -0
- package/src/constants.js +164 -0
- package/src/errors.js +159 -0
- package/src/format-converter.js +731 -0
- package/src/index.js +40 -0
- package/src/oauth.js +346 -0
- package/src/server.js +517 -0
- package/src/token-extractor.js +146 -0
- package/src/utils/helpers.js +33 -0
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account Manager
|
|
3
|
+
* Manages multiple Antigravity accounts with sticky selection,
|
|
4
|
+
* automatic failover, and smart cooldown for rate-limited accounts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFile, writeFile, mkdir, access } from 'fs/promises';
|
|
8
|
+
import { constants as fsConstants } from 'fs';
|
|
9
|
+
import { dirname } from 'path';
|
|
10
|
+
import { execSync } from 'child_process';
|
|
11
|
+
import {
|
|
12
|
+
ACCOUNT_CONFIG_PATH,
|
|
13
|
+
ANTIGRAVITY_DB_PATH,
|
|
14
|
+
DEFAULT_COOLDOWN_MS,
|
|
15
|
+
TOKEN_REFRESH_INTERVAL_MS,
|
|
16
|
+
ANTIGRAVITY_ENDPOINT_FALLBACKS,
|
|
17
|
+
ANTIGRAVITY_HEADERS,
|
|
18
|
+
DEFAULT_PROJECT_ID,
|
|
19
|
+
MAX_WAIT_BEFORE_ERROR_MS
|
|
20
|
+
} from './constants.js';
|
|
21
|
+
import { refreshAccessToken } from './oauth.js';
|
|
22
|
+
import { formatDuration } from './utils/helpers.js';
|
|
23
|
+
|
|
24
|
+
export class AccountManager {
|
|
25
|
+
#accounts = [];
|
|
26
|
+
#currentIndex = 0;
|
|
27
|
+
#configPath;
|
|
28
|
+
#settings = {};
|
|
29
|
+
#initialized = false;
|
|
30
|
+
|
|
31
|
+
// Per-account caches
|
|
32
|
+
#tokenCache = new Map(); // email -> { token, extractedAt }
|
|
33
|
+
#projectCache = new Map(); // email -> projectId
|
|
34
|
+
|
|
35
|
+
constructor(configPath = ACCOUNT_CONFIG_PATH) {
|
|
36
|
+
this.#configPath = configPath;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Initialize the account manager by loading config
|
|
41
|
+
*/
|
|
42
|
+
async initialize() {
|
|
43
|
+
if (this.#initialized) return;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// Check if config file exists using async access
|
|
47
|
+
await access(this.#configPath, fsConstants.F_OK);
|
|
48
|
+
const configData = await readFile(this.#configPath, 'utf-8');
|
|
49
|
+
const config = JSON.parse(configData);
|
|
50
|
+
|
|
51
|
+
this.#accounts = (config.accounts || []).map(acc => ({
|
|
52
|
+
...acc,
|
|
53
|
+
isRateLimited: acc.isRateLimited || false,
|
|
54
|
+
rateLimitResetTime: acc.rateLimitResetTime || null,
|
|
55
|
+
lastUsed: acc.lastUsed || null
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
this.#settings = config.settings || {};
|
|
59
|
+
this.#currentIndex = config.activeIndex || 0;
|
|
60
|
+
|
|
61
|
+
// Clamp currentIndex to valid range
|
|
62
|
+
if (this.#currentIndex >= this.#accounts.length) {
|
|
63
|
+
this.#currentIndex = 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log(`[AccountManager] Loaded ${this.#accounts.length} account(s) from config`);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (error.code === 'ENOENT') {
|
|
69
|
+
// No config file - use single account from Antigravity database
|
|
70
|
+
console.log('[AccountManager] No config file found. Using Antigravity database (single account mode)');
|
|
71
|
+
} else {
|
|
72
|
+
console.error('[AccountManager] Failed to load config:', error.message);
|
|
73
|
+
}
|
|
74
|
+
// Fall back to default account
|
|
75
|
+
await this.#loadDefaultAccount();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Clear any expired rate limits
|
|
79
|
+
this.clearExpiredLimits();
|
|
80
|
+
|
|
81
|
+
this.#initialized = true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Load the default account from Antigravity's database
|
|
86
|
+
*/
|
|
87
|
+
async #loadDefaultAccount() {
|
|
88
|
+
try {
|
|
89
|
+
const authData = this.#extractTokenFromDB();
|
|
90
|
+
if (authData?.apiKey) {
|
|
91
|
+
this.#accounts = [{
|
|
92
|
+
email: authData.email || 'default@antigravity',
|
|
93
|
+
source: 'database',
|
|
94
|
+
isRateLimited: false,
|
|
95
|
+
rateLimitResetTime: null,
|
|
96
|
+
lastUsed: null
|
|
97
|
+
}];
|
|
98
|
+
// Pre-cache the token
|
|
99
|
+
this.#tokenCache.set(this.#accounts[0].email, {
|
|
100
|
+
token: authData.apiKey,
|
|
101
|
+
extractedAt: Date.now()
|
|
102
|
+
});
|
|
103
|
+
console.log(`[AccountManager] Loaded default account: ${this.#accounts[0].email}`);
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error('[AccountManager] Failed to load default account:', error.message);
|
|
107
|
+
// Create empty account list - will fail on first request
|
|
108
|
+
this.#accounts = [];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Extract token from Antigravity's SQLite database
|
|
114
|
+
*/
|
|
115
|
+
#extractTokenFromDB(dbPath = ANTIGRAVITY_DB_PATH) {
|
|
116
|
+
const result = execSync(
|
|
117
|
+
`sqlite3 "${dbPath}" "SELECT value FROM ItemTable WHERE key = 'antigravityAuthStatus';"`,
|
|
118
|
+
{ encoding: 'utf-8', timeout: 5000 }
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
if (!result || !result.trim()) {
|
|
122
|
+
throw new Error('No auth status found in database');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return JSON.parse(result.trim());
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get the number of accounts
|
|
130
|
+
* @returns {number} Number of configured accounts
|
|
131
|
+
*/
|
|
132
|
+
getAccountCount() {
|
|
133
|
+
return this.#accounts.length;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Check if all accounts are rate-limited
|
|
138
|
+
* @returns {boolean} True if all accounts are rate-limited
|
|
139
|
+
*/
|
|
140
|
+
isAllRateLimited() {
|
|
141
|
+
if (this.#accounts.length === 0) return true;
|
|
142
|
+
return this.#accounts.every(acc => acc.isRateLimited);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get list of available (non-rate-limited, non-invalid) accounts
|
|
147
|
+
* @returns {Array<Object>} Array of available account objects
|
|
148
|
+
*/
|
|
149
|
+
getAvailableAccounts() {
|
|
150
|
+
return this.#accounts.filter(acc => !acc.isRateLimited && !acc.isInvalid);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get list of invalid accounts
|
|
155
|
+
* @returns {Array<Object>} Array of invalid account objects
|
|
156
|
+
*/
|
|
157
|
+
getInvalidAccounts() {
|
|
158
|
+
return this.#accounts.filter(acc => acc.isInvalid);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Clear expired rate limits
|
|
163
|
+
* @returns {number} Number of rate limits cleared
|
|
164
|
+
*/
|
|
165
|
+
clearExpiredLimits() {
|
|
166
|
+
const now = Date.now();
|
|
167
|
+
let cleared = 0;
|
|
168
|
+
|
|
169
|
+
for (const account of this.#accounts) {
|
|
170
|
+
if (account.isRateLimited && account.rateLimitResetTime && account.rateLimitResetTime <= now) {
|
|
171
|
+
account.isRateLimited = false;
|
|
172
|
+
account.rateLimitResetTime = null;
|
|
173
|
+
cleared++;
|
|
174
|
+
console.log(`[AccountManager] Rate limit expired for: ${account.email}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (cleared > 0) {
|
|
179
|
+
this.saveToDisk();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return cleared;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Clear all rate limits to force a fresh check
|
|
187
|
+
* (Optimistic retry strategy)
|
|
188
|
+
* @returns {void}
|
|
189
|
+
*/
|
|
190
|
+
resetAllRateLimits() {
|
|
191
|
+
for (const account of this.#accounts) {
|
|
192
|
+
account.isRateLimited = false;
|
|
193
|
+
// distinct from "clearing" expired limits, we blindly reset here
|
|
194
|
+
// we keep the time? User said "clear isRateLimited value, and rateLimitResetTime"
|
|
195
|
+
// So we clear both.
|
|
196
|
+
account.rateLimitResetTime = null;
|
|
197
|
+
}
|
|
198
|
+
console.log('[AccountManager] Reset all rate limits for optimistic retry');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Pick the next available account (fallback when current is unavailable).
|
|
203
|
+
* Sets activeIndex to the selected account's index.
|
|
204
|
+
* @returns {Object|null} The next available account or null if none available
|
|
205
|
+
*/
|
|
206
|
+
pickNext() {
|
|
207
|
+
this.clearExpiredLimits();
|
|
208
|
+
|
|
209
|
+
const available = this.getAvailableAccounts();
|
|
210
|
+
if (available.length === 0) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Clamp index to valid range
|
|
215
|
+
if (this.#currentIndex >= this.#accounts.length) {
|
|
216
|
+
this.#currentIndex = 0;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Find next available account starting from index AFTER current
|
|
220
|
+
for (let i = 1; i <= this.#accounts.length; i++) {
|
|
221
|
+
const idx = (this.#currentIndex + i) % this.#accounts.length;
|
|
222
|
+
const account = this.#accounts[idx];
|
|
223
|
+
|
|
224
|
+
if (!account.isRateLimited && !account.isInvalid) {
|
|
225
|
+
// Set activeIndex to this account (not +1)
|
|
226
|
+
this.#currentIndex = idx;
|
|
227
|
+
account.lastUsed = Date.now();
|
|
228
|
+
|
|
229
|
+
const position = idx + 1;
|
|
230
|
+
const total = this.#accounts.length;
|
|
231
|
+
console.log(`[AccountManager] Using account: ${account.email} (${position}/${total})`);
|
|
232
|
+
|
|
233
|
+
// Persist the change (don't await to avoid blocking)
|
|
234
|
+
this.saveToDisk();
|
|
235
|
+
|
|
236
|
+
return account;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get the current account without advancing the index (sticky selection).
|
|
245
|
+
* Used for cache continuity - sticks to the same account until rate-limited.
|
|
246
|
+
* @returns {Object|null} The current account or null if unavailable/rate-limited
|
|
247
|
+
*/
|
|
248
|
+
getCurrentStickyAccount() {
|
|
249
|
+
this.clearExpiredLimits();
|
|
250
|
+
|
|
251
|
+
if (this.#accounts.length === 0) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Clamp index to valid range
|
|
256
|
+
if (this.#currentIndex >= this.#accounts.length) {
|
|
257
|
+
this.#currentIndex = 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Get current account directly (activeIndex = current account)
|
|
261
|
+
const account = this.#accounts[this.#currentIndex];
|
|
262
|
+
|
|
263
|
+
// Return if available
|
|
264
|
+
if (account && !account.isRateLimited && !account.isInvalid) {
|
|
265
|
+
account.lastUsed = Date.now();
|
|
266
|
+
// Persist the change (don't await to avoid blocking)
|
|
267
|
+
this.saveToDisk();
|
|
268
|
+
return account;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Check if we should wait for the current account's rate limit to reset.
|
|
276
|
+
* Used for sticky account selection - wait if rate limit is short (≤ threshold).
|
|
277
|
+
* @returns {{shouldWait: boolean, waitMs: number, account: Object|null}}
|
|
278
|
+
*/
|
|
279
|
+
shouldWaitForCurrentAccount() {
|
|
280
|
+
if (this.#accounts.length === 0) {
|
|
281
|
+
return { shouldWait: false, waitMs: 0, account: null };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Clamp index to valid range
|
|
285
|
+
if (this.#currentIndex >= this.#accounts.length) {
|
|
286
|
+
this.#currentIndex = 0;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Get current account directly (activeIndex = current account)
|
|
290
|
+
const account = this.#accounts[this.#currentIndex];
|
|
291
|
+
|
|
292
|
+
if (!account || account.isInvalid) {
|
|
293
|
+
return { shouldWait: false, waitMs: 0, account: null };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (account.isRateLimited && account.rateLimitResetTime) {
|
|
297
|
+
const waitMs = account.rateLimitResetTime - Date.now();
|
|
298
|
+
|
|
299
|
+
// If wait time is within threshold, recommend waiting
|
|
300
|
+
if (waitMs > 0 && waitMs <= MAX_WAIT_BEFORE_ERROR_MS) {
|
|
301
|
+
return { shouldWait: true, waitMs, account };
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return { shouldWait: false, waitMs: 0, account };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Pick an account with sticky selection preference.
|
|
310
|
+
* Prefers the current account for cache continuity, only switches when:
|
|
311
|
+
* - Current account is rate-limited for > 2 minutes
|
|
312
|
+
* - Current account is invalid
|
|
313
|
+
* @returns {{account: Object|null, waitMs: number}} Account to use and optional wait time
|
|
314
|
+
*/
|
|
315
|
+
pickStickyAccount() {
|
|
316
|
+
// First try to get the current sticky account
|
|
317
|
+
const stickyAccount = this.getCurrentStickyAccount();
|
|
318
|
+
if (stickyAccount) {
|
|
319
|
+
return { account: stickyAccount, waitMs: 0 };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Check if we should wait for current account
|
|
323
|
+
const waitInfo = this.shouldWaitForCurrentAccount();
|
|
324
|
+
if (waitInfo.shouldWait) {
|
|
325
|
+
console.log(`[AccountManager] Waiting ${formatDuration(waitInfo.waitMs)} for sticky account: ${waitInfo.account.email}`);
|
|
326
|
+
return { account: null, waitMs: waitInfo.waitMs };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Current account unavailable for too long, switch to next available
|
|
330
|
+
const nextAccount = this.pickNext();
|
|
331
|
+
if (nextAccount) {
|
|
332
|
+
console.log(`[AccountManager] Switched to new account for cache: ${nextAccount.email}`);
|
|
333
|
+
}
|
|
334
|
+
return { account: nextAccount, waitMs: 0 };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Mark an account as rate-limited
|
|
339
|
+
* @param {string} email - Email of the account to mark
|
|
340
|
+
* @param {number|null} resetMs - Time in ms until rate limit resets (optional)
|
|
341
|
+
*/
|
|
342
|
+
markRateLimited(email, resetMs = null) {
|
|
343
|
+
const account = this.#accounts.find(a => a.email === email);
|
|
344
|
+
if (!account) return;
|
|
345
|
+
|
|
346
|
+
account.isRateLimited = true;
|
|
347
|
+
const cooldownMs = resetMs || this.#settings.cooldownDurationMs || DEFAULT_COOLDOWN_MS;
|
|
348
|
+
account.rateLimitResetTime = Date.now() + cooldownMs;
|
|
349
|
+
|
|
350
|
+
console.log(
|
|
351
|
+
`[AccountManager] Rate limited: ${email}. Available in ${formatDuration(cooldownMs)}`
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
this.saveToDisk();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Mark an account as invalid (credentials need re-authentication)
|
|
359
|
+
* @param {string} email - Email of the account to mark
|
|
360
|
+
* @param {string} reason - Reason for marking as invalid
|
|
361
|
+
*/
|
|
362
|
+
markInvalid(email, reason = 'Unknown error') {
|
|
363
|
+
const account = this.#accounts.find(a => a.email === email);
|
|
364
|
+
if (!account) return;
|
|
365
|
+
|
|
366
|
+
account.isInvalid = true;
|
|
367
|
+
account.invalidReason = reason;
|
|
368
|
+
account.invalidAt = Date.now();
|
|
369
|
+
|
|
370
|
+
console.log(
|
|
371
|
+
`[AccountManager] ⚠ Account INVALID: ${email}`
|
|
372
|
+
);
|
|
373
|
+
console.log(
|
|
374
|
+
`[AccountManager] Reason: ${reason}`
|
|
375
|
+
);
|
|
376
|
+
console.log(
|
|
377
|
+
`[AccountManager] Run 'npm run accounts' to re-authenticate this account`
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
this.saveToDisk();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Get the minimum wait time until any account becomes available
|
|
385
|
+
* @returns {number} Wait time in milliseconds
|
|
386
|
+
*/
|
|
387
|
+
getMinWaitTimeMs() {
|
|
388
|
+
if (!this.isAllRateLimited()) return 0;
|
|
389
|
+
|
|
390
|
+
const now = Date.now();
|
|
391
|
+
let minWait = Infinity;
|
|
392
|
+
let soonestAccount = null;
|
|
393
|
+
|
|
394
|
+
for (const account of this.#accounts) {
|
|
395
|
+
if (account.rateLimitResetTime) {
|
|
396
|
+
const wait = account.rateLimitResetTime - now;
|
|
397
|
+
if (wait > 0 && wait < minWait) {
|
|
398
|
+
minWait = wait;
|
|
399
|
+
soonestAccount = account;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (soonestAccount) {
|
|
405
|
+
console.log(`[AccountManager] Shortest wait: ${formatDuration(minWait)} (account: ${soonestAccount.email})`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return minWait === Infinity ? DEFAULT_COOLDOWN_MS : minWait;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Get OAuth token for an account
|
|
413
|
+
* @param {Object} account - Account object with email and credentials
|
|
414
|
+
* @returns {Promise<string>} OAuth access token
|
|
415
|
+
* @throws {Error} If token refresh fails
|
|
416
|
+
*/
|
|
417
|
+
async getTokenForAccount(account) {
|
|
418
|
+
// Check cache first
|
|
419
|
+
const cached = this.#tokenCache.get(account.email);
|
|
420
|
+
if (cached && (Date.now() - cached.extractedAt) < TOKEN_REFRESH_INTERVAL_MS) {
|
|
421
|
+
return cached.token;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Get fresh token based on source
|
|
425
|
+
let token;
|
|
426
|
+
|
|
427
|
+
if (account.source === 'oauth' && account.refreshToken) {
|
|
428
|
+
// OAuth account - use refresh token to get new access token
|
|
429
|
+
try {
|
|
430
|
+
const tokens = await refreshAccessToken(account.refreshToken);
|
|
431
|
+
token = tokens.accessToken;
|
|
432
|
+
// Clear invalid flag on success
|
|
433
|
+
if (account.isInvalid) {
|
|
434
|
+
account.isInvalid = false;
|
|
435
|
+
account.invalidReason = null;
|
|
436
|
+
await this.saveToDisk();
|
|
437
|
+
}
|
|
438
|
+
console.log(`[AccountManager] Refreshed OAuth token for: ${account.email}`);
|
|
439
|
+
} catch (error) {
|
|
440
|
+
console.error(`[AccountManager] Failed to refresh token for ${account.email}:`, error.message);
|
|
441
|
+
// Mark account as invalid (credentials need re-auth)
|
|
442
|
+
this.markInvalid(account.email, error.message);
|
|
443
|
+
throw new Error(`AUTH_INVALID: ${account.email}: ${error.message}`);
|
|
444
|
+
}
|
|
445
|
+
} else if (account.source === 'manual' && account.apiKey) {
|
|
446
|
+
token = account.apiKey;
|
|
447
|
+
} else {
|
|
448
|
+
// Extract from database
|
|
449
|
+
const dbPath = account.dbPath || ANTIGRAVITY_DB_PATH;
|
|
450
|
+
const authData = this.#extractTokenFromDB(dbPath);
|
|
451
|
+
token = authData.apiKey;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Cache the token
|
|
455
|
+
this.#tokenCache.set(account.email, {
|
|
456
|
+
token,
|
|
457
|
+
extractedAt: Date.now()
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
return token;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Get project ID for an account
|
|
465
|
+
* @param {Object} account - Account object
|
|
466
|
+
* @param {string} token - OAuth access token
|
|
467
|
+
* @returns {Promise<string>} Project ID
|
|
468
|
+
*/
|
|
469
|
+
async getProjectForAccount(account, token) {
|
|
470
|
+
// Check cache first
|
|
471
|
+
const cached = this.#projectCache.get(account.email);
|
|
472
|
+
if (cached) {
|
|
473
|
+
return cached;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// OAuth or manual accounts may have projectId specified
|
|
477
|
+
if (account.projectId) {
|
|
478
|
+
this.#projectCache.set(account.email, account.projectId);
|
|
479
|
+
return account.projectId;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Discover project via loadCodeAssist API
|
|
483
|
+
const project = await this.#discoverProject(token);
|
|
484
|
+
this.#projectCache.set(account.email, project);
|
|
485
|
+
return project;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Discover project ID via Cloud Code API
|
|
490
|
+
*/
|
|
491
|
+
async #discoverProject(token) {
|
|
492
|
+
for (const endpoint of ANTIGRAVITY_ENDPOINT_FALLBACKS) {
|
|
493
|
+
try {
|
|
494
|
+
const response = await fetch(`${endpoint}/v1internal:loadCodeAssist`, {
|
|
495
|
+
method: 'POST',
|
|
496
|
+
headers: {
|
|
497
|
+
'Authorization': `Bearer ${token}`,
|
|
498
|
+
'Content-Type': 'application/json',
|
|
499
|
+
...ANTIGRAVITY_HEADERS
|
|
500
|
+
},
|
|
501
|
+
body: JSON.stringify({
|
|
502
|
+
metadata: {
|
|
503
|
+
ideType: 'IDE_UNSPECIFIED',
|
|
504
|
+
platform: 'PLATFORM_UNSPECIFIED',
|
|
505
|
+
pluginType: 'GEMINI'
|
|
506
|
+
}
|
|
507
|
+
})
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
if (!response.ok) continue;
|
|
511
|
+
|
|
512
|
+
const data = await response.json();
|
|
513
|
+
|
|
514
|
+
if (typeof data.cloudaicompanionProject === 'string') {
|
|
515
|
+
return data.cloudaicompanionProject;
|
|
516
|
+
}
|
|
517
|
+
if (data.cloudaicompanionProject?.id) {
|
|
518
|
+
return data.cloudaicompanionProject.id;
|
|
519
|
+
}
|
|
520
|
+
} catch (error) {
|
|
521
|
+
console.log(`[AccountManager] Project discovery failed at ${endpoint}:`, error.message);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
console.log(`[AccountManager] Using default project: ${DEFAULT_PROJECT_ID}`);
|
|
526
|
+
return DEFAULT_PROJECT_ID;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Clear project cache for an account (useful on auth errors)
|
|
531
|
+
* @param {string|null} email - Email to clear cache for, or null to clear all
|
|
532
|
+
*/
|
|
533
|
+
clearProjectCache(email = null) {
|
|
534
|
+
if (email) {
|
|
535
|
+
this.#projectCache.delete(email);
|
|
536
|
+
} else {
|
|
537
|
+
this.#projectCache.clear();
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Clear token cache for an account (useful on auth errors)
|
|
543
|
+
* @param {string|null} email - Email to clear cache for, or null to clear all
|
|
544
|
+
*/
|
|
545
|
+
clearTokenCache(email = null) {
|
|
546
|
+
if (email) {
|
|
547
|
+
this.#tokenCache.delete(email);
|
|
548
|
+
} else {
|
|
549
|
+
this.#tokenCache.clear();
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Save current state to disk (async)
|
|
555
|
+
* @returns {Promise<void>}
|
|
556
|
+
*/
|
|
557
|
+
async saveToDisk() {
|
|
558
|
+
try {
|
|
559
|
+
// Ensure directory exists
|
|
560
|
+
const dir = dirname(this.#configPath);
|
|
561
|
+
await mkdir(dir, { recursive: true });
|
|
562
|
+
|
|
563
|
+
const config = {
|
|
564
|
+
accounts: this.#accounts.map(acc => ({
|
|
565
|
+
email: acc.email,
|
|
566
|
+
source: acc.source,
|
|
567
|
+
dbPath: acc.dbPath || null,
|
|
568
|
+
refreshToken: acc.source === 'oauth' ? acc.refreshToken : undefined,
|
|
569
|
+
apiKey: acc.source === 'manual' ? acc.apiKey : undefined,
|
|
570
|
+
projectId: acc.projectId || undefined,
|
|
571
|
+
addedAt: acc.addedAt || undefined,
|
|
572
|
+
isRateLimited: acc.isRateLimited,
|
|
573
|
+
rateLimitResetTime: acc.rateLimitResetTime,
|
|
574
|
+
isInvalid: acc.isInvalid || false,
|
|
575
|
+
invalidReason: acc.invalidReason || null,
|
|
576
|
+
lastUsed: acc.lastUsed
|
|
577
|
+
})),
|
|
578
|
+
settings: this.#settings,
|
|
579
|
+
activeIndex: this.#currentIndex
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
await writeFile(this.#configPath, JSON.stringify(config, null, 2));
|
|
583
|
+
} catch (error) {
|
|
584
|
+
console.error('[AccountManager] Failed to save config:', error.message);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Get status object for logging/API
|
|
590
|
+
* @returns {{accounts: Array, settings: Object}} Status object with accounts and settings
|
|
591
|
+
*/
|
|
592
|
+
getStatus() {
|
|
593
|
+
const available = this.getAvailableAccounts();
|
|
594
|
+
const rateLimited = this.#accounts.filter(a => a.isRateLimited);
|
|
595
|
+
const invalid = this.getInvalidAccounts();
|
|
596
|
+
|
|
597
|
+
return {
|
|
598
|
+
total: this.#accounts.length,
|
|
599
|
+
available: available.length,
|
|
600
|
+
rateLimited: rateLimited.length,
|
|
601
|
+
invalid: invalid.length,
|
|
602
|
+
summary: `${this.#accounts.length} total, ${available.length} available, ${rateLimited.length} rate-limited, ${invalid.length} invalid`,
|
|
603
|
+
accounts: this.#accounts.map(a => ({
|
|
604
|
+
email: a.email,
|
|
605
|
+
source: a.source,
|
|
606
|
+
isRateLimited: a.isRateLimited,
|
|
607
|
+
rateLimitResetTime: a.rateLimitResetTime,
|
|
608
|
+
isInvalid: a.isInvalid || false,
|
|
609
|
+
invalidReason: a.invalidReason || null,
|
|
610
|
+
lastUsed: a.lastUsed
|
|
611
|
+
}))
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Get settings
|
|
617
|
+
* @returns {Object} Current settings object
|
|
618
|
+
*/
|
|
619
|
+
getSettings() {
|
|
620
|
+
return { ...this.#settings };
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Get all accounts (internal use for quota fetching)
|
|
625
|
+
* Returns the full account objects including credentials
|
|
626
|
+
* @returns {Array<Object>} Array of account objects
|
|
627
|
+
*/
|
|
628
|
+
getAllAccounts() {
|
|
629
|
+
return this.#accounts;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
export default AccountManager;
|