@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.
- package/LICENSE +21 -0
- package/README.md +622 -0
- package/bin/cli.js +124 -0
- package/package.json +80 -0
- package/public/app.js +228 -0
- package/public/css/src/input.css +523 -0
- package/public/css/style.css +1 -0
- package/public/favicon.svg +10 -0
- package/public/index.html +381 -0
- package/public/js/components/account-manager.js +245 -0
- package/public/js/components/claude-config.js +420 -0
- package/public/js/components/dashboard/charts.js +589 -0
- package/public/js/components/dashboard/filters.js +362 -0
- package/public/js/components/dashboard/stats.js +110 -0
- package/public/js/components/dashboard.js +236 -0
- package/public/js/components/logs-viewer.js +100 -0
- package/public/js/components/models.js +36 -0
- package/public/js/components/server-config.js +349 -0
- package/public/js/config/constants.js +102 -0
- package/public/js/data-store.js +386 -0
- package/public/js/settings-store.js +58 -0
- package/public/js/store.js +78 -0
- package/public/js/translations/en.js +351 -0
- package/public/js/translations/id.js +396 -0
- package/public/js/translations/pt.js +287 -0
- package/public/js/translations/tr.js +342 -0
- package/public/js/translations/zh.js +357 -0
- package/public/js/utils/account-actions.js +189 -0
- package/public/js/utils/error-handler.js +96 -0
- package/public/js/utils/model-config.js +42 -0
- package/public/js/utils/validators.js +77 -0
- package/public/js/utils.js +69 -0
- package/public/views/accounts.html +329 -0
- package/public/views/dashboard.html +484 -0
- package/public/views/logs.html +97 -0
- package/public/views/models.html +331 -0
- package/public/views/settings.html +1329 -0
- package/src/account-manager/credentials.js +243 -0
- package/src/account-manager/index.js +380 -0
- package/src/account-manager/onboarding.js +117 -0
- package/src/account-manager/rate-limits.js +237 -0
- package/src/account-manager/storage.js +136 -0
- package/src/account-manager/strategies/base-strategy.js +104 -0
- package/src/account-manager/strategies/hybrid-strategy.js +195 -0
- package/src/account-manager/strategies/index.js +79 -0
- package/src/account-manager/strategies/round-robin-strategy.js +76 -0
- package/src/account-manager/strategies/sticky-strategy.js +138 -0
- package/src/account-manager/strategies/trackers/health-tracker.js +162 -0
- package/src/account-manager/strategies/trackers/index.js +8 -0
- package/src/account-manager/strategies/trackers/token-bucket-tracker.js +121 -0
- package/src/auth/database.js +169 -0
- package/src/auth/oauth.js +419 -0
- package/src/auth/token-extractor.js +117 -0
- package/src/cli/accounts.js +512 -0
- package/src/cli/refresh.js +201 -0
- package/src/cli/setup.js +338 -0
- package/src/cloudcode/index.js +29 -0
- package/src/cloudcode/message-handler.js +386 -0
- package/src/cloudcode/model-api.js +248 -0
- package/src/cloudcode/rate-limit-parser.js +181 -0
- package/src/cloudcode/request-builder.js +93 -0
- package/src/cloudcode/session-manager.js +47 -0
- package/src/cloudcode/sse-parser.js +121 -0
- package/src/cloudcode/sse-streamer.js +293 -0
- package/src/cloudcode/streaming-handler.js +492 -0
- package/src/config.js +107 -0
- package/src/constants.js +278 -0
- package/src/errors.js +238 -0
- package/src/fallback-config.js +29 -0
- package/src/format/content-converter.js +193 -0
- package/src/format/index.js +20 -0
- package/src/format/request-converter.js +248 -0
- package/src/format/response-converter.js +120 -0
- package/src/format/schema-sanitizer.js +673 -0
- package/src/format/signature-cache.js +88 -0
- package/src/format/thinking-utils.js +558 -0
- package/src/index.js +146 -0
- package/src/modules/usage-stats.js +205 -0
- package/src/server.js +861 -0
- package/src/utils/claude-config.js +245 -0
- package/src/utils/helpers.js +51 -0
- package/src/utils/logger.js +142 -0
- package/src/utils/native-module-helper.js +162 -0
- 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);
|