@kamel-ahmed/proxy-claude 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.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +622 -0
  3. package/bin/cli.js +124 -0
  4. package/package.json +80 -0
  5. package/public/app.js +228 -0
  6. package/public/css/src/input.css +523 -0
  7. package/public/css/style.css +1 -0
  8. package/public/favicon.svg +10 -0
  9. package/public/index.html +381 -0
  10. package/public/js/components/account-manager.js +245 -0
  11. package/public/js/components/claude-config.js +420 -0
  12. package/public/js/components/dashboard/charts.js +589 -0
  13. package/public/js/components/dashboard/filters.js +362 -0
  14. package/public/js/components/dashboard/stats.js +110 -0
  15. package/public/js/components/dashboard.js +236 -0
  16. package/public/js/components/logs-viewer.js +100 -0
  17. package/public/js/components/models.js +36 -0
  18. package/public/js/components/server-config.js +349 -0
  19. package/public/js/config/constants.js +102 -0
  20. package/public/js/data-store.js +386 -0
  21. package/public/js/settings-store.js +58 -0
  22. package/public/js/store.js +78 -0
  23. package/public/js/translations/en.js +351 -0
  24. package/public/js/translations/id.js +396 -0
  25. package/public/js/translations/pt.js +287 -0
  26. package/public/js/translations/tr.js +342 -0
  27. package/public/js/translations/zh.js +357 -0
  28. package/public/js/utils/account-actions.js +189 -0
  29. package/public/js/utils/error-handler.js +96 -0
  30. package/public/js/utils/model-config.js +42 -0
  31. package/public/js/utils/validators.js +77 -0
  32. package/public/js/utils.js +69 -0
  33. package/public/views/accounts.html +329 -0
  34. package/public/views/dashboard.html +484 -0
  35. package/public/views/logs.html +97 -0
  36. package/public/views/models.html +331 -0
  37. package/public/views/settings.html +1329 -0
  38. package/src/account-manager/credentials.js +243 -0
  39. package/src/account-manager/index.js +380 -0
  40. package/src/account-manager/onboarding.js +117 -0
  41. package/src/account-manager/rate-limits.js +237 -0
  42. package/src/account-manager/storage.js +136 -0
  43. package/src/account-manager/strategies/base-strategy.js +104 -0
  44. package/src/account-manager/strategies/hybrid-strategy.js +195 -0
  45. package/src/account-manager/strategies/index.js +79 -0
  46. package/src/account-manager/strategies/round-robin-strategy.js +76 -0
  47. package/src/account-manager/strategies/sticky-strategy.js +138 -0
  48. package/src/account-manager/strategies/trackers/health-tracker.js +162 -0
  49. package/src/account-manager/strategies/trackers/index.js +8 -0
  50. package/src/account-manager/strategies/trackers/token-bucket-tracker.js +121 -0
  51. package/src/auth/database.js +169 -0
  52. package/src/auth/oauth.js +419 -0
  53. package/src/auth/token-extractor.js +117 -0
  54. package/src/cli/accounts.js +512 -0
  55. package/src/cli/refresh.js +201 -0
  56. package/src/cli/setup.js +338 -0
  57. package/src/cloudcode/index.js +29 -0
  58. package/src/cloudcode/message-handler.js +386 -0
  59. package/src/cloudcode/model-api.js +248 -0
  60. package/src/cloudcode/rate-limit-parser.js +181 -0
  61. package/src/cloudcode/request-builder.js +93 -0
  62. package/src/cloudcode/session-manager.js +47 -0
  63. package/src/cloudcode/sse-parser.js +121 -0
  64. package/src/cloudcode/sse-streamer.js +293 -0
  65. package/src/cloudcode/streaming-handler.js +492 -0
  66. package/src/config.js +107 -0
  67. package/src/constants.js +278 -0
  68. package/src/errors.js +238 -0
  69. package/src/fallback-config.js +29 -0
  70. package/src/format/content-converter.js +193 -0
  71. package/src/format/index.js +20 -0
  72. package/src/format/request-converter.js +248 -0
  73. package/src/format/response-converter.js +120 -0
  74. package/src/format/schema-sanitizer.js +673 -0
  75. package/src/format/signature-cache.js +88 -0
  76. package/src/format/thinking-utils.js +558 -0
  77. package/src/index.js +146 -0
  78. package/src/modules/usage-stats.js +205 -0
  79. package/src/server.js +861 -0
  80. package/src/utils/claude-config.js +245 -0
  81. package/src/utils/helpers.js +51 -0
  82. package/src/utils/logger.js +142 -0
  83. package/src/utils/native-module-helper.js +162 -0
  84. package/src/webui/index.js +707 -0
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Token Extractor Module
3
+ * Extracts OAuth tokens from Antigravity's SQLite database
4
+ *
5
+ * The database is automatically updated by Antigravity when tokens refresh,
6
+ * so this approach doesn't require any manual intervention.
7
+ */
8
+
9
+ import {
10
+ TOKEN_REFRESH_INTERVAL_MS,
11
+ ANTIGRAVITY_AUTH_PORT
12
+ } from '../constants.js';
13
+ import { getAuthStatus } from './database.js';
14
+ import { logger } from '../utils/logger.js';
15
+
16
+ // Cache for the extracted token
17
+ let cachedToken = null;
18
+ let tokenExtractedAt = null;
19
+
20
+ /**
21
+ * Extract the chat params from Antigravity's HTML page (fallback method)
22
+ */
23
+ async function extractChatParams() {
24
+ try {
25
+ const response = await fetch(`http://127.0.0.1:${ANTIGRAVITY_AUTH_PORT}/`);
26
+ const html = await response.text();
27
+
28
+ // Find the base64-encoded chatParams in the HTML
29
+ const match = html.match(/window\.chatParams\s*=\s*'([^']+)'/);
30
+ if (!match) {
31
+ throw new Error('Could not find chatParams in Antigravity page');
32
+ }
33
+
34
+ // Decode base64
35
+ const base64Data = match[1];
36
+ const jsonString = Buffer.from(base64Data, 'base64').toString('utf-8');
37
+ const config = JSON.parse(jsonString);
38
+
39
+ return config;
40
+ } catch (error) {
41
+ if (error.code === 'ECONNREFUSED') {
42
+ throw new Error(
43
+ `Cannot connect to Antigravity on port ${ANTIGRAVITY_AUTH_PORT}. ` +
44
+ 'Make sure Antigravity is running.'
45
+ );
46
+ }
47
+ throw error;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Get fresh token data - tries DB first, falls back to HTML page
53
+ */
54
+ async function getTokenData() {
55
+ // Try database first (preferred - always has fresh token)
56
+ try {
57
+ const dbData = getAuthStatus();
58
+ if (dbData?.apiKey) {
59
+ logger.info('[Token] Got fresh token from SQLite database');
60
+ return dbData;
61
+ }
62
+ } catch (err) {
63
+ logger.warn('[Token] DB extraction failed, trying HTML page...');
64
+ }
65
+
66
+ // Fallback to HTML page
67
+ try {
68
+ const pageData = await extractChatParams();
69
+ if (pageData?.apiKey) {
70
+ logger.warn('[Token] Got token from HTML page (may be stale)');
71
+ return pageData;
72
+ }
73
+ } catch (err) {
74
+ logger.warn(`[Token] HTML page extraction failed: ${err.message}`);
75
+ }
76
+
77
+ throw new Error(
78
+ 'Could not extract token from Antigravity. ' +
79
+ 'Make sure Antigravity is running and you are logged in.'
80
+ );
81
+ }
82
+
83
+ /**
84
+ * Check if the cached token needs refresh
85
+ */
86
+ function needsRefresh() {
87
+ if (!cachedToken || !tokenExtractedAt) {
88
+ return true;
89
+ }
90
+ return Date.now() - tokenExtractedAt > TOKEN_REFRESH_INTERVAL_MS;
91
+ }
92
+
93
+ /**
94
+ * Get the current OAuth token (with caching)
95
+ */
96
+ export async function getToken() {
97
+ if (needsRefresh()) {
98
+ const data = await getTokenData();
99
+ cachedToken = data.apiKey;
100
+ tokenExtractedAt = Date.now();
101
+ }
102
+ return cachedToken;
103
+ }
104
+
105
+ /**
106
+ * Force refresh the token (useful if requests start failing)
107
+ */
108
+ export async function forceRefresh() {
109
+ cachedToken = null;
110
+ tokenExtractedAt = null;
111
+ return getToken();
112
+ }
113
+
114
+ export default {
115
+ getToken,
116
+ forceRefresh
117
+ };
@@ -0,0 +1,512 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Account Management CLI
5
+ *
6
+ * Interactive CLI for adding and managing Google accounts
7
+ * for the Antigravity Claude Proxy.
8
+ *
9
+ * Usage:
10
+ * node src/cli/accounts.js # Interactive mode
11
+ * node src/cli/accounts.js add # Add new account(s)
12
+ * node src/cli/accounts.js list # List all accounts
13
+ * node src/cli/accounts.js clear # Remove all accounts
14
+ */
15
+
16
+ import { createInterface } from 'readline/promises';
17
+ import { stdin, stdout } from 'process';
18
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
19
+ import { dirname } from 'path';
20
+ import { spawn } from 'child_process';
21
+ import net from 'net';
22
+ import { ACCOUNT_CONFIG_PATH, DEFAULT_PORT, MAX_ACCOUNTS } from '../constants.js';
23
+ import {
24
+ getAuthorizationUrl,
25
+ startCallbackServer,
26
+ completeOAuthFlow,
27
+ refreshAccessToken,
28
+ getUserEmail,
29
+ extractCodeFromInput
30
+ } from '../auth/oauth.js';
31
+
32
+ const SERVER_PORT = process.env.PORT || DEFAULT_PORT;
33
+
34
+ /**
35
+ * Check if the Antigravity Proxy server is running
36
+ * Returns true if port is occupied
37
+ */
38
+ function isServerRunning() {
39
+ return new Promise((resolve) => {
40
+ const socket = new net.Socket();
41
+ socket.setTimeout(1000);
42
+
43
+ socket.on('connect', () => {
44
+ socket.destroy();
45
+ resolve(true); // Server is running
46
+ });
47
+
48
+ socket.on('timeout', () => {
49
+ socket.destroy();
50
+ resolve(false);
51
+ });
52
+
53
+ socket.on('error', (err) => {
54
+ socket.destroy();
55
+ resolve(false); // Port free
56
+ });
57
+
58
+ socket.connect(SERVER_PORT, 'localhost');
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Enforce that server is stopped before proceeding
64
+ */
65
+ async function ensureServerStopped() {
66
+ const isRunning = await isServerRunning();
67
+ if (isRunning) {
68
+ console.error(`
69
+ \x1b[31mError: Antigravity Proxy server is currently running on port ${SERVER_PORT}.\x1b[0m
70
+
71
+ Please stop the server (Ctrl+C) before adding or managing accounts.
72
+ This ensures that your account changes are loaded correctly when you restart the server.
73
+ `);
74
+ process.exit(1);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Create readline interface
80
+ */
81
+ function createRL() {
82
+ return createInterface({ input: stdin, output: stdout });
83
+ }
84
+
85
+ /**
86
+ * Open URL in default browser
87
+ */
88
+ function openBrowser(url) {
89
+ const platform = process.platform;
90
+ let command;
91
+ let args;
92
+
93
+ if (platform === 'darwin') {
94
+ command = 'open';
95
+ args = [url];
96
+ } else if (platform === 'win32') {
97
+ command = 'cmd';
98
+ args = ['/c', 'start', '', url];
99
+ } else {
100
+ command = 'xdg-open';
101
+ args = [url];
102
+ }
103
+
104
+ const child = spawn(command, args, { stdio: 'ignore', detached: true });
105
+ child.on('error', () => {
106
+ console.log('\n⚠ Could not open browser automatically.');
107
+ console.log('Please open this URL manually:', url);
108
+ });
109
+ child.unref();
110
+ }
111
+
112
+ /**
113
+ * Load existing accounts from config
114
+ */
115
+ function loadAccounts() {
116
+ try {
117
+ if (existsSync(ACCOUNT_CONFIG_PATH)) {
118
+ const data = readFileSync(ACCOUNT_CONFIG_PATH, 'utf-8');
119
+ const config = JSON.parse(data);
120
+ return config.accounts || [];
121
+ }
122
+ } catch (error) {
123
+ console.error('Error loading accounts:', error.message);
124
+ }
125
+ return [];
126
+ }
127
+
128
+ /**
129
+ * Save accounts to config
130
+ */
131
+ function saveAccounts(accounts, settings = {}) {
132
+ try {
133
+ const dir = dirname(ACCOUNT_CONFIG_PATH);
134
+ if (!existsSync(dir)) {
135
+ mkdirSync(dir, { recursive: true });
136
+ }
137
+
138
+ const config = {
139
+ accounts: accounts.map(acc => ({
140
+ email: acc.email,
141
+ source: 'oauth',
142
+ refreshToken: acc.refreshToken,
143
+ projectId: acc.projectId,
144
+ addedAt: acc.addedAt || new Date().toISOString(),
145
+ lastUsed: acc.lastUsed || null,
146
+ modelRateLimits: acc.modelRateLimits || {}
147
+ })),
148
+ settings: {
149
+ maxRetries: 5,
150
+ ...settings
151
+ },
152
+ activeIndex: 0
153
+ };
154
+
155
+ writeFileSync(ACCOUNT_CONFIG_PATH, JSON.stringify(config, null, 2));
156
+ console.log(`\n✓ Saved ${accounts.length} account(s) to ${ACCOUNT_CONFIG_PATH}`);
157
+ } catch (error) {
158
+ console.error('Error saving accounts:', error.message);
159
+ throw error;
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Display current accounts
165
+ */
166
+ function displayAccounts(accounts) {
167
+ if (accounts.length === 0) {
168
+ console.log('\nNo accounts configured.');
169
+ return;
170
+ }
171
+
172
+ console.log(`\n${accounts.length} account(s) saved:`);
173
+ accounts.forEach((acc, i) => {
174
+ // Check for any active model-specific rate limits
175
+ const hasActiveLimit = Object.values(acc.modelRateLimits || {}).some(
176
+ limit => limit.isRateLimited && limit.resetTime > Date.now()
177
+ );
178
+ const status = hasActiveLimit ? ' (rate-limited)' : '';
179
+ console.log(` ${i + 1}. ${acc.email}${status}`);
180
+ });
181
+ }
182
+
183
+ /**
184
+ * Add a new account via OAuth with automatic callback
185
+ */
186
+ async function addAccount(existingAccounts) {
187
+ console.log('\n=== Add Google Account ===\n');
188
+
189
+ // Generate authorization URL
190
+ const { url, verifier, state } = getAuthorizationUrl();
191
+
192
+ console.log('Opening browser for Google sign-in...');
193
+ console.log('(If browser does not open, copy this URL manually)\n');
194
+ console.log(` ${url}\n`);
195
+
196
+ // Open browser
197
+ openBrowser(url);
198
+
199
+ // Start callback server and wait for code
200
+ console.log('Waiting for authentication (timeout: 2 minutes)...\n');
201
+
202
+ try {
203
+ const code = await startCallbackServer(state);
204
+
205
+ console.log('Received authorization code. Exchanging for tokens...');
206
+ const result = await completeOAuthFlow(code, verifier);
207
+
208
+ // Check if account already exists
209
+ const existing = existingAccounts.find(a => a.email === result.email);
210
+ if (existing) {
211
+ console.log(`\n⚠ Account ${result.email} already exists. Updating tokens.`);
212
+ existing.refreshToken = result.refreshToken;
213
+ existing.projectId = result.projectId;
214
+ existing.addedAt = new Date().toISOString();
215
+ return null; // Don't add duplicate
216
+ }
217
+
218
+ console.log(`\n✓ Successfully authenticated: ${result.email}`);
219
+ if (result.projectId) {
220
+ console.log(` Project ID: ${result.projectId}`);
221
+ }
222
+
223
+ return {
224
+ email: result.email,
225
+ refreshToken: result.refreshToken,
226
+ projectId: result.projectId,
227
+ addedAt: new Date().toISOString(),
228
+ modelRateLimits: {}
229
+ };
230
+ } catch (error) {
231
+ console.error(`\n✗ Authentication failed: ${error.message}`);
232
+ return null;
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Add a new account via OAuth with manual code input (no-browser mode)
238
+ * For headless servers without a desktop environment
239
+ */
240
+ async function addAccountNoBrowser(existingAccounts, rl) {
241
+ console.log('\n=== Add Google Account (No-Browser Mode) ===\n');
242
+
243
+ // Generate authorization URL
244
+ const { url, verifier, state } = getAuthorizationUrl();
245
+
246
+ console.log('Copy the following URL and open it in a browser on another device:\n');
247
+ console.log(` ${url}\n`);
248
+ console.log('After signing in, you will be redirected to a localhost URL.');
249
+ console.log('Copy the ENTIRE redirect URL or just the authorization code.\n');
250
+
251
+ const input = await rl.question('Paste the callback URL or authorization code: ');
252
+
253
+ try {
254
+ const { code, state: extractedState } = extractCodeFromInput(input);
255
+
256
+ // Validate state if present
257
+ if (extractedState && extractedState !== state) {
258
+ console.log('\n⚠ State mismatch detected. This could indicate a security issue.');
259
+ console.log('Proceeding anyway as this is manual mode...');
260
+ }
261
+
262
+ console.log('\nExchanging authorization code for tokens...');
263
+ const result = await completeOAuthFlow(code, verifier);
264
+
265
+ // Check if account already exists
266
+ const existing = existingAccounts.find(a => a.email === result.email);
267
+ if (existing) {
268
+ console.log(`\n⚠ Account ${result.email} already exists. Updating tokens.`);
269
+ existing.refreshToken = result.refreshToken;
270
+ existing.projectId = result.projectId;
271
+ existing.addedAt = new Date().toISOString();
272
+ return null; // Don't add duplicate
273
+ }
274
+
275
+ console.log(`\n✓ Successfully authenticated: ${result.email}`);
276
+ if (result.projectId) {
277
+ console.log(` Project ID: ${result.projectId}`);
278
+ }
279
+
280
+ return {
281
+ email: result.email,
282
+ refreshToken: result.refreshToken,
283
+ projectId: result.projectId,
284
+ addedAt: new Date().toISOString(),
285
+ modelRateLimits: {}
286
+ };
287
+ } catch (error) {
288
+ console.error(`\n✗ Authentication failed: ${error.message}`);
289
+ return null;
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Interactive remove accounts flow
295
+ */
296
+ async function interactiveRemove(rl) {
297
+ while (true) {
298
+ const accounts = loadAccounts();
299
+ if (accounts.length === 0) {
300
+ console.log('\nNo accounts to remove.');
301
+ return;
302
+ }
303
+
304
+ displayAccounts(accounts);
305
+ console.log('\nEnter account number to remove (or 0 to cancel)');
306
+
307
+ const answer = await rl.question('> ');
308
+ const index = parseInt(answer, 10);
309
+
310
+ if (isNaN(index) || index < 0 || index > accounts.length) {
311
+ console.log('\n❌ Invalid selection.');
312
+ continue;
313
+ }
314
+
315
+ if (index === 0) {
316
+ return; // Exit
317
+ }
318
+
319
+ const removed = accounts[index - 1]; // 1-based to 0-based
320
+ const confirm = await rl.question(`\nAre you sure you want to remove ${removed.email}? [y/N]: `);
321
+
322
+ if (confirm.toLowerCase() === 'y') {
323
+ accounts.splice(index - 1, 1);
324
+ saveAccounts(accounts);
325
+ console.log(`\n✓ Removed ${removed.email}`);
326
+ } else {
327
+ console.log('\nCancelled.');
328
+ }
329
+
330
+ const removeMore = await rl.question('\nRemove another account? [y/N]: ');
331
+ if (removeMore.toLowerCase() !== 'y') {
332
+ break;
333
+ }
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Interactive add accounts flow (Main Menu)
339
+ * @param {Object} rl - readline interface
340
+ * @param {boolean} noBrowser - if true, use manual code input mode
341
+ */
342
+ async function interactiveAdd(rl, noBrowser = false) {
343
+ if (noBrowser) {
344
+ console.log('\n📋 No-browser mode: You will manually paste the authorization code.\n');
345
+ }
346
+
347
+ const accounts = loadAccounts();
348
+
349
+ if (accounts.length > 0) {
350
+ displayAccounts(accounts);
351
+
352
+ const choice = await rl.question('\n(a)dd new, (r)emove existing, (f)resh start, or (e)xit? [a/r/f/e]: ');
353
+ const c = choice.toLowerCase();
354
+
355
+ if (c === 'r') {
356
+ await interactiveRemove(rl);
357
+ return; // Return to main or exit? Given this is "add", we probably exit after sub-task.
358
+ } else if (c === 'f') {
359
+ console.log('\nStarting fresh - existing accounts will be replaced.');
360
+ accounts.length = 0;
361
+ } else if (c === 'a') {
362
+ console.log('\nAdding to existing accounts.');
363
+ } else if (c === 'e') {
364
+ console.log('\nExiting...');
365
+ return; // Exit cleanly
366
+ } else {
367
+ console.log('\nInvalid choice, defaulting to add.');
368
+ }
369
+ }
370
+
371
+ // Add single account
372
+ if (accounts.length >= MAX_ACCOUNTS) {
373
+ console.log(`\nMaximum of ${MAX_ACCOUNTS} accounts reached.`);
374
+ return;
375
+ }
376
+
377
+ // Use appropriate add function based on mode
378
+ const newAccount = noBrowser
379
+ ? await addAccountNoBrowser(accounts, rl)
380
+ : await addAccount(accounts);
381
+
382
+ if (newAccount) {
383
+ accounts.push(newAccount);
384
+ saveAccounts(accounts);
385
+ } else if (accounts.length > 0) {
386
+ // Even if newAccount is null (duplicate update), save the updated accounts
387
+ saveAccounts(accounts);
388
+ }
389
+
390
+ if (accounts.length > 0) {
391
+ displayAccounts(accounts);
392
+ console.log('\nTo add more accounts, run this command again.');
393
+ } else {
394
+ console.log('\nNo accounts to save.');
395
+ }
396
+ }
397
+
398
+ /**
399
+ * List accounts
400
+ */
401
+ async function listAccounts() {
402
+ const accounts = loadAccounts();
403
+ displayAccounts(accounts);
404
+
405
+ if (accounts.length > 0) {
406
+ console.log(`\nConfig file: ${ACCOUNT_CONFIG_PATH}`);
407
+ }
408
+ }
409
+
410
+ /**
411
+ * Clear all accounts
412
+ */
413
+ async function clearAccounts(rl) {
414
+ const accounts = loadAccounts();
415
+
416
+ if (accounts.length === 0) {
417
+ console.log('No accounts to clear.');
418
+ return;
419
+ }
420
+
421
+ displayAccounts(accounts);
422
+
423
+ const confirm = await rl.question('\nAre you sure you want to remove all accounts? [y/N]: ');
424
+ if (confirm.toLowerCase() === 'y') {
425
+ saveAccounts([]);
426
+ console.log('All accounts removed.');
427
+ } else {
428
+ console.log('Cancelled.');
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Verify accounts (test refresh tokens)
434
+ */
435
+ async function verifyAccounts() {
436
+ const accounts = loadAccounts();
437
+
438
+ if (accounts.length === 0) {
439
+ console.log('No accounts to verify.');
440
+ return;
441
+ }
442
+
443
+ console.log('\nVerifying accounts...\n');
444
+
445
+ for (const account of accounts) {
446
+ try {
447
+ const tokens = await refreshAccessToken(account.refreshToken);
448
+ const email = await getUserEmail(tokens.accessToken);
449
+ console.log(` ✓ ${email} - OK`);
450
+ } catch (error) {
451
+ console.log(` ✗ ${account.email} - ${error.message}`);
452
+ }
453
+ }
454
+ }
455
+
456
+ /**
457
+ * Main CLI
458
+ */
459
+ async function main() {
460
+ const args = process.argv.slice(2);
461
+ const command = args[0] || 'add';
462
+ const noBrowser = args.includes('--no-browser');
463
+
464
+ console.log('╔════════════════════════════════════════╗');
465
+ console.log('║ Antigravity Proxy Account Manager ║');
466
+ console.log('║ Use --no-browser for headless mode ║');
467
+ console.log('╚════════════════════════════════════════╝');
468
+
469
+ const rl = createRL();
470
+
471
+ try {
472
+ switch (command) {
473
+ case 'add':
474
+ await ensureServerStopped();
475
+ await interactiveAdd(rl, noBrowser);
476
+ break;
477
+ case 'list':
478
+ await listAccounts();
479
+ break;
480
+ case 'clear':
481
+ await ensureServerStopped();
482
+ await clearAccounts(rl);
483
+ break;
484
+ case 'verify':
485
+ await verifyAccounts();
486
+ break;
487
+ case 'help':
488
+ console.log('\nUsage:');
489
+ console.log(' node src/cli/accounts.js add Add new account(s)');
490
+ console.log(' node src/cli/accounts.js list List all accounts');
491
+ console.log(' node src/cli/accounts.js verify Verify account tokens');
492
+ console.log(' node src/cli/accounts.js clear Remove all accounts');
493
+ console.log(' node src/cli/accounts.js help Show this help');
494
+ console.log('\nOptions:');
495
+ console.log(' --no-browser Manual authorization code input (for headless servers)');
496
+ break;
497
+ case 'remove':
498
+ await ensureServerStopped();
499
+ await interactiveRemove(rl);
500
+ break;
501
+ default:
502
+ console.log(`Unknown command: ${command}`);
503
+ console.log('Run with "help" for usage information.');
504
+ }
505
+ } finally {
506
+ rl.close();
507
+ // Force exit to prevent hanging
508
+ process.exit(0);
509
+ }
510
+ }
511
+
512
+ main().catch(console.error);