antigravity-claude-proxy 1.2.6 → 1.2.7

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 CHANGED
@@ -69,7 +69,9 @@ If you have Antigravity installed and logged in, the proxy will automatically ex
69
69
 
70
70
  **Option B: Add Google Accounts via OAuth (Recommended for Multi-Account)**
71
71
 
72
- Add one or more Google accounts for load balancing:
72
+ Add one or more Google accounts for load balancing.
73
+
74
+ #### Desktop/Laptop (with browser)
73
75
 
74
76
  ```bash
75
77
  # If installed via npm
@@ -84,7 +86,22 @@ npm run accounts:add
84
86
 
85
87
  This opens your browser for Google OAuth. Sign in and authorize access. Repeat for multiple accounts.
86
88
 
87
- Manage accounts:
89
+ #### Headless Server (Docker, SSH, no desktop)
90
+
91
+ ```bash
92
+ # If installed via npm
93
+ antigravity-claude-proxy accounts add --no-browser
94
+
95
+ # If using npx
96
+ npx antigravity-claude-proxy accounts add -- --no-browser
97
+
98
+ # If cloned locally
99
+ npm run accounts:add -- --no-browser
100
+ ```
101
+
102
+ This displays an OAuth URL you can open on another device (phone/laptop). After signing in, copy the redirect URL or authorization code and paste it back into the terminal.
103
+
104
+ #### Manage accounts
88
105
 
89
106
  ```bash
90
107
  # List all accounts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antigravity-claude-proxy",
3
- "version": "1.2.6",
3
+ "version": "1.2.7",
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",
@@ -26,7 +26,8 @@
26
26
  "test:interleaved": "node tests/test-interleaved-thinking.cjs",
27
27
  "test:images": "node tests/test-images.cjs",
28
28
  "test:caching": "node tests/test-caching-streaming.cjs",
29
- "test:crossmodel": "node tests/test-cross-model-thinking.cjs"
29
+ "test:crossmodel": "node tests/test-cross-model-thinking.cjs",
30
+ "test:oauth": "node tests/test-oauth-no-browser.cjs"
30
31
  },
31
32
  "keywords": [
32
33
  "claude",
package/src/auth/oauth.js CHANGED
@@ -57,6 +57,56 @@ export function getAuthorizationUrl() {
57
57
  };
58
58
  }
59
59
 
60
+ /**
61
+ * Extract authorization code and state from user input.
62
+ * User can paste either:
63
+ * - Full callback URL: http://localhost:51121/oauth-callback?code=xxx&state=xxx
64
+ * - Just the code parameter: 4/0xxx...
65
+ *
66
+ * @param {string} input - User input (URL or code)
67
+ * @returns {{code: string, state: string|null}} Extracted code and optional state
68
+ */
69
+ export function extractCodeFromInput(input) {
70
+ if (!input || typeof input !== 'string') {
71
+ throw new Error('No input provided');
72
+ }
73
+
74
+ const trimmed = input.trim();
75
+
76
+ // Check if it looks like a URL
77
+ if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
78
+ try {
79
+ const url = new URL(trimmed);
80
+ const code = url.searchParams.get('code');
81
+ const state = url.searchParams.get('state');
82
+ const error = url.searchParams.get('error');
83
+
84
+ if (error) {
85
+ throw new Error(`OAuth error: ${error}`);
86
+ }
87
+
88
+ if (!code) {
89
+ throw new Error('No authorization code found in URL');
90
+ }
91
+
92
+ return { code, state };
93
+ } catch (e) {
94
+ if (e.message.includes('OAuth error') || e.message.includes('No authorization code')) {
95
+ throw e;
96
+ }
97
+ throw new Error('Invalid URL format');
98
+ }
99
+ }
100
+
101
+ // Assume it's a raw code
102
+ // Google auth codes typically start with "4/" and are long
103
+ if (trimmed.length < 10) {
104
+ throw new Error('Input is too short to be a valid authorization code');
105
+ }
106
+
107
+ return { code: trimmed, state: null };
108
+ }
109
+
60
110
  /**
61
111
  * Start a local server to receive the OAuth callback
62
112
  * Returns a promise that resolves with the authorization code
@@ -338,6 +388,7 @@ export async function completeOAuthFlow(code, verifier) {
338
388
 
339
389
  export default {
340
390
  getAuthorizationUrl,
391
+ extractCodeFromInput,
341
392
  startCallbackServer,
342
393
  exchangeCode,
343
394
  refreshAccessToken,
@@ -25,7 +25,8 @@ import {
25
25
  startCallbackServer,
26
26
  completeOAuthFlow,
27
27
  refreshAccessToken,
28
- getUserEmail
28
+ getUserEmail,
29
+ extractCodeFromInput
29
30
  } from '../auth/oauth.js';
30
31
 
31
32
  const SERVER_PORT = process.env.PORT || DEFAULT_PORT;
@@ -229,6 +230,63 @@ async function addAccount(existingAccounts) {
229
230
  }
230
231
  }
231
232
 
233
+ /**
234
+ * Add a new account via OAuth with manual code input (no-browser mode)
235
+ * For headless servers without a desktop environment
236
+ */
237
+ async function addAccountNoBrowser(existingAccounts, rl) {
238
+ console.log('\n=== Add Google Account (No-Browser Mode) ===\n');
239
+
240
+ // Generate authorization URL
241
+ const { url, verifier, state } = getAuthorizationUrl();
242
+
243
+ console.log('Copy the following URL and open it in a browser on another device:\n');
244
+ console.log(` ${url}\n`);
245
+ console.log('After signing in, you will be redirected to a localhost URL.');
246
+ console.log('Copy the ENTIRE redirect URL or just the authorization code.\n');
247
+
248
+ const input = await rl.question('Paste the callback URL or authorization code: ');
249
+
250
+ try {
251
+ const { code, state: extractedState } = extractCodeFromInput(input);
252
+
253
+ // Validate state if present
254
+ if (extractedState && extractedState !== state) {
255
+ console.log('\n⚠ State mismatch detected. This could indicate a security issue.');
256
+ console.log('Proceeding anyway as this is manual mode...');
257
+ }
258
+
259
+ console.log('\nExchanging authorization code for tokens...');
260
+ const result = await completeOAuthFlow(code, verifier);
261
+
262
+ // Check if account already exists
263
+ const existing = existingAccounts.find(a => a.email === result.email);
264
+ if (existing) {
265
+ console.log(`\n⚠ Account ${result.email} already exists. Updating tokens.`);
266
+ existing.refreshToken = result.refreshToken;
267
+ existing.projectId = result.projectId;
268
+ existing.addedAt = new Date().toISOString();
269
+ return null; // Don't add duplicate
270
+ }
271
+
272
+ console.log(`\n✓ Successfully authenticated: ${result.email}`);
273
+ if (result.projectId) {
274
+ console.log(` Project ID: ${result.projectId}`);
275
+ }
276
+
277
+ return {
278
+ email: result.email,
279
+ refreshToken: result.refreshToken,
280
+ projectId: result.projectId,
281
+ addedAt: new Date().toISOString(),
282
+ modelRateLimits: {}
283
+ };
284
+ } catch (error) {
285
+ console.error(`\n✗ Authentication failed: ${error.message}`);
286
+ return null;
287
+ }
288
+ }
289
+
232
290
  /**
233
291
  * Interactive remove accounts flow
234
292
  */
