antigravity-claude-proxy 1.1.5 → 1.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antigravity-claude-proxy",
3
- "version": "1.1.5",
3
+ "version": "1.2.0",
4
4
  "description": "Proxy server to use Antigravity's Claude models with Claude Code CLI",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -14,11 +14,11 @@
14
14
  "scripts": {
15
15
  "start": "node src/index.js",
16
16
  "dev": "node --watch src/index.js",
17
- "accounts": "node src/accounts-cli.js",
18
- "accounts:add": "node src/accounts-cli.js add",
19
- "accounts:list": "node src/accounts-cli.js list",
20
- "accounts:remove": "node src/accounts-cli.js remove",
21
- "accounts:verify": "node src/accounts-cli.js verify",
17
+ "accounts": "node src/cli/accounts.js",
18
+ "accounts:add": "node src/cli/accounts.js add",
19
+ "accounts:list": "node src/cli/accounts.js list",
20
+ "accounts:remove": "node src/cli/accounts.js remove",
21
+ "accounts:verify": "node src/cli/accounts.js verify",
22
22
  "test": "node tests/run-all.cjs",
23
23
  "test:signatures": "node tests/test-thinking-signatures.cjs",
24
24
  "test:multiturn": "node tests/test-multiturn-thinking-tools.cjs",
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Credentials Management
3
+ *
4
+ * Handles OAuth token handling and project discovery.
5
+ */
6
+
7
+ import {
8
+ ANTIGRAVITY_DB_PATH,
9
+ TOKEN_REFRESH_INTERVAL_MS,
10
+ ANTIGRAVITY_ENDPOINT_FALLBACKS,
11
+ ANTIGRAVITY_HEADERS,
12
+ DEFAULT_PROJECT_ID
13
+ } from '../constants.js';
14
+ import { refreshAccessToken } from '../auth/oauth.js';
15
+ import { getAuthStatus } from '../auth/database.js';
16
+ import { logger } from '../utils/logger.js';
17
+
18
+ /**
19
+ * Get OAuth token for an account
20
+ *
21
+ * @param {Object} account - Account object with email and credentials
22
+ * @param {Map} tokenCache - Token cache map
23
+ * @param {Function} onInvalid - Callback when account is invalid (email, reason)
24
+ * @param {Function} onSave - Callback to save changes
25
+ * @returns {Promise<string>} OAuth access token
26
+ * @throws {Error} If token refresh fails
27
+ */
28
+ export async function getTokenForAccount(account, tokenCache, onInvalid, onSave) {
29
+ // Check cache first
30
+ const cached = tokenCache.get(account.email);
31
+ if (cached && (Date.now() - cached.extractedAt) < TOKEN_REFRESH_INTERVAL_MS) {
32
+ return cached.token;
33
+ }
34
+
35
+ // Get fresh token based on source
36
+ let token;
37
+
38
+ if (account.source === 'oauth' && account.refreshToken) {
39
+ // OAuth account - use refresh token to get new access token
40
+ try {
41
+ const tokens = await refreshAccessToken(account.refreshToken);
42
+ token = tokens.accessToken;
43
+ // Clear invalid flag on success
44
+ if (account.isInvalid) {
45
+ account.isInvalid = false;
46
+ account.invalidReason = null;
47
+ if (onSave) await onSave();
48
+ }
49
+ logger.success(`[AccountManager] Refreshed OAuth token for: ${account.email}`);
50
+ } catch (error) {
51
+ logger.error(`[AccountManager] Failed to refresh token for ${account.email}:`, error.message);
52
+ // Mark account as invalid (credentials need re-auth)
53
+ if (onInvalid) onInvalid(account.email, error.message);
54
+ throw new Error(`AUTH_INVALID: ${account.email}: ${error.message}`);
55
+ }
56
+ } else if (account.source === 'manual' && account.apiKey) {
57
+ token = account.apiKey;
58
+ } else {
59
+ // Extract from database
60
+ const dbPath = account.dbPath || ANTIGRAVITY_DB_PATH;
61
+ const authData = getAuthStatus(dbPath);
62
+ token = authData.apiKey;
63
+ }
64
+
65
+ // Cache the token
66
+ tokenCache.set(account.email, {
67
+ token,
68
+ extractedAt: Date.now()
69
+ });
70
+
71
+ return token;
72
+ }
73
+
74
+ /**
75
+ * Get project ID for an account
76
+ *
77
+ * @param {Object} account - Account object
78
+ * @param {string} token - OAuth access token
79
+ * @param {Map} projectCache - Project cache map
80
+ * @returns {Promise<string>} Project ID
81
+ */
82
+ export async function getProjectForAccount(account, token, projectCache) {
83
+ // Check cache first
84
+ const cached = projectCache.get(account.email);
85
+ if (cached) {
86
+ return cached;
87
+ }
88
+
89
+ // OAuth or manual accounts may have projectId specified
90
+ if (account.projectId) {
91
+ projectCache.set(account.email, account.projectId);
92
+ return account.projectId;
93
+ }
94
+
95
+ // Discover project via loadCodeAssist API
96
+ const project = await discoverProject(token);
97
+ projectCache.set(account.email, project);
98
+ return project;
99
+ }
100
+
101
+ /**
102
+ * Discover project ID via Cloud Code API
103
+ *
104
+ * @param {string} token - OAuth access token
105
+ * @returns {Promise<string>} Project ID
106
+ */
107
+ export async function discoverProject(token) {
108
+ for (const endpoint of ANTIGRAVITY_ENDPOINT_FALLBACKS) {
109
+ try {
110
+ const response = await fetch(`${endpoint}/v1internal:loadCodeAssist`, {
111
+ method: 'POST',
112
+ headers: {
113
+ 'Authorization': `Bearer ${token}`,
114
+ 'Content-Type': 'application/json',
115
+ ...ANTIGRAVITY_HEADERS
116
+ },
117
+ body: JSON.stringify({
118
+ metadata: {
119
+ ideType: 'IDE_UNSPECIFIED',
120
+ platform: 'PLATFORM_UNSPECIFIED',
121
+ pluginType: 'GEMINI'
122
+ }
123
+ })
124
+ });
125
+
126
+ if (!response.ok) continue;
127
+
128
+ const data = await response.json();
129
+
130
+ if (typeof data.cloudaicompanionProject === 'string') {
131
+ return data.cloudaicompanionProject;
132
+ }
133
+ if (data.cloudaicompanionProject?.id) {
134
+ return data.cloudaicompanionProject.id;
135
+ }
136
+ } catch (error) {
137
+ logger.warn(`[AccountManager] Project discovery failed at ${endpoint}:`, error.message);
138
+ }
139
+ }
140
+
141
+ logger.info(`[AccountManager] Using default project: ${DEFAULT_PROJECT_ID}`);
142
+ return DEFAULT_PROJECT_ID;
143
+ }
144
+
145
+ /**
146
+ * Clear project cache for an account
147
+ *
148
+ * @param {Map} projectCache - Project cache map
149
+ * @param {string|null} email - Email to clear cache for, or null to clear all
150
+ */
151
+ export function clearProjectCache(projectCache, email = null) {
152
+ if (email) {
153
+ projectCache.delete(email);
154
+ } else {
155
+ projectCache.clear();
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Clear token cache for an account
161
+ *
162
+ * @param {Map} tokenCache - Token cache map
163
+ * @param {string|null} email - Email to clear cache for, or null to clear all
164
+ */
165
+ export function clearTokenCache(tokenCache, email = null) {
166
+ if (email) {
167
+ tokenCache.delete(email);
168
+ } else {
169
+ tokenCache.clear();
170
+ }
171
+ }
@@ -0,0 +1,293 @@
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 { ACCOUNT_CONFIG_PATH } from '../constants.js';
8
+ import { loadAccounts, loadDefaultAccount, saveAccounts } from './storage.js';
9
+ import {
10
+ isAllRateLimited as checkAllRateLimited,
11
+ getAvailableAccounts as getAvailable,
12
+ getInvalidAccounts as getInvalid,
13
+ clearExpiredLimits as clearLimits,
14
+ resetAllRateLimits as resetLimits,
15
+ markRateLimited as markLimited,
16
+ markInvalid as markAccountInvalid,
17
+ getMinWaitTimeMs as getMinWait
18
+ } from './rate-limits.js';
19
+ import {
20
+ getTokenForAccount as fetchToken,
21
+ getProjectForAccount as fetchProject,
22
+ clearProjectCache as clearProject,
23
+ clearTokenCache as clearToken
24
+ } from './credentials.js';
25
+ import {
26
+ pickNext as selectNext,
27
+ getCurrentStickyAccount as getSticky,
28
+ shouldWaitForCurrentAccount as shouldWait,
29
+ pickStickyAccount as selectSticky
30
+ } from './selection.js';
31
+ import { logger } from '../utils/logger.js';
32
+
33
+ export class AccountManager {
34
+ #accounts = [];
35
+ #currentIndex = 0;
36
+ #configPath;
37
+ #settings = {};
38
+ #initialized = false;
39
+
40
+ // Per-account caches
41
+ #tokenCache = new Map(); // email -> { token, extractedAt }
42
+ #projectCache = new Map(); // email -> projectId
43
+
44
+ constructor(configPath = ACCOUNT_CONFIG_PATH) {
45
+ this.#configPath = configPath;
46
+ }
47
+
48
+ /**
49
+ * Initialize the account manager by loading config
50
+ */
51
+ async initialize() {
52
+ if (this.#initialized) return;
53
+
54
+ const { accounts, settings, activeIndex } = await loadAccounts(this.#configPath);
55
+
56
+ this.#accounts = accounts;
57
+ this.#settings = settings;
58
+ this.#currentIndex = activeIndex;
59
+
60
+ // If config exists but has no accounts, fall back to Antigravity database
61
+ if (this.#accounts.length === 0) {
62
+ logger.warn('[AccountManager] No accounts in config. Falling back to Antigravity database');
63
+ const { accounts: defaultAccounts, tokenCache } = loadDefaultAccount();
64
+ this.#accounts = defaultAccounts;
65
+ this.#tokenCache = tokenCache;
66
+ }
67
+
68
+ // Clear any expired rate limits
69
+ this.clearExpiredLimits();
70
+
71
+ this.#initialized = true;
72
+ }
73
+
74
+ /**
75
+ * Get the number of accounts
76
+ * @returns {number} Number of configured accounts
77
+ */
78
+ getAccountCount() {
79
+ return this.#accounts.length;
80
+ }
81
+
82
+ /**
83
+ * Check if all accounts are rate-limited
84
+ * @returns {boolean} True if all accounts are rate-limited
85
+ */
86
+ isAllRateLimited() {
87
+ return checkAllRateLimited(this.#accounts);
88
+ }
89
+
90
+ /**
91
+ * Get list of available (non-rate-limited, non-invalid) accounts
92
+ * @returns {Array<Object>} Array of available account objects
93
+ */
94
+ getAvailableAccounts() {
95
+ return getAvailable(this.#accounts);
96
+ }
97
+
98
+ /**
99
+ * Get list of invalid accounts
100
+ * @returns {Array<Object>} Array of invalid account objects
101
+ */
102
+ getInvalidAccounts() {
103
+ return getInvalid(this.#accounts);
104
+ }
105
+
106
+ /**
107
+ * Clear expired rate limits
108
+ * @returns {number} Number of rate limits cleared
109
+ */
110
+ clearExpiredLimits() {
111
+ const cleared = clearLimits(this.#accounts);
112
+ if (cleared > 0) {
113
+ this.saveToDisk();
114
+ }
115
+ return cleared;
116
+ }
117
+
118
+ /**
119
+ * Clear all rate limits to force a fresh check
120
+ * (Optimistic retry strategy)
121
+ * @returns {void}
122
+ */
123
+ resetAllRateLimits() {
124
+ resetLimits(this.#accounts);
125
+ }
126
+
127
+ /**
128
+ * Pick the next available account (fallback when current is unavailable).
129
+ * Sets activeIndex to the selected account's index.
130
+ * @returns {Object|null} The next available account or null if none available
131
+ */
132
+ pickNext() {
133
+ const { account, newIndex } = selectNext(this.#accounts, this.#currentIndex, () => this.saveToDisk());
134
+ this.#currentIndex = newIndex;
135
+ return account;
136
+ }
137
+
138
+ /**
139
+ * Get the current account without advancing the index (sticky selection).
140
+ * Used for cache continuity - sticks to the same account until rate-limited.
141
+ * @returns {Object|null} The current account or null if unavailable/rate-limited
142
+ */
143
+ getCurrentStickyAccount() {
144
+ const { account, newIndex } = getSticky(this.#accounts, this.#currentIndex, () => this.saveToDisk());
145
+ this.#currentIndex = newIndex;
146
+ return account;
147
+ }
148
+
149
+ /**
150
+ * Check if we should wait for the current account's rate limit to reset.
151
+ * Used for sticky account selection - wait if rate limit is short (≤ threshold).
152
+ * @returns {{shouldWait: boolean, waitMs: number, account: Object|null}}
153
+ */
154
+ shouldWaitForCurrentAccount() {
155
+ return shouldWait(this.#accounts, this.#currentIndex);
156
+ }
157
+
158
+ /**
159
+ * Pick an account with sticky selection preference.
160
+ * Prefers the current account for cache continuity, only switches when:
161
+ * - Current account is rate-limited for > 2 minutes
162
+ * - Current account is invalid
163
+ * @returns {{account: Object|null, waitMs: number}} Account to use and optional wait time
164
+ */
165
+ pickStickyAccount() {
166
+ const { account, waitMs, newIndex } = selectSticky(this.#accounts, this.#currentIndex, () => this.saveToDisk());
167
+ this.#currentIndex = newIndex;
168
+ return { account, waitMs };
169
+ }
170
+
171
+ /**
172
+ * Mark an account as rate-limited
173
+ * @param {string} email - Email of the account to mark
174
+ * @param {number|null} resetMs - Time in ms until rate limit resets (optional)
175
+ */
176
+ markRateLimited(email, resetMs = null) {
177
+ markLimited(this.#accounts, email, resetMs, this.#settings);
178
+ this.saveToDisk();
179
+ }
180
+
181
+ /**
182
+ * Mark an account as invalid (credentials need re-authentication)
183
+ * @param {string} email - Email of the account to mark
184
+ * @param {string} reason - Reason for marking as invalid
185
+ */
186
+ markInvalid(email, reason = 'Unknown error') {
187
+ markAccountInvalid(this.#accounts, email, reason);
188
+ this.saveToDisk();
189
+ }
190
+
191
+ /**
192
+ * Get the minimum wait time until any account becomes available
193
+ * @returns {number} Wait time in milliseconds
194
+ */
195
+ getMinWaitTimeMs() {
196
+ return getMinWait(this.#accounts);
197
+ }
198
+
199
+ /**
200
+ * Get OAuth token for an account
201
+ * @param {Object} account - Account object with email and credentials
202
+ * @returns {Promise<string>} OAuth access token
203
+ * @throws {Error} If token refresh fails
204
+ */
205
+ async getTokenForAccount(account) {
206
+ return fetchToken(
207
+ account,
208
+ this.#tokenCache,
209
+ (email, reason) => this.markInvalid(email, reason),
210
+ () => this.saveToDisk()
211
+ );
212
+ }
213
+
214
+ /**
215
+ * Get project ID for an account
216
+ * @param {Object} account - Account object
217
+ * @param {string} token - OAuth access token
218
+ * @returns {Promise<string>} Project ID
219
+ */
220
+ async getProjectForAccount(account, token) {
221
+ return fetchProject(account, token, this.#projectCache);
222
+ }
223
+
224
+ /**
225
+ * Clear project cache for an account (useful on auth errors)
226
+ * @param {string|null} email - Email to clear cache for, or null to clear all
227
+ */
228
+ clearProjectCache(email = null) {
229
+ clearProject(this.#projectCache, email);
230
+ }
231
+
232
+ /**
233
+ * Clear token cache for an account (useful on auth errors)
234
+ * @param {string|null} email - Email to clear cache for, or null to clear all
235
+ */
236
+ clearTokenCache(email = null) {
237
+ clearToken(this.#tokenCache, email);
238
+ }
239
+
240
+ /**
241
+ * Save current state to disk (async)
242
+ * @returns {Promise<void>}
243
+ */
244
+ async saveToDisk() {
245
+ await saveAccounts(this.#configPath, this.#accounts, this.#settings, this.#currentIndex);
246
+ }
247
+
248
+ /**
249
+ * Get status object for logging/API
250
+ * @returns {{accounts: Array, settings: Object}} Status object with accounts and settings
251
+ */
252
+ getStatus() {
253
+ const available = this.getAvailableAccounts();
254
+ const rateLimited = this.#accounts.filter(a => a.isRateLimited);
255
+ const invalid = this.getInvalidAccounts();
256
+
257
+ return {
258
+ total: this.#accounts.length,
259
+ available: available.length,
260
+ rateLimited: rateLimited.length,
261
+ invalid: invalid.length,
262
+ summary: `${this.#accounts.length} total, ${available.length} available, ${rateLimited.length} rate-limited, ${invalid.length} invalid`,
263
+ accounts: this.#accounts.map(a => ({
264
+ email: a.email,
265
+ source: a.source,
266
+ isRateLimited: a.isRateLimited,
267
+ rateLimitResetTime: a.rateLimitResetTime,
268
+ isInvalid: a.isInvalid || false,
269
+ invalidReason: a.invalidReason || null,
270
+ lastUsed: a.lastUsed
271
+ }))
272
+ };
273
+ }
274
+
275
+ /**
276
+ * Get settings
277
+ * @returns {Object} Current settings object
278
+ */
279
+ getSettings() {
280
+ return { ...this.#settings };
281
+ }
282
+
283
+ /**
284
+ * Get all accounts (internal use for quota fetching)
285
+ * Returns the full account objects including credentials
286
+ * @returns {Array<Object>} Array of account objects
287
+ */
288
+ getAllAccounts() {
289
+ return this.#accounts;
290
+ }
291
+ }
292
+
293
+ export default AccountManager;
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Rate Limit Management
3
+ *
4
+ * Handles rate limit tracking and state management for accounts.
5
+ */
6
+
7
+ import { DEFAULT_COOLDOWN_MS } from '../constants.js';
8
+ import { formatDuration } from '../utils/helpers.js';
9
+ import { logger } from '../utils/logger.js';
10
+
11
+ /**
12
+ * Check if all accounts are rate-limited
13
+ *
14
+ * @param {Array} accounts - Array of account objects
15
+ * @returns {boolean} True if all accounts are rate-limited
16
+ */
17
+ export function isAllRateLimited(accounts) {
18
+ if (accounts.length === 0) return true;
19
+ return accounts.every(acc => acc.isRateLimited);
20
+ }
21
+
22
+ /**
23
+ * Get list of available (non-rate-limited, non-invalid) accounts
24
+ *
25
+ * @param {Array} accounts - Array of account objects
26
+ * @returns {Array} Array of available account objects
27
+ */
28
+ export function getAvailableAccounts(accounts) {
29
+ return accounts.filter(acc => !acc.isRateLimited && !acc.isInvalid);
30
+ }
31
+
32
+ /**
33
+ * Get list of invalid accounts
34
+ *
35
+ * @param {Array} accounts - Array of account objects
36
+ * @returns {Array} Array of invalid account objects
37
+ */
38
+ export function getInvalidAccounts(accounts) {
39
+ return accounts.filter(acc => acc.isInvalid);
40
+ }
41
+
42
+ /**
43
+ * Clear expired rate limits
44
+ *
45
+ * @param {Array} accounts - Array of account objects
46
+ * @returns {number} Number of rate limits cleared
47
+ */
48
+ export function clearExpiredLimits(accounts) {
49
+ const now = Date.now();
50
+ let cleared = 0;
51
+
52
+ for (const account of accounts) {
53
+ if (account.isRateLimited && account.rateLimitResetTime && account.rateLimitResetTime <= now) {
54
+ account.rateLimitResetTime = null;
55
+ cleared++;
56
+ logger.success(`[AccountManager] Rate limit expired for: ${account.email}`);
57
+ }
58
+ }
59
+
60
+ return cleared;
61
+ }
62
+
63
+ /**
64
+ * Clear all rate limits to force a fresh check (optimistic retry strategy)
65
+ *
66
+ * @param {Array} accounts - Array of account objects
67
+ */
68
+ export function resetAllRateLimits(accounts) {
69
+ for (const account of accounts) {
70
+ account.isRateLimited = false;
71
+ account.rateLimitResetTime = null;
72
+ }
73
+ logger.warn('[AccountManager] Reset all rate limits for optimistic retry');
74
+ }
75
+
76
+ /**
77
+ * Mark an account as rate-limited
78
+ *
79
+ * @param {Array} accounts - Array of account objects
80
+ * @param {string} email - Email of the account to mark
81
+ * @param {number|null} resetMs - Time in ms until rate limit resets (optional)
82
+ * @param {Object} settings - Settings object with cooldownDurationMs
83
+ * @returns {boolean} True if account was found and marked
84
+ */
85
+ export function markRateLimited(accounts, email, resetMs = null, settings = {}) {
86
+ const account = accounts.find(a => a.email === email);
87
+ if (!account) return false;
88
+
89
+ account.isRateLimited = true;
90
+ const cooldownMs = resetMs || settings.cooldownDurationMs || DEFAULT_COOLDOWN_MS;
91
+ account.rateLimitResetTime = Date.now() + cooldownMs;
92
+
93
+ logger.warn(
94
+ `[AccountManager] Rate limited: ${email}. Available in ${formatDuration(cooldownMs)}`
95
+ );
96
+
97
+ return true;
98
+ }
99
+
100
+ /**
101
+ * Mark an account as invalid (credentials need re-authentication)
102
+ *
103
+ * @param {Array} accounts - Array of account objects
104
+ * @param {string} email - Email of the account to mark
105
+ * @param {string} reason - Reason for marking as invalid
106
+ * @returns {boolean} True if account was found and marked
107
+ */
108
+ export function markInvalid(accounts, email, reason = 'Unknown error') {
109
+ const account = accounts.find(a => a.email === email);
110
+ if (!account) return false;
111
+
112
+ account.isInvalid = true;
113
+ account.invalidReason = reason;
114
+ account.invalidAt = Date.now();
115
+
116
+ logger.error(
117
+ `[AccountManager] ⚠ Account INVALID: ${email}`
118
+ );
119
+ logger.error(
120
+ `[AccountManager] Reason: ${reason}`
121
+ );
122
+ logger.error(
123
+ `[AccountManager] Run 'npm run accounts' to re-authenticate this account`
124
+ );
125
+
126
+ return true;
127
+ }
128
+
129
+ /**
130
+ * Get the minimum wait time until any account becomes available
131
+ *
132
+ * @param {Array} accounts - Array of account objects
133
+ * @returns {number} Wait time in milliseconds
134
+ */
135
+ export function getMinWaitTimeMs(accounts) {
136
+ if (!isAllRateLimited(accounts)) return 0;
137
+
138
+ const now = Date.now();
139
+ let minWait = Infinity;
140
+ let soonestAccount = null;
141
+
142
+ for (const account of accounts) {
143
+ if (account.rateLimitResetTime) {
144
+ const wait = account.rateLimitResetTime - now;
145
+ if (wait > 0 && wait < minWait) {
146
+ minWait = wait;
147
+ soonestAccount = account;
148
+ }
149
+ }
150
+ }
151
+
152
+ if (soonestAccount) {
153
+ logger.info(`[AccountManager] Shortest wait: ${formatDuration(minWait)} (account: ${soonestAccount.email})`);
154
+ }
155
+
156
+ return minWait === Infinity ? DEFAULT_COOLDOWN_MS : minWait;
157
+ }