@@ -275,8 +333,14 @@ async function interactiveRemove(rl) {
275
333
 
276
334
  /**
277
335
  * Interactive add accounts flow (Main Menu)
336
+ * @param {Object} rl - readline interface
337
+ * @param {boolean} noBrowser - if true, use manual code input mode
278
338
  */
279
- async function interactiveAdd(rl) {
339
+ async function interactiveAdd(rl, noBrowser = false) {
340
+ if (noBrowser) {
341
+ console.log('\n📋 No-browser mode: You will manually paste the authorization code.\n');
342
+ }
343
+
280
344
  const accounts = loadAccounts();
281
345
 
282
346
  if (accounts.length > 0) {
@@ -307,7 +371,11 @@ async function interactiveAdd(rl) {
307
371
  return;
308
372
  }
309
373
 
310
- const newAccount = await addAccount(accounts);
374
+ // Use appropriate add function based on mode
375
+ const newAccount = noBrowser
376
+ ? await addAccountNoBrowser(accounts, rl)
377
+ : await addAccount(accounts);
378
+
311
379
  if (newAccount) {
312
380
  accounts.push(newAccount);
313
381
  saveAccounts(accounts);
@@ -388,9 +456,11 @@ async function verifyAccounts() {
388
456
  async function main() {
389
457
  const args = process.argv.slice(2);
390
458
  const command = args[0] || 'add';
459
+ const noBrowser = args.includes('--no-browser');
391
460
 
392
461
  console.log('╔════════════════════════════════════════╗');
393
462
  console.log('║ Antigravity Proxy Account Manager ║');
463
+ console.log('║ Use --no-browser for headless mode ║');
394
464
  console.log('╚════════════════════════════════════════╝');
395
465
 
396
466
  const rl = createRL();
@@ -399,7 +469,7 @@ async function main() {
399
469
  switch (command) {
400
470
  case 'add':
401
471
  await ensureServerStopped();
402
- await interactiveAdd(rl);
472
+ await interactiveAdd(rl, noBrowser);
403
473
  break;
404
474
  case 'list':
405
475
  await listAccounts();
@@ -418,6 +488,8 @@ async function main() {
418
488
  console.log(' node src/cli/accounts.js verify Verify account tokens');
419
489
  console.log(' node src/cli/accounts.js clear Remove all accounts');
420
490
  console.log(' node src/cli/accounts.js help Show this help');
491
+ console.log('\nOptions:');
492
+ console.log(' --no-browser Manual authorization code input (for headless servers)');
421
493
  break;
422
494
  case 'remove':
423
495
  await ensureServerStopped